From baca90b589222d25d440d3666d84cea0f75cc385 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 4 Apr 2019 09:49:40 -0400 Subject: [PATCH] Refactor nested AST transform. * Ensures only one `let` is ever created * Reuses the same yielded block param for every invocation of the same underlying component * Ensures that `Foo::Bar::Baz` works (arbitrary nesting levels) * Uses custom suffix to ensure no block param collisions exist --- lib/ast-nested-transform.js | 78 ++++++++++--------- lib/ast-transform.js | 2 +- .../angle-bracket-invocation-test.js | 23 ++++++ 3 files changed, 64 insertions(+), 39 deletions(-) diff --git a/lib/ast-nested-transform.js b/lib/ast-nested-transform.js index e51ac63..3b97f1a 100644 --- a/lib/ast-nested-transform.js +++ b/lib/ast-nested-transform.js @@ -12,6 +12,9 @@ class AngleBracketPolyfill { transform(ast) { let b = this.syntax.builders; + // in order to debug in https://https://astexplorer.net/#/gist/0590eb883edfcd163b183514df4cc717 + // **** copy from here **** + function dasherize(string) { return string.replace(/[A-Z]/g, function(char, index) { if (index === 0 || !ALPHA.test(string[index - 1])) { @@ -22,58 +25,57 @@ class AngleBracketPolyfill { }); } - function replaceNestedComponents(string) { - return string.replace('::', '/'); - } + let rootProgram; + let letBlock; + let yieldedComponents = new Map(); - function getBlockParamName(node) { - let unnestedName = node.tag.replace('::', ''); - let possibleName = unnestedName; - - const nestedNames = node.children - .map(child => { - switch (child.type) { - case 'ElementNode': - return child.tag; - case 'MustacheStatement': - return child.path.original; - default: - break; - } - }) - .filter(child => child); - - let adder = 0; - while (nestedNames.indexOf(possibleName) !== -1) { - adder++; - possibleName = `${unnestedName}${adder}`; + function ensureLetWrapper() { + if (!letBlock) { + letBlock = b.block('let', [], b.hash([]), b.program(rootProgram.body), null, null); + rootProgram.body = [letBlock]; } - - return possibleName; } - function wrapAngeBrackedComponentWithLetHelper(node) { - let tag = node.tag; + let counter = 0; + function localNameForYieldedComponent(tag) { + let localName = yieldedComponents.get(tag); + if (!localName) { + localName = tag.replace(/::/g, '') + '_ANGLE_' + counter++; + let transformedPath = dasherize(tag.replace(/::/g, '/')); - let params = [ - b.sexpr(b.path('component'), [b.string(dasherize(replaceNestedComponents(tag)))]), - ]; + let positionalArg = b.sexpr(b.path('component'), [b.string(transformedPath)]); + letBlock.params.push(positionalArg); + letBlock.program.blockParams.push(localName); - node.tag = getBlockParamName(node); - let program = b.program([node], [getBlockParamName(node)]); - program.__ignore = true; + yieldedComponents.set(tag, localName); + } - return b.block('let', params, b.hash([]), program, null, null); + return localName; } let visitor = { + // supports glimmer-vm@0.39 + Template(node) { + rootProgram = node; + }, + + // supports glimmer-vm < 0.39 + Program(node) { + // on older ember versions `Program` is used for both the "wrapping + // template" and for each block + if (!rootProgram) { + rootProgram = node; + } + }, + ElementNode(node) { let tag = node.tag; - if (tag.indexOf('::') !== -1 && tag.charAt(0) === tag.charAt(0).toUpperCase()) { - let newNode = wrapAngeBrackedComponentWithLetHelper(node); + if (tag.indexOf('::') !== -1) { + ensureLetWrapper(); - return newNode; + let localName = localNameForYieldedComponent(tag); + node.tag = localName; } }, }; diff --git a/lib/ast-transform.js b/lib/ast-transform.js index f50676f..4d32caa 100644 --- a/lib/ast-transform.js +++ b/lib/ast-transform.js @@ -28,7 +28,7 @@ class AngleBracketPolyfill { } function replaceNestedComponents(string) { - return string.replace('::', '/'); + return string.replace(/::/g, '/'); } function isSimple(mustache) { diff --git a/tests/integration/components/angle-bracket-invocation-test.js b/tests/integration/components/angle-bracket-invocation-test.js index 0385b1c..ed64924 100644 --- a/tests/integration/components/angle-bracket-invocation-test.js +++ b/tests/integration/components/angle-bracket-invocation-test.js @@ -192,6 +192,19 @@ module('Integration | Component | angle-bracket-invocation', function(hooks) { assert.dom('[data-foo="bar"]').exists(); }); + test('nested paths do not conflict with non-nested paths with similar names', async function(assert) { + this.owner.register('template:components/foo/bar', hbs`hi rwjblue!`); + this.owner.register('template:components/foo-bar', hbs`hi rtablada!`); + + await render(hbs` + + + `); + + assert.dom('[data-foo="bar"]').hasText('hi rwjblue!'); + assert.dom('[data-foo="baz"]').hasText('hi rtablada!'); + }); + test('invoke nested path', async function(assert) { this.owner.register('template:components/foo/bar', hbs`hi rwjblue!`); @@ -202,6 +215,16 @@ module('Integration | Component | angle-bracket-invocation', function(hooks) { assert.dom('[data-foo="bar"]').exists(); }); + test('invoke deeply nested path', async function(assert) { + this.owner.register('template:components/foo/bar/baz/qux', hbs`hi rwjblue!`); + + await render(hbs` + + `); + + assert.dom('[data-foo="bar"]').exists(); + }); + test('invoke dynamic - path', async function(assert) { this.owner.register('service:elsewhere', Service.extend()); this.owner.register(