From 5ab91a98aa0392be942cf2c4706223dd8c9202d0 Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Fri, 17 Nov 2017 19:27:54 -0500 Subject: [PATCH] Adds type annotations for ember-glimmer macro implementations --- packages/ember-glimmer/lib/syntax.ts | 17 ++++-- .../ember-glimmer/lib/syntax/-text-area.ts | 4 +- packages/ember-glimmer/lib/syntax/input.ts | 11 ++-- packages/ember-glimmer/lib/syntax/mount.ts | 43 +++++++++------ packages/ember-glimmer/lib/syntax/outlet.ts | 55 ++++++++++++------- packages/ember-glimmer/lib/syntax/render.ts | 19 +++++-- packages/ember-glimmer/lib/syntax/utils.ts | 4 +- packages/ember-glimmer/lib/utils/bindings.ts | 14 +++-- packages/ember-glimmer/lib/views/outlet.ts | 4 +- 9 files changed, 111 insertions(+), 60 deletions(-) diff --git a/packages/ember-glimmer/lib/syntax.ts b/packages/ember-glimmer/lib/syntax.ts index 1675048a9a9..d12e6a7dac2 100644 --- a/packages/ember-glimmer/lib/syntax.ts +++ b/packages/ember-glimmer/lib/syntax.ts @@ -1,18 +1,22 @@ +import { InlineMacros, BlockMacros, OpcodeBuilderDSL, Block } from '@glimmer/runtime'; +import * as WireFormat from '@glimmer/wire-format'; import { assert } from 'ember-debug'; import { textAreaMacro } from './syntax/-text-area'; import { blockComponentMacro, inlineComponentMacro, } from './syntax/dynamic-component'; +import Environment from './environment'; import { inputMacro } from './syntax/input'; import { mountMacro } from './syntax/mount'; import { outletMacro } from './syntax/outlet'; import { renderMacro } from './syntax/render'; import { hashToArgs } from './syntax/utils'; import { wrapComponentClassAttribute } from './utils/bindings'; +import { Option } from '@glimmer/util'; -function refineInlineSyntax(name: string, params: any[], hash: any, builder: any) { - assert(`You attempted to overwrite the built-in helper "${name}" which is not allowed. Please rename the helper.`, !(builder.env.builtInHelpers[name] && builder.env.owner.hasRegistration(`helper:${name}`))); +function refineInlineSyntax(name: string, params: WireFormat.Core.Params, hash: WireFormat.Core.Hash, builder: OpcodeBuilderDSL) { + assert(`You attempted to overwrite the built-in helper "${name}" which is not allowed. Please rename the helper.`, !((builder.env as Environment).builtInHelpers[name] && (builder.env as Environment).owner.hasRegistration(`helper:${name}`))); let definition; if (name.indexOf('-') > -1) { @@ -28,7 +32,7 @@ function refineInlineSyntax(name: string, params: any[], hash: any, builder: any return false; } -function refineBlockSyntax(name: string, params: any[], hash: any, _default: any, inverse: any, builder: any) { +function refineBlockSyntax(name: string, params: WireFormat.Core.Params, hash: WireFormat.Core.Hash, _default: Option, inverse: Option, builder: OpcodeBuilderDSL) { if (name.indexOf('-') === -1) { return false; } @@ -53,16 +57,17 @@ function refineBlockSyntax(name: string, params: any[], hash: any, _default: any return false; } -export const experimentalMacros: any[] = []; +export type ExperimentalMacro = (blocks: BlockMacros, inlines: InlineMacros) => void; +export const experimentalMacros: ExperimentalMacro[] = []; // This is a private API to allow for experimental macros // to be created in user space. Registering a macro should // should be done in an initializer. -export function registerMacros(macro: any) { +export function registerMacros(macro: ExperimentalMacro) { experimentalMacros.push(macro); } -export function populateMacros(blocks: any, inlines: any) { +export function populateMacros(blocks: BlockMacros, inlines: InlineMacros) { inlines.add('outlet', outletMacro); inlines.add('component', inlineComponentMacro); inlines.add('render', renderMacro); diff --git a/packages/ember-glimmer/lib/syntax/-text-area.ts b/packages/ember-glimmer/lib/syntax/-text-area.ts index d1139df630f..16e39ac22a3 100644 --- a/packages/ember-glimmer/lib/syntax/-text-area.ts +++ b/packages/ember-glimmer/lib/syntax/-text-area.ts @@ -1,7 +1,9 @@ +import { OpcodeBuilderDSL } from '@glimmer/runtime'; +import * as WireFormat from '@glimmer/wire-format'; import { wrapComponentClassAttribute } from '../utils/bindings'; import { hashToArgs } from './utils'; -export function textAreaMacro(_name: string, params: any[], hash: any, builder: any) { +export function textAreaMacro(_name: string, params: WireFormat.Core.Params, hash: WireFormat.Core.Hash, builder: OpcodeBuilderDSL) { let definition = builder.env.getComponentDefinition('-text-area', builder.meta.templateMeta); wrapComponentClassAttribute(hash); builder.component.static(definition, [params, hashToArgs(hash), null, null]); diff --git a/packages/ember-glimmer/lib/syntax/input.ts b/packages/ember-glimmer/lib/syntax/input.ts index d1b7e519301..b4914b8b5db 100644 --- a/packages/ember-glimmer/lib/syntax/input.ts +++ b/packages/ember-glimmer/lib/syntax/input.ts @@ -1,12 +1,15 @@ /** @module ember */ +import * as WireFormat from '@glimmer/wire-format'; import { assert } from 'ember-debug'; import { wrapComponentClassAttribute } from '../utils/bindings'; import { dynamicComponentMacro } from './dynamic-component'; import { hashToArgs } from './utils'; +import { OpcodeBuilderDSL } from '@glimmer/runtime'; +import { unwrap } from '@glimmer/util'; -function buildSyntax(type: string, params: any[], hash: any, builder: any) { +function buildSyntax(type: string, params: WireFormat.Core.Params, hash: WireFormat.Core.Hash, builder: OpcodeBuilderDSL) { let definition = builder.env.getComponentDefinition(type, builder.meta.templateMeta); builder.component.static(definition, [params, hashToArgs(hash), null, null]); return true; @@ -148,7 +151,7 @@ function buildSyntax(type: string, params: any[], hash: any, builder: any) { @public */ -export function inputMacro(_name: string, params: any[], hash: any[], builder: any) { +export function inputMacro(_name: string, params: WireFormat.Core.Params, hash: WireFormat.Core.Hash, builder: OpcodeBuilderDSL): boolean { let keys; let values; let typeIndex = -1; @@ -164,7 +167,7 @@ export function inputMacro(_name: string, params: any[], hash: any[], builder: a if (!params) { params = []; } if (typeIndex > -1) { - let typeArg = values[typeIndex]; + let typeArg = unwrap(values)[typeIndex]; if (Array.isArray(typeArg)) { return dynamicComponentMacro(params, hash, null, null, builder); } else if (typeArg === 'checkbox') { @@ -173,7 +176,7 @@ export function inputMacro(_name: string, params: any[], hash: any[], builder: a 'you must use `checked=someBooleanValue` instead.', valueIndex === -1, ); - wrapComponentClassAttribute(hash); + wrapComponentClassAttribute(unwrap(hash)); return buildSyntax('-checkbox', params, hash, builder); } } diff --git a/packages/ember-glimmer/lib/syntax/mount.ts b/packages/ember-glimmer/lib/syntax/mount.ts index bc636b0c5bb..d96aae7c33d 100644 --- a/packages/ember-glimmer/lib/syntax/mount.ts +++ b/packages/ember-glimmer/lib/syntax/mount.ts @@ -3,19 +3,24 @@ */ import { Arguments, - VM + VM, + OpcodeBuilderDSL, + ComponentArgs } from '@glimmer/runtime'; +import * as WireFormat from '@glimmer/wire-format'; import { assert } from 'ember-debug'; import { EMBER_ENGINES_MOUNT_PARAMS } from 'ember/features'; import Environment from '../environment'; import { MountDefinition } from '../component-managers/mount'; import { hashToArgs } from './utils'; +import { Option } from '@glimmer/util'; +import { Tag, PathReference } from '@glimmer/reference'; -function dynamicEngineFor(vm: VM, args: Arguments, meta: any) { - let env = vm.env; - let nameRef = args.positional.at(0); +function dynamicEngineFor(vm: VM, args: Arguments) { + let env = vm.env as Environment; + let nameRef = args.positional.at(0) as PathReference; - return new DynamicEngineReference({ nameRef, env, meta }); + return new DynamicEngineReference({ nameRef, env }); } /** @@ -59,7 +64,7 @@ function dynamicEngineFor(vm: VM, args: Arguments, meta: any) { @category ember-application-engines @public */ -export function mountMacro(_name: string, params: any[], hash: any, builder: any) { +export function mountMacro(_name: string, params: WireFormat.Core.Params, hash: WireFormat.Core.Hash, builder: OpcodeBuilderDSL) { if (EMBER_ENGINES_MOUNT_PARAMS) { assert( 'You can only pass a single positional argument to the {{mount}} helper, e.g. {{mount "chat-engine"}}.', @@ -72,30 +77,32 @@ export function mountMacro(_name: string, params: any[], hash: any, builder: any ); } - let definitionArgs = [params.slice(0, 1), null, null, null]; - let args = [null, hashToArgs(hash), null, null]; + let definitionArgs: ComponentArgs = [params.slice(0, 1), null, null, null]; + let args: ComponentArgs = [[], hashToArgs(hash), null, null]; builder.component.dynamic(definitionArgs, dynamicEngineFor, args); return true; } class DynamicEngineReference { - public tag: any; - public nameRef: any; + public tag: Tag; + public nameRef: PathReference; public env: Environment; - public meta: any; - private _lastName: any; - private _lastDef: any; - constructor({ nameRef, env, meta }: any) { + private _lastName: Option; + private _lastDef: Option; + constructor({ nameRef, env }: { nameRef: PathReference, env: Environment }) { this.tag = nameRef.tag; this.nameRef = nameRef; this.env = env; - this.meta = meta; - this._lastName = undefined; - this._lastDef = undefined; + this._lastName = null; + this._lastDef = null; + } + + get(): never { + throw new Error('unexpected get on DynamicEngineReference'); } value() { - let { env, nameRef /*meta*/ } = this; + let { env, nameRef } = this; let nameOrDef = nameRef.value(); if (typeof nameOrDef === 'string') { diff --git a/packages/ember-glimmer/lib/syntax/outlet.ts b/packages/ember-glimmer/lib/syntax/outlet.ts index 1a4e190b024..ee0e99d34d0 100644 --- a/packages/ember-glimmer/lib/syntax/outlet.ts +++ b/packages/ember-glimmer/lib/syntax/outlet.ts @@ -3,47 +3,57 @@ import { ConstReference, TagWrapper, UpdatableTag, + PathReference, + Tag, } from '@glimmer/reference'; import { Arguments, VM, + OpcodeBuilderDSL, + ComponentArgs, } from '@glimmer/runtime'; +import * as WireFormat from '@glimmer/wire-format'; +import { Option, unwrap } from '@glimmer/util'; + import { OutletComponentDefinition } from '../component-managers/outlet'; import { DynamicScope } from '../renderer'; +import { OutletState } from '../views/outlet'; class OutletComponentReference { - public outletNameRef: any; - public parentOutletStateRef: any; - public definition: any; - public lastState: any; + public outletNameRef: PathReference; + public parentOutletStateRef: PathReference>; + public definition: Option; + public lastState: Option; public outletStateTag: TagWrapper; - public tag: any; + public tag: Tag; - constructor(outletNameRef: any, parentOutletStateRef: any) { + constructor(outletNameRef: PathReference, parentOutletStateRef: PathReference>) { this.outletNameRef = outletNameRef; this.parentOutletStateRef = parentOutletStateRef; this.definition = null; this.lastState = null; let outletStateTag = this.outletStateTag = UpdatableTag.create(parentOutletStateRef.tag); - this.tag = combine([outletStateTag.inner, outletNameRef.tag]); + this.tag = combine([outletStateTag, outletNameRef.tag]); + } + + get(): never { + throw new Error('unexpected get on OutletComponentReference'); } value() { let { outletNameRef, parentOutletStateRef, definition, lastState } = this; let outletName = outletNameRef.value(); - let outletStateRef = parentOutletStateRef.get('outlets').get(outletName); + let outletStateRef = parentOutletStateRef.get('outlets').get(outletName) as PathReference>; let newState = this.lastState = outletStateRef.value(); this.outletStateTag.inner.update(outletStateRef.tag); definition = revalidate(definition, lastState, newState); - let hasTemplate = newState && newState.render.template; - if (definition) { return definition; - } else if (hasTemplate) { + } else if (newState && newState.render && newState.render.template) { return this.definition = new OutletComponentDefinition(outletName, newState.render.template); } else { return this.definition = null; @@ -51,7 +61,7 @@ class OutletComponentReference { } } -function revalidate(definition: any, lastState: any, newState: any) { +function revalidate(definition: Option, lastState: Option, newState: Option): Option { if (!lastState && !newState) { return definition; } @@ -60,16 +70,22 @@ function revalidate(definition: any, lastState: any, newState: any) { return null; } - if ( - newState.render.template === lastState.render.template && - newState.render.controller === lastState.render.controller - ) { + if (outletStatesEqual(unwrap(lastState), unwrap(newState))) { return definition; } return null; } +function outletStatesEqual(lastState: OutletState, newState: OutletState): boolean { + if (!lastState.render || !newState.render) { return false; } + + return ( + newState.render.template === lastState.render.template && + newState.render.controller === lastState.render.controller + ); +} + function outletComponentFor(vm: VM, args: Arguments) { let { outletState } = vm.dynamicScope() as DynamicScope; @@ -133,12 +149,13 @@ function outletComponentFor(vm: VM, args: Arguments) { @for Ember.Templates.helpers @public */ -export function outletMacro(_name: string, params: any[], _hash: any[], builder: any) { +export function outletMacro(_name: string, params: Option, _hash: Option, builder: OpcodeBuilderDSL) { if (!params) { params = []; } - let definitionArgs = [params.slice(0, 1), null, null, null]; - let emptyArgs = [[], null, null, null]; // FIXME + + let definitionArgs: ComponentArgs = [params.slice(0,1), null, null, null]; + let emptyArgs: ComponentArgs = [[], null, null, null]; // FIXME builder.component.dynamic(definitionArgs, outletComponentFor, emptyArgs); return true; } diff --git a/packages/ember-glimmer/lib/syntax/render.ts b/packages/ember-glimmer/lib/syntax/render.ts index f19dfb608a5..3de96227c34 100644 --- a/packages/ember-glimmer/lib/syntax/render.ts +++ b/packages/ember-glimmer/lib/syntax/render.ts @@ -6,7 +6,11 @@ import { ConstReference, isConst } from '@glimmer/reference'; import { Arguments, VM, + OpcodeBuilderDSL, + ComponentArgs, } from '@glimmer/runtime'; +import * as WireFormat from '@glimmer/wire-format'; +import { Option } from '@glimmer/util'; import { assert } from 'ember-debug'; import { NON_SINGLETON_RENDER_MANAGER, @@ -17,7 +21,7 @@ import Environment from '../environment'; import { OwnedTemplate } from '../template'; import { hashToArgs } from './utils'; -function makeComponentDefinition(vm: VM, args: Arguments) { +function makeComponentDefinition(vm: VM, args: Arguments): ConstReference { let env = vm.env as Environment; let nameRef = args.positional.at(0); @@ -128,12 +132,17 @@ function makeComponentDefinition(vm: VM, args: Arguments) { @public @deprecated Use a component instead */ -export function renderMacro(_name: string, params: any[], hash: any[], builder: any) { +export function renderMacro(_name: string, params: Option, hash: WireFormat.Core.Hash, builder: OpcodeBuilderDSL): boolean { if (!params) { params = []; } - let definitionArgs = [params.slice(0), hash, null, null]; - let args = [params.slice(1), hashToArgs(hash), null, null]; - builder.component.dynamic(definitionArgs, makeComponentDefinition, args); + let definitionArgs: ComponentArgs = [params.slice(0), hash, null, null]; + let args: ComponentArgs = [params.slice(1), hashToArgs(hash), null, null]; + + // We have to coerce makeComponentDefinition into an `any` here because + // Glimmer VM is typing this too restrictively as a PathReference when it + // should just be a Reference (paths are never looked up on + // ComponentDefinitions. + builder.component.dynamic(definitionArgs, makeComponentDefinition as any, args); return true; } diff --git a/packages/ember-glimmer/lib/syntax/utils.ts b/packages/ember-glimmer/lib/syntax/utils.ts index 69d357a106e..a46031bca45 100644 --- a/packages/ember-glimmer/lib/syntax/utils.ts +++ b/packages/ember-glimmer/lib/syntax/utils.ts @@ -1,4 +1,6 @@ -export function hashToArgs(hash: any[]) { +import * as WireFormat from '@glimmer/wire-format'; + +export function hashToArgs(hash: WireFormat.Core.Hash): WireFormat.Core.Hash { if (hash === null) { return null; } let names = hash[0].map((key: string) => `@${key}`); return [names, hash[1]]; diff --git a/packages/ember-glimmer/lib/utils/bindings.ts b/packages/ember-glimmer/lib/utils/bindings.ts index 67c9ec0b3a2..66f28d94041 100644 --- a/packages/ember-glimmer/lib/utils/bindings.ts +++ b/packages/ember-glimmer/lib/utils/bindings.ts @@ -14,6 +14,7 @@ import { import { Ops, } from '@glimmer/wire-format'; +import * as WireFormat from '@glimmer/wire-format'; import { assert } from 'ember-debug'; import { get } from 'ember-metal'; import { String as StringUtils } from 'ember-runtime'; @@ -41,7 +42,7 @@ function referenceForParts(component: Component, parts: string[]) { } // TODO we should probably do this transform at build time -export function wrapComponentClassAttribute(hash: any[]) { +export function wrapComponentClassAttribute(hash: WireFormat.Core.Hash) { if (!hash) { return hash; } @@ -50,19 +51,24 @@ export function wrapComponentClassAttribute(hash: any[]) { let index = keys.indexOf('class'); if (index !== -1) { - let [ type ] = values[index]; + let [ type ] = values[index] as WireFormat.Expressions.TupleExpression | string; if (type === Ops.Get || type === Ops.MaybeLocal) { - let getExp = values[index]; + let getExp = expectTupleExpression(values[index]); let path = getExp[getExp.length - 1]; let propName = path[path.length - 1]; - hash[1][index] = [Ops.Helper, ['-class'], [getExp, propName]]; + hash[1][index] = [Ops.Helper, '-class', [getExp, propName], null] as WireFormat.Expressions.Helper; } } return hash; } +function expectTupleExpression(expr: WireFormat.Expression): WireFormat.Expressions.TupleExpression { + assert(`Expected a TupleExpression but got expression ${expr}`, Array.isArray(expr)); + return expr as WireFormat.Expressions.TupleExpression; +} + export const AttributeBinding = { parse(microsyntax: string): [string, string, boolean] { let colonIndex = microsyntax.indexOf(':'); diff --git a/packages/ember-glimmer/lib/views/outlet.ts b/packages/ember-glimmer/lib/views/outlet.ts index 238579daaa5..875d662ce99 100644 --- a/packages/ember-glimmer/lib/views/outlet.ts +++ b/packages/ember-glimmer/lib/views/outlet.ts @@ -62,7 +62,7 @@ class OrphanedOutletStateReference extends RootOutletStateReference { let state = Object.create(null); state[matched.render.outlet] = matched; matched.wasUsed = true; - return { outlets: state, render: undefined }; + return { outlets: state, render: null }; } } @@ -100,7 +100,7 @@ export interface OutletState { outlets: { [name: string]: OutletState | undefined; }; - render: RenderState | undefined; + render: Option; } export interface BootEnvironment {