Skip to content

Commit

Permalink
Add stricter types for API init
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Wielander committed Oct 12, 2020
1 parent 435ea6d commit c3e4295
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 65 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/node_modules
/dist
**/build/**
/test/cypress
28 changes: 14 additions & 14 deletions lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import createEditor from './editor'
import createNavigator from './navigator'
import createApp from './app'
import locations from './locations'
import { BaseExtensionSDK, EntryFieldInfo, FieldInfo, NavigatorAPI, KnownSDK } from './types'
import { BaseExtensionSDK, EntryFieldInfo, NavigatorAPI, KnownSDK, ConnectMessage } from './types'
import { Channel } from './channel'

const DEFAULT_API_PRODUCERS = [
Expand All @@ -19,8 +19,8 @@ const DEFAULT_API_PRODUCERS = [
makeWindowAPI
]

type producerFunc = (channel: Channel, data: any, currentWindow: Window) => any
const LOCATION_TO_API_PRODUCERS: { [location: string]: producerFunc[] } = {
type ProducerFunc = (channel: Channel, data: ConnectMessage, currentWindow: Window) => any
const LOCATION_TO_API_PRODUCERS: { [location: string]: ProducerFunc[] } = {
[locations.LOCATION_ENTRY_FIELD]: DEFAULT_API_PRODUCERS,
[locations.LOCATION_ENTRY_FIELD_SIDEBAR]: DEFAULT_API_PRODUCERS,
[locations.LOCATION_ENTRY_SIDEBAR]: [makeSharedAPI, makeEntryAPI, makeEditorAPI, makeWindowAPI],
Expand All @@ -30,17 +30,19 @@ const LOCATION_TO_API_PRODUCERS: { [location: string]: producerFunc[] } = {
[locations.LOCATION_APP_CONFIG]: [makeSharedAPI, makeAppAPI]
}

// TODO Remove any once we know payload
// TODO data is directly received through event passthrough
export default function createAPI(channel: Channel, data: any, currentWindow: Window): KnownSDK {
export default function createAPI(
channel: Channel,
data: ConnectMessage,
currentWindow: Window
): KnownSDK {
const producers = LOCATION_TO_API_PRODUCERS[data.location as string] || DEFAULT_API_PRODUCERS

return producers.reduce((api, produce) => {
return { ...api, ...produce(channel, data, currentWindow) }
}, {}) as any
}

function makeSharedAPI(channel: Channel, data: any): BaseExtensionSDK {
function makeSharedAPI(channel: Channel, data: ConnectMessage): BaseExtensionSDK {
const { user, parameters, locales, ids, initialContentTypes } = data
const currentLocation = data.location || locations.LOCATION_ENTRY_FIELD

Expand Down Expand Up @@ -89,12 +91,7 @@ function makeEditorAPI(channel: Channel, data: any) {

function makeEntryAPI(
channel: Channel,
{
locales,
contentType,
entry,
fieldInfo
}: { locales: any; contentType: string; entry: any; fieldInfo: any }
{ locales, contentType, entry, fieldInfo }: ConnectMessage
) {
const createEntryField = (info: EntryFieldInfo) => new Field(channel, info, locales.default)

Expand All @@ -104,7 +101,10 @@ function makeEntryAPI(
}
}

function makeFieldAPI(channel: Channel, { field }: { field: FieldInfo }) {
function makeFieldAPI(channel: Channel, { field }: ConnectMessage) {
if (!field) {
throw new Error('FieldAPI called for location without "field" property defined.')
}
return {
field: new FieldLocale(channel, field)
}
Expand Down
7 changes: 3 additions & 4 deletions lib/channel.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Signal } from './signal'
import { ConnectMessage } from './types'

// TODO derive arg1 and arg2 shape of onConnect callback
// TODO Both come through initial event.data over Channel
export default function connect(
currentWindow: Window,
onConnect: (arg1: Channel, args2: any, arg3: any) => void
onConnect: (channel: Channel, message: ConnectMessage, messageQueue: unknown[]) => void
) {
waitForConnect(currentWindow, (params: any, messageQueue: any) => {
waitForConnect(currentWindow, (params: ConnectMessage, messageQueue: unknown[]) => {
const channel = new Channel(params.id, currentWindow)
onConnect(channel, params, messageQueue)
})
Expand Down
5 changes: 2 additions & 3 deletions lib/editor.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { EditorInterface } from '../typings'
import { EditorInterface, EditorLocaleSettings } from './types'
import { Channel } from './channel'
import { MemoizedSignal } from './signal'

export default function createEditor(channel: Channel, editorInterface: EditorInterface) {
const _localeSettingsSygnal = new MemoizedSignal(undefined)
const _showDisabledFieldsSygnal = new MemoizedSignal(undefined)

// TODO any
channel.addHandler('localeSettingsChanged', (settings: any) => {
channel.addHandler('localeSettingsChanged', (settings: EditorLocaleSettings) => {
_localeSettingsSygnal.dispatch(settings)
})

Expand Down
9 changes: 5 additions & 4 deletions lib/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,18 @@ export default class Field implements EntryFieldAPI {
return this.setValue(undefined, locale)
}

// TODO maybe swap argument positions to get rid of swap logic
onValueChanged(locale: string | Function, handler?: Function) {
onValueChanged(locale: string | ((value: any) => void), handler?: (value: any) => void) {
const h = handler || locale
if (!handler) {
locale = ''
}
return this._getFieldLocale(locale as string).onValueChanged(h as any)
}

// TODO maybe swap argument positions to get rid of swap logic
onIsDisabledChanged(locale: string | Function, handler?: Function) {
onIsDisabledChanged(
locale: string | ((isDisabled: boolean) => void),
handler?: (isDisabled: boolean) => void
) {
const h = handler || locale
if (!handler) {
locale = ''
Expand Down
51 changes: 27 additions & 24 deletions lib/initialize.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { KnownSDK } from './types'
import { ConnectMessage, KnownSDK } from './types'
import connect, { Channel } from './channel'

export default function createInitializer(
currentWindow: Window,
apiCreator: (channel: Channel, data: any, window: Window) => KnownSDK
): <T extends KnownSDK = KnownSDK>(initCallback: (sdk: T) => any) => void {
apiCreator: (channel: Channel, data: ConnectMessage, window: Window) => KnownSDK
) {
const connectDeferred = createDeferred()

connectDeferred.promise.then(([channel]: [Channel]) => {
Expand All @@ -20,29 +20,32 @@ export default function createInitializer(
(...args) => connectDeferred.resolve(args)
)

// TODO Replace sdk/customSdk [any] with api and customApi types
return function init(initCb: (sdk: any, customSdk: any) => any, { makeCustomApi = null } = {}) {
connectDeferred.promise.then(([channel, params, messageQueue]: [Channel, any, any]) => {
const api = apiCreator(channel, params, currentWindow)
return function init(
initCb: (sdk: KnownSDK, customSdk: any) => any,
{ makeCustomApi = null } = {}
) {
connectDeferred.promise.then(
([channel, params, messageQueue]: [Channel, ConnectMessage, unknown[]]) => {
const api = apiCreator(channel, params, currentWindow)

let customApi
if (typeof makeCustomApi === 'function') {
// Reason for the typecast: https://github.com/microsoft/TypeScript/issues/14889
customApi = (makeCustomApi as any)(channel, params)
}
let customApi
if (typeof makeCustomApi === 'function') {
// Reason for the typecast: https://github.com/microsoft/TypeScript/issues/14889
customApi = (makeCustomApi as any)(channel, params)
}

// Handle pending incoming messages.
// APIs are created before so handlers are already
// registered on the channel.
messageQueue.forEach(m => {
// TODO Expose private handleMessage method
;(channel as any)._handleMessage(m)
})

// Handle pending incoming messages.
// APIs are created before so handlers are already
// registered on the channel.
// TODO message has "id" or "method" attribute defined
messageQueue.forEach((m: any) => {
// TODO Expose private handleMessage method
;(channel as any)._handleMessage(m)
})

// Hand over control to the developer.
initCb(api, customApi)
})
// Hand over control to the developer.
initCb(api, customApi)
}
)
}
}

Expand Down
3 changes: 1 addition & 2 deletions lib/space.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ContentType } from '../typings'
import { ContentType, SpaceAPI } from './types'
import { Channel } from './channel'
import { SpaceAPI } from './types'

const spaceMethods: Array<keyof SpaceAPI> = [
'getContentType',
Expand Down
32 changes: 25 additions & 7 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ export interface EntryFieldInfo {
values: { [locale: string]: any }
}

export interface ConnectMessage {
id: string
location: Location[keyof Location]
parameters: ParametersAPI
locales: LocalesAPI
user: User
initialContentTypes: ContentType[]
ids: IdsAPI
contentType: ContentType
editorInterface?: EditorInterface
entry: {
sys: EntrySys
metadata?: any
}
fieldInfo: EntryFieldInfo[]
field?: FieldInfo
}

/* Field API */
export interface FieldAPI {
/** The ID of a field is defined in an entry's content type. */
Expand Down Expand Up @@ -569,16 +587,16 @@ export interface IdsAPI {
contentType: string
}

export interface EditorLocaleSettings {
mode: 'multi' | 'single'
focused?: string
active?: Array<string>
}

export interface SharedEditorSDK {
editor: {
editorInterface: EditorInterface
onLocaleSettingsChanged: (
callback: (value: {
mode: 'multi' | 'single'
focused?: string
active?: Array<string>
}) => any
) => Function
onLocaleSettingsChanged: (callback: (value: EditorLocaleSettings) => any) => Function
onShowDisabledFieldsChanged: (callback: (value: boolean) => any) => Function
}
/** Allows to read and update the value of any field of the current entry and to get the entry's metadata */
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
],
"scripts": {
"test": "ts-mocha 'test/unit/*.[jt]s'",
"lint": "tsc -b && eslint '{lib,test}/**/*.{t,j}s'",
"lint:fix": "eslint --fix lib/ test/",
"lint": "eslint '{lib,test}/**/*.{t,j}s'",
"lint:fix": "npm run lint -- --fix",
"build": "npm run typings && rollup -c --compact",
"build:debug": "npm run build -- --sourcemap",
"typings": "tsc -b typings.tsconfig.json && mv lib/types.d.ts typings.d.ts && cat module-declaration.d.ts >> typings.d.ts",
Expand Down
10 changes: 5 additions & 5 deletions test/unit/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { makeDOM, mockMutationObserver, expect } from '../helpers'

import createAPI from '../../lib/api'
import locations from '../../lib/locations'
import { AppExtensionSDK, ConnectMessage } from '../../lib/types'

const sharedExpected = [
'location',
Expand All @@ -19,7 +20,7 @@ const sharedExpected = [
function test(expected: string[], location: string | undefined, expectedLocation = location) {
const channel = { addHandler: () => {} } as any

const data = {
const data = ({
location,
user: 'USER',
parameters: 'PARAMS',
Expand All @@ -45,7 +46,7 @@ function test(expected: string[], location: string | undefined, expectedLocation
ids: {
extension: 'my-test-id'
}
}
} as unknown) as ConnectMessage

const dom = makeDOM()
mockMutationObserver(dom, () => {})
Expand Down Expand Up @@ -110,10 +111,9 @@ describe('createAPI()', () => {
it('returns correct shape of the app API (app)', () => {
const expected = ['app']

const api = test(expected, locations.LOCATION_APP_CONFIG)
const api = (test(expected, locations.LOCATION_APP_CONFIG) as unknown) as AppExtensionSDK

// TODO app not on this object?
expect((api as any).app).to.have.all.keys([
expect(api.app).to.have.all.keys([
'setReady',
'isInstalled',
'getParameters',
Expand Down

0 comments on commit c3e4295

Please sign in to comment.