diff --git a/.eslintrc.js b/.eslintrc.js index 3bc604e7d..c9a0cdd47 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { root: true, parserOptions: { - ecmaVersion: 2017, + ecmaVersion: 'latest', }, env: { node: true, diff --git a/README.md b/README.md index eda22814f..b1f42e7fb 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,22 @@ If you would like to only convert certain component invocations to use the angle } ``` +### Making helper invocations unambiguous + +You may want to convert invocations like `{{concat "foo" "bar"}}` into `{{(concat "foo" "bar")}}`, which may be useful as a temporary step when upgrading to strict-mode Embroider. + +In your **config/anglebrackets-codemod-config.json**, add this: + +```js +{ + "unambiguousHelpers": true +} +``` + +Note that unambiguous helpers do not work in non-Embroider Ember, as of January 2024. + +Note that ambiguous invocations that cannot be statically distinguished between a helper, a property and a component — will not be modified. + ## Debugging Workflow Oftentimes, you want to debug the codemod or the transform to identify issues with the code or to understand diff --git a/package.json b/package.json index 989a091ec..ce66b4937 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "yargs": "^17.7.2" }, "devDependencies": { + "compare-fixture": "^1.1.0", "coveralls": "^3.1.1", "eslint": "^8.22.0", "eslint-config-prettier": "^8.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b335425d3..535f838f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ dependencies: version: 17.7.2 devDependencies: + compare-fixture: + specifier: ^1.1.0 + version: 1.1.0 coveralls: specifier: ^3.1.1 version: 3.1.1 @@ -2083,7 +2086,6 @@ packages: /@types/minimatch@3.0.5: resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} - dev: false /@types/node@20.10.3: resolution: {integrity: sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==} @@ -2913,6 +2915,14 @@ packages: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: false + /compare-fixture@1.1.0: + resolution: {integrity: sha512-xAfzI5+Xin4fTixi3A4LzA5bI/zU0aT6+Kf1BX3fdkOAaUvzpVJxRbACR05VH+CfQfKZPamqV87PZWBQhCa8NQ==} + hasBin: true + dependencies: + mocha-diff: 1.0.2 + walk-sync: 3.0.0 + dev: true + /component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} dev: false @@ -3166,6 +3176,11 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dev: true + /diff@5.1.0: + resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} + engines: {node: '>=0.3.1'} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3290,7 +3305,6 @@ packages: /ensure-posix-path@1.1.1: resolution: {integrity: sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==} - dev: false /err-code@1.1.2: resolution: {integrity: sha512-CJAN+O0/yA1CKfRn9SXOGctSpEM7DCon/r/5r2eXFMY2zCCJBasFhcM5I+1kh3Ap11FsQCX+vGHceNPvpWKhoA==} @@ -5413,7 +5427,6 @@ packages: dependencies: '@types/minimatch': 3.0.5 minimatch: 3.1.2 - dev: false /mem@4.3.0: resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==} @@ -5533,6 +5546,13 @@ packages: dependencies: minimist: 1.2.8 + /mocha-diff@1.0.2: + resolution: {integrity: sha512-LJXN9eSTwVTPzo4Ja6Z8CuxcjK9HYE17J+3+0KxCwHmJeOPBb6v+YtXQRg53NCHO/lrCvRgYhAMw2ubkZTueYA==} + dependencies: + diff: 5.1.0 + supports-color: 9.4.0 + dev: true + /move-concurrently@1.0.1: resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==} dependencies: @@ -6956,6 +6976,11 @@ packages: has-flag: 4.0.0 dev: true + /supports-color@9.4.0: + resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} + engines: {node: '>=12'} + dev: true + /supports-hyperlinks@2.3.0: resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} engines: {node: '>=8'} @@ -7372,6 +7397,16 @@ packages: minimatch: 3.1.2 dev: false + /walk-sync@3.0.0: + resolution: {integrity: sha512-41TvKmDGVpm2iuH7o+DAOt06yyu/cSHpX3uzAwetzASvlNtVddgIjXIb2DfB/Wa20B1Jo86+1Dv1CraSU7hWdw==} + engines: {node: 10.* || >= 12.*} + dependencies: + '@types/minimatch': 3.0.5 + ensure-posix-path: 1.1.1 + matcher-collection: 2.0.1 + minimatch: 3.1.2 + dev: true + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: diff --git a/test/fixtures/with-telemetry/input/app/templates/application.hbs b/test/fixtures/with-telemetry/input/app/templates/application.hbs index f665128ad..332dd51e4 100644 --- a/test/fixtures/with-telemetry/input/app/templates/application.hbs +++ b/test/fixtures/with-telemetry/input/app/templates/application.hbs @@ -14,27 +14,8 @@ {{/bs-nav}}
- {{utils/bee-bop}} {{-wat-wat}} - {{utils/-wat-wat}} - {{#if this.isDetailPage}} -

- {{currentComponent.title}} -

-

- {{currentComponent.description}} -

- {{api-reference component=this.currentComponent}} - {{/if}} - {{outlet}} - - {{#bs-button id="openModal" onClick=(action this.addModal)}}Open{{/bs-button}} - - {{#if hasModal}} - {{#bs-modal-simple open=modal onHidden=(action "removeModal") title="Dynamic Dialog"}} - Hi there - {{/bs-modal-simple}} - {{/if}} + {{outlet}} {{file-less foo=true}}
diff --git a/test/fixtures/with-telemetry/input/app/templates/components/file-less.hbs b/test/fixtures/with-telemetry/input/app/templates/components/file-less.hbs index b9c03c0c8..6c801e1d9 100644 --- a/test/fixtures/with-telemetry/input/app/templates/components/file-less.hbs +++ b/test/fixtures/with-telemetry/input/app/templates/components/file-less.hbs @@ -1,2 +1 @@
this template has no js
-{{#bs-button type="primary"}}Primary{{/bs-button}} diff --git a/test/fixtures/with-telemetry/output/app/templates/application.hbs b/test/fixtures/with-telemetry/output/app/templates/application.hbs index 077434de3..87bcba4c9 100644 --- a/test/fixtures/with-telemetry/output/app/templates/application.hbs +++ b/test/fixtures/with-telemetry/output/app/templates/application.hbs @@ -14,27 +14,8 @@
- {{(utils/bee-bop)}} - {{(-wat-wat)}} - {{(utils/-wat-wat)}} - {{#if this.isDetailPage}} -

- {{currentComponent.title}} -

-

- {{currentComponent.description}} -

- - {{/if}} - {{outlet}} - - Open - - {{#if hasModal}} - - Hi there - - {{/if}} + {{-wat-wat}} + {{outlet}}
diff --git a/test/fixtures/with-telemetry/output/app/templates/components/file-less.hbs b/test/fixtures/with-telemetry/output/app/templates/components/file-less.hbs index 4c50d5d9c..6c801e1d9 100644 --- a/test/fixtures/with-telemetry/output/app/templates/components/file-less.hbs +++ b/test/fixtures/with-telemetry/output/app/templates/components/file-less.hbs @@ -1,2 +1 @@
this template has no js
-Primary diff --git a/test/fixtures/with-telemetry/output/app/templates/components/foo-bar.hbs b/test/fixtures/with-telemetry/output/app/templates/components/foo-bar.hbs index 44dd7b86b..47fb0ecdd 100644 --- a/test/fixtures/with-telemetry/output/app/templates/components/foo-bar.hbs +++ b/test/fixtures/with-telemetry/output/app/templates/components/foo-bar.hbs @@ -1,4 +1,4 @@ -{{(biz-baz canConvert="no" why="helper" where="local")}} +{{biz-baz canConvert="no" why="helper" where="local"}} diff --git a/test/fixtures/without-telemetry/input/app/templates/application.hbs b/test/fixtures/without-telemetry/input/app/templates/application.hbs index 648a45edb..1647f3b36 100644 --- a/test/fixtures/without-telemetry/input/app/templates/application.hbs +++ b/test/fixtures/without-telemetry/input/app/templates/application.hbs @@ -1,9 +1,9 @@ -{{site-header user=this.user class=(if this.user.isAdmin "admin")}} + -{{#super-select selected=this.user.country as |s|}} + {{#each this.availableCountries as |country|}} - {{#s.option value=country}}{{country.name}}{{/s.option}} + {{country.name}} {{/each}} -{{/super-select}} + -{{ui/button text="Click me"}} + diff --git a/test/run-test.js b/test/run-test.js index 1db6f569c..c69839ce8 100644 --- a/test/run-test.js +++ b/test/run-test.js @@ -45,7 +45,8 @@ const execOpts = { cwd: inputDir, stderr: 'inherit' }; console.log('comparing results'); try { - await execa('diff', ['-rq', './app', '../output/app'], execOpts); + const compareFixture = await import('compare-fixture'); + compareFixture.default(path.join(inputDir, 'app'), path.join(inputDir, '../output/app')); } catch (e) { console.error('codemod did not run successfully'); console.log(e); diff --git a/transforms/angle-brackets/index.js b/transforms/angle-brackets/index.js index 293a8653d..ea5a69568 100755 --- a/transforms/angle-brackets/index.js +++ b/transforms/angle-brackets/index.js @@ -31,6 +31,7 @@ function getOptions() { options.includeValuelessDataTestAttributes = !!config.includeValuelessDataTestAttributes; options.skipBuiltInComponents = !!config.skipBuiltInComponents; + options.unambiguousHelpers = !!config.unambiguousHelpers; } if (cliOptions.telemetry) { diff --git a/transforms/angle-brackets/telemetry/invokable.js b/transforms/angle-brackets/telemetry/invokable.js index f80ceed48..62a3f8545 100644 --- a/transforms/angle-brackets/telemetry/invokable.js +++ b/transforms/angle-brackets/telemetry/invokable.js @@ -4,7 +4,18 @@ const HELPER = 'Helper'; const COMPONENT = 'Component'; function invokableName(name, type) { - let invokePath = type === HELPER ? '/helpers/' : '/components/'; + let invokePath; + + if (name.startsWith('@ember/component/')) { + invokePath = '@ember/component/'; + } else if (name.startsWith('@ember/routing/')) { + invokePath = '@ember/routing/'; + } else if (type === HELPER) { + invokePath = '/helpers/'; + } else { + invokePath = '/components/'; + } + return name.substring(name.lastIndexOf(invokePath) + invokePath.length, name.length); } diff --git a/transforms/angle-brackets/transform.js b/transforms/angle-brackets/transform.js index 9d40f8d2d..8d94df81f 100755 --- a/transforms/angle-brackets/transform.js +++ b/transforms/angle-brackets/transform.js @@ -341,18 +341,26 @@ function isKnownHelper(fullName, config, invokableData) { } if (isTelemetryData) { - let isComponent = - !config.helpers.includes(name) && - [...(components || []), ...BUILT_IN_COMPONENTS].includes(name); + if (config.unambiguousHelpers) { + let isComponent = + !config.helpers.includes(name) && + [...(components || []), ...BUILT_IN_COMPONENTS].includes(name); - if (isComponent) { - return false; - } + if (isComponent) { + return false; + } - let mergedHelpers = [...KNOWN_HELPERS, ...(helpers || [])]; - let isHelper = mergedHelpers.includes(name) || config.helpers.includes(name); - let strName = `${name}`; // coerce boolean and number to string - return isHelper && !strName.includes('.'); + let mergedHelpers = [...KNOWN_HELPERS, ...(helpers || [])]; + let isHelper = mergedHelpers.includes(name) || config.helpers.includes(name); + let strName = `${name}`; // coerce boolean and number to string + return isHelper && !strName.includes('.'); + } else { + let mergedHelpers = [...KNOWN_HELPERS, ...(helpers || [])]; + let isHelper = mergedHelpers.includes(name) || config.helpers.includes(name); + let isComponent = [...(components || []), ...BUILT_IN_COMPONENTS].includes(name); + let strName = `${name}`; // coerce boolean and number to string + return (isHelper || !isComponent) && !strName.includes('.'); + } } else { return KNOWN_HELPERS.includes(name) || config.helpers.includes(name); } @@ -489,6 +497,7 @@ function transformToAngleBracket(fileInfo, config, invokableData) { ) { return transformComponentNode(node, fileInfo, config); } else if ( + config.unambiguousHelpers && isTagKnownHelper && node.path.type !== 'SubExpression' && walkerPath.parent.node.type !== 'AttrNode' && diff --git a/transforms/angle-brackets/transform.test.js b/transforms/angle-brackets/transform.test.js index 15daa3f85..eec96619c 100644 --- a/transforms/angle-brackets/transform.test.js +++ b/transforms/angle-brackets/transform.test.js @@ -476,7 +476,7 @@ test('let', () => { {{#let (capitalize this.person.firstName) (capitalize this.person.lastName) as |firstName lastName| }} - Welcome back {{(concat firstName ' ' lastName)}} + Welcome back {{concat firstName ' ' lastName}} Account Details: First Name: {{firstName}} @@ -554,8 +554,8 @@ test('link-to-inline', () => { Segments Segments {{segment.name}} - {{(t \\"show\\")}} - {{(t \\"show\\")}} + {{t \\"show\\"}} + {{t \\"show\\"}} Show Show Show @@ -839,7 +839,7 @@ test('t-helper', () => { expect(runTest('t-helper.hbs', input)).toMatchInlineSnapshot(` " - {{(t \\"some.string\\" param=\\"string\\" another=1)}} + {{t \\"some.string\\" param=\\"string\\" another=1}} " `); }); @@ -977,7 +977,7 @@ test('skip-default-helpers', () => { expect(runTest('skip-default-helpers.hbs', input, options)).toMatchInlineSnapshot(` "
- {{(liquid-outlet)}} + {{liquid-outlet}}
@@ -997,11 +997,11 @@ test('skip-default-helpers', () => { Two {{/liquid-if}} - {{(moment '12-25-1995' 'MM-DD-YYYY')}} - {{(moment-from '1995-12-25' '2995-12-25' hideAffix=true)}} + {{moment '12-25-1995' 'MM-DD-YYYY'}} + {{moment-from '1995-12-25' '2995-12-25' hideAffix=true}} - {{(some-helper1 foo=true)}} - {{(some-helper2 foo=true)}} + {{some-helper1 foo=true}} + {{some-helper2 foo=true}} " `); }); @@ -1039,7 +1039,7 @@ test('skip-default-helpers (no-config)', () => { expect(runTest('skip-default-helpers.hbs', input)).toMatchInlineSnapshot(` "
- {{(liquid-outlet)}} + {{liquid-outlet}}
@@ -1059,8 +1059,8 @@ test('skip-default-helpers (no-config)', () => { Two {{/liquid-if}} - {{(moment '12-25-1995' 'MM-DD-YYYY')}} - {{(moment-from '1995-12-25' '2995-12-25' hideAffix=true)}} + {{moment '12-25-1995' 'MM-DD-YYYY'}} + {{moment-from '1995-12-25' '2995-12-25' hideAffix=true}} @@ -1086,8 +1086,8 @@ test('custom-options', () => { expect(runTest('custom-options.hbs', input, options)).toMatchInlineSnapshot(` " - {{(some-helper1 foo=true)}} - {{(some-helper2 foo=true)}} + {{some-helper1 foo=true}} + {{some-helper2 foo=true}} {{link-to \\"Title\\" \\"some.route\\"}} {{textarea value=this.model.body}} {{input type=\\"checkbox\\" name=\\"email-opt-in\\" checked=this.model.emailPreference}} @@ -1180,7 +1180,7 @@ test('preserve arguments', () => { " {{foo-bar data-baz class=\\"baz\\"}} - {{(t \\"show\\")}} + {{t \\"show\\"}} " `); }); @@ -1284,9 +1284,9 @@ test('wallstreet-telemetry', () => { expect(runTest('wallstreet-telemetry.hbs', input)).toMatchInlineSnapshot(` " {{nested$helper}} - {{(nested::helper)}} - {{(nested::helper param=\\"yeah!\\")}} - {{(helper-1)}} + {{nested::helper}} + {{nested::helper param=\\"yeah!\\"}} + {{helper-1}} " `); }); @@ -1294,16 +1294,16 @@ test('wallstreet-telemetry', () => { test('wrapping-helpers-with-parens', () => { let input = ` {{fooknownhelper}} - {{(fooknownhelper)}} + {{fooknownhelper}} {{fooknownhelper data-test-foo foo="bar"}} {{foounknownhelper}} `; expect(runTest('wrapping-helpers-with-parens.hbs', input)).toMatchInlineSnapshot(` " - {{(fooknownhelper)}} - {{(fooknownhelper)}} - {{(fooknownhelper data-test-foo foo=\\"bar\\")}} + {{fooknownhelper}} + {{fooknownhelper}} + {{fooknownhelper data-test-foo foo=\\"bar\\"}} {{foounknownhelper}} " `); @@ -1386,3 +1386,18 @@ test('unknown helper with args', () => { " `); }); + +test('unambiguousHelpers: true', () => { + let input = ` + {{helper-1}} + {{nested/helper "some.string" param="string" another=1}} + `; + + expect(runTest('unambiguousHelpers: true', input, { unambiguousHelpers: true })) + .toMatchInlineSnapshot(` + " + {{(helper-1)}} + {{(nested/helper \\"some.string\\" param=\\"string\\" another=1)}} + " + `); +});