Skip to content

Commit

Permalink
[BREAKING] Use plain functions for AST plugins.
Browse files Browse the repository at this point in the history
This changes AST Plugins to be "plain functions" with the following interface:

```ts
import { AST, Syntax } from '@glimmer/syntax';

export type NodeVisitor = {
  [P in keyof AST.Nodes]?: NodeHandler<AST.Nodes[P]>;
};

export interface ASTPluginResult {
  name: string;
  visitors: NodeVisitor;
}

export interface ASTPlugin {
  (env: ASTPluginEnvironment): ASTPluginResult;
}

export interface ASTPluginEnvironment {
  meta?: any;
  syntax: Syntax;
}
```

The current system of instantiation is fairly confusing and makes a number of use cases pretty difficult (where you need to preserve some other outside state that you access from within your plugin's `transform`).

The old API can easily be reimplemented in terms of the plain functions in consumers (e.g. Ember for compatibility):

```js
let uuid = 0;

  function ensurePlugin(FunctionOrPlugin: any): ASTPlugin {
    if (FunctionOrPlugin.prototype && FunctionOrPlugin.prototype.transform) {
      return (env) => {
        return {
          name: 'legacy-transform' + (++uuid) ,
          visitors: {
            Program(node: AST.Program) {
              let plugin = new FunctionOrPlugin(env);

              plugin.syntax = env.syntax;

              return plugin.transform(node);
            }
          }
        };
      };
    } else {
      return FunctionOrPlugin;
    }
  }
```

A test exists for this (to ensure Ember can provide backwards compat).
  • Loading branch information
rwjblue committed Jun 21, 2017
1 parent 6fbb27b commit 28652d4
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 130 deletions.
7 changes: 2 additions & 5 deletions packages/@glimmer/compiler/lib/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ import { preprocess } from "@glimmer/syntax";
import TemplateCompiler, { CompileOptions } from "./template-compiler";
import { SerializedTemplateWithLazyBlock, TemplateJavascript, TemplateMeta } from "@glimmer/wire-format";
import { Option } from "@glimmer/interfaces";
import { TransformASTPluginFactory } from "@glimmer/syntax";
import { PreprocessOptions } from "@glimmer/syntax";

export interface TemplateIdFn {
(src: string): Option<string>;
}

export interface PrecompileOptions<T extends TemplateMeta> extends CompileOptions<T> {
export interface PrecompileOptions<T extends TemplateMeta> extends CompileOptions<T>, PreprocessOptions {
id?: TemplateIdFn;
plugins?: {
ast?: TransformASTPluginFactory[]
};
}

declare function require(id: string): any;
Expand Down
5 changes: 3 additions & 2 deletions packages/@glimmer/syntax/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// used by ember-compiler
export {
preprocess,
TransformASTPlugin,
TransformASTPluginFactory,
PreprocessOptions,
ASTPlugin,
ASTPluginEnvironment,
Syntax
} from './lib/parser/tokenizer-event-handlers';

Expand Down
40 changes: 23 additions & 17 deletions packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import * as AST from "../types/nodes";
import SyntaxError from '../errors/syntax-error';
import { Tag } from "../parser";
import builders from "../builders";
import traverse from "../traversal/traverse";
import traverse, { NodeVisitor } from "../traversal/traverse";
import print from "../generation/print";
import Walker from "../traversal/walker";
import * as handlebars from "handlebars";
import { assign } from '@glimmer/util';

const voidMap: {
[tagName: string]: boolean
Expand Down Expand Up @@ -307,38 +308,43 @@ export const syntax: Syntax = {
};

/**
* TransformASTPlugins can make changes to the Glimmer template AST before
* compilation begins. Plugins implement a `transform()` method that takes a
* `Program` AST node and should return the modified program.
*/
export interface TransformASTPlugin {
syntax: Syntax;
transform(program: AST.Program): AST.Program;
ASTPlugins can make changes to the Glimmer template AST before
compilation begins.
*/
export interface ASTPlugin {
(env: ASTPluginEnvironment): ASTPluginResult;
}

export interface TransformASTPluginFactory {
new (options: PreprocessOptions): TransformASTPlugin;
export interface ASTPluginResult {
name: string;
visitors: NodeVisitor;

This comment has been minimized.

Copy link
@chriseppstein

chriseppstein Jun 22, 2017

The plural here is confusing and seems to imply NodeVisitor[] instead of NodeVisitor.

This comment has been minimized.

Copy link
@mmun

mmun Jun 22, 2017

Member

Yes, I agree.

This comment has been minimized.

Copy link
@mmun

mmun Jun 22, 2017

Member

I did not notice the changes introducing visitors unfortunately, although I'm pretty sure @rwjblue and I talked about visitors being the wrong word a few times in chat. :P

Another way of typing it:

export interface ASTPlugin {
  name: string;
  visitor: Visitor;
}

type IntoASTPlugin = ASTPlugin | ((env: ASTPluginEnvironment) => ASTPlugin);

export interface PreprocessOptions {
  plugins?: {
    ast?: IntoASTPlugin[]
  }
}

This comment has been minimized.

Copy link
@rwjblue

rwjblue Jun 22, 2017

Author Member

😩

}

export interface ASTPluginEnvironment {
meta?: any;
syntax: Syntax;
}

export interface PreprocessOptions {
plugins?: {
ast?: TransformASTPluginFactory[]
ast?: ASTPlugin[];
};
}

export function preprocess(html: string, options?: PreprocessOptions): AST.Program {
let ast = (typeof html === 'object') ? html : handlebars.parse(html);
let combined = new TokenizerEventHandlers(html, options).acceptNode(ast);
let program = new TokenizerEventHandlers(html, options).acceptNode(ast);

if (options && options.plugins && options.plugins.ast) {
for (let i = 0, l = options.plugins.ast.length; i < l; i++) {
let plugin = new options.plugins.ast[i](options);
let transform = options.plugins.ast[i];
let env = assign({}, options, { syntax }, { plugins: undefined });

plugin.syntax = syntax;
let pluginResult = transform(env);

combined = plugin.transform(combined);
traverse(program, pluginResult.visitors);
}
}

return combined;
}
return program;
}
38 changes: 11 additions & 27 deletions packages/@glimmer/syntax/lib/traversal/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,21 @@ import {
import * as nodes from '../types/nodes';
import { Option } from "@glimmer/interfaces";

export interface NodeVisitor {
All?: NodeHandler<nodes.BaseNode>;
Program?: NodeHandler<nodes.Program>;
MustacheStatement?: NodeHandler<nodes.MustacheStatement>;
BlockStatement?: NodeHandler<nodes.BlockStatement>;
ElementModifierStatement?: NodeHandler<nodes.ElementModifierStatement>;
PartialStatement?: NodeHandler<nodes.PartialStatement>;
CommentStatement?: NodeHandler<nodes.CommentStatement>;
MustacheCommentStatement?: NodeHandler<nodes.MustacheCommentStatement>;
ElementNode?: NodeHandler<nodes.ElementNode>;
AttrNode?: NodeHandler<nodes.AttrNode>;
TextNode?: NodeHandler<nodes.TextNode>;
ConcatStatement?: NodeHandler<nodes.ConcatStatement>;
SubExpression?: NodeHandler<nodes.SubExpression>;
PathExpression?: NodeHandler<nodes.PathExpression>;
StringLiteral?: NodeHandler<nodes.StringLiteral>;
BooleanLiteral?: NodeHandler<nodes.BooleanLiteral>;
NumberLiteral?: NodeHandler<nodes.NumberLiteral>;
UndefinedLiteral?: NodeHandler<nodes.UndefinedLiteral>;
NullLiteral?: NodeHandler<nodes.NullLiteral>;
Hash?: NodeHandler<nodes.Hash>;
HashPair?: NodeHandler<nodes.HashPair>;
}
export type NodeHandler<T extends nodes.Node> = NodeHandlerFunction<T> | EnterExitNodeHandler<T>;

export type SpecificNodeVisitor = {
[P in keyof nodes.Nodes]?: NodeHandler<nodes.Nodes[P]>;
};

export type NodeHandler<T> = NodeHandlerFunction<T> | EnterExitNodeHandler<T>;
export interface NodeVisitor extends SpecificNodeVisitor {
All?: NodeHandler<nodes.Node>;
}

export interface NodeHandlerFunction<T> {
(node: T): any | null | undefined;
export interface NodeHandlerFunction<T extends nodes.Node> {
(this: null, node: T): any | null | undefined;
}

export interface EnterExitNodeHandler<T> {
export interface EnterExitNodeHandler<T extends nodes.Node> {
enter?: NodeHandlerFunction<T>;
exit?: NodeHandlerFunction<T>;
keys?: any;
Expand Down
174 changes: 95 additions & 79 deletions packages/@glimmer/syntax/test/plugin-node-test.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,134 @@
import { preprocess as parse, Walker, Syntax, AST } from '@glimmer/syntax';
import {
preprocess,
Syntax,
Walker,
AST,
ASTPlugin
} from '@glimmer/syntax';

const { test } = QUnit;

QUnit.module('[glimmer-syntax] Plugins - AST Transforms');

test('AST plugins can be provided to the compiler', assert => {
test('function based AST plugins can be provided to the compiler', assert => {
assert.expect(1);

class Plugin {
syntax: Syntax;

transform(program: AST.Program): AST.Program {
assert.ok(true, 'transform was called!');
return program;
}
}

parse('<div></div>', {
preprocess('<div></div>', {
plugins: {
ast: [ Plugin ]
ast: [
() => ({
name: 'plugin-a',
visitors: {
Program() {
assert.ok(true, 'transform was called!');
}
}
})
]
}
});
});

test('provides syntax package as `syntax` prop if value is null', assert => {
test('plugins are provided the syntax package', assert => {
assert.expect(1);

class Plugin {
syntax: Syntax;
transform(program: AST.Program): AST.Program {
assert.equal(this.syntax.Walker, Walker);
return program;
}
}

parse('<div></div>', {
preprocess('<div></div>', {
plugins: {
ast: [ Plugin ]
ast: [
({ syntax }) => {
assert.equal(syntax.Walker, Walker);

return { name: 'plugin-a', visitors: {} };
}
]
}
});
});

test('AST plugins can modify the AST', assert => {
assert.expect(1);
test('can support the legacy AST transform API via ASTPlugin', assert => {
function ensurePlugin(FunctionOrPlugin: any): ASTPlugin {
if (FunctionOrPlugin.prototype && FunctionOrPlugin.prototype.transform) {
return (env) => {
return {
name: 'plugin-a',

visitors: {
Program(node: AST.Program) {
let plugin = new FunctionOrPlugin(env);

plugin.syntax = env.syntax;

return plugin.transform(node);
}
}
};
};
} else {
return FunctionOrPlugin;
}
}

class Plugin {
syntax: Syntax;
transform(): AST.Program {
return {
type: 'Program',
body: [],
blockParams: [],
isSynthetic: true,
loc: {
start: { line: 0, column: 0 },
end: { line: 0, column: 1 }
}
} as AST.Program;

transform(program: AST.Program): AST.Program {
assert.ok(true, 'transform was called!');
return program;
}
}

let ast = parse('<div></div>', {
preprocess('<div></div>', {
plugins: {
ast: [ Plugin ]
ast: [ ensurePlugin(Plugin) ]
}
});

assert.ok(ast['isSynthetic'], 'return value from AST transform is used');
});

test('AST plugins can be chained', assert => {
assert.expect(2);

class Plugin {
syntax: Syntax;
transform(): AST.Program {
return {
type: 'Program',
body: [],
blockParams: [],
isFromFirstPlugin: true,
loc: {
start: { line: 0, column: 0 },
end: { line: 0, column: 1 }
assert.expect(3);

let first = () => {
return {
name: 'first',
visitors: {
Program(program: AST.Program) {
program['isFromFirstPlugin'] = true;
}
} as AST.Program;
}
}

class SecondaryPlugin {
syntax: Syntax;
transform(program: AST.Program) {
assert.equal(program['isFromFirstPlugin'], true, 'AST from first plugin is passed to second');
return {
type: 'Program',
body: [],
blockParams: [],
isFromSecondPlugin: true,
loc: {
start: { line: 0, column: 0 },
end: { line: 0, column: 1 }
}
};
};

let second = () => {
return {
name: 'second',
visitors: {
Program(node: AST.Program) {
assert.equal(node['isFromFirstPlugin'], true, 'AST from first plugin is passed to second');

node['isFromSecondPlugin'] = true;
}
} as AST.Program;
}
}
}
};
};

let third = () => {
return {
name: 'third',
visitors: {
Program(node: AST.Program) {
assert.equal(node['isFromSecondPlugin'], true, 'AST from second plugin is passed to third');

node['isFromThirdPlugin'] = true;
}
}
};
};

let ast = parse('<div></div>', {
let ast = preprocess('<div></div>', {
plugins: {
ast: [
Plugin,
SecondaryPlugin
]
ast: [first, second, third]
}
});

assert.equal(ast['isFromSecondPlugin'], true, 'return value from last AST transform is used');
assert.equal(ast['isFromThirdPlugin'], true, 'return value from last AST transform is used');
});

0 comments on commit 28652d4

Please sign in to comment.