Repository: ember-codemods/ember-angle-brackets-codemod
Pull request #509 (2023-12-04 16:58:06)

Make helpers unambiguious with parens aka subexpressions (take 2)
Supercedes #507.

Quite a lot going on, please review by commit. 🙏 


Patch:
diff --git a/transforms/angle-brackets/known-helpers.js b/transforms/angle-brackets/known-helpers.js
index 87a7ee7..57d4e18 100755
--- a/transforms/angle-brackets/known-helpers.js
+++ b/transforms/angle-brackets/known-helpers.js
@@ -18,14 +18,12 @@ const KNOWN_HELPERS = [
   'loc',
   'log',
   'mut',
-  'outlet',
   'partial',
   'query-params',
   'readonly',
   'unbound',
   'unless',
   'with',
-  'yield',
 
   // Glimmer VM
   'identity', // glimmer blocks
diff --git a/transforms/angle-brackets/telemetry/mock-invokables.js b/transforms/angle-brackets/telemetry/mock-invokables.js
index 9715c6e..a00d442 100644
--- a/transforms/angle-brackets/telemetry/mock-invokables.js
+++ b/transforms/angle-brackets/telemetry/mock-invokables.js
@@ -101,4 +101,7 @@ module.exports = {
   'app/helpers/nested/helper': {
     type: 'Helper',
   },
+  'app/helpers/fooknownhelper': {
+    type: 'Helper',
+  },
 };
diff --git a/transforms/angle-brackets/transform.js b/transforms/angle-brackets/transform.js
index c4f3e97..9d40f8d 100755
--- a/transforms/angle-brackets/transform.js
+++ b/transforms/angle-brackets/transform.js
@@ -46,7 +46,11 @@ function isBuiltInComponent(key) {
 }
 
 function isNestedComponentTagName(tagName) {
-  return tagName && tagName.includes && (tagName.includes('/') || tagName.includes('-'));
+  return (
+    tagName &&
+    tagName.includes &&
+    (tagName.includes('/') || (tagName.includes('-') && tagName.includes('.')))
+  );
 }
 
 function isWallStreet(tagName) {
@@ -326,7 +330,7 @@ function getNonDataAttributesFromParams(params) {
   return params.filter((it) => !isDataAttrPathExpression(it));
 }
 
-function shouldIgnoreMustacheStatement(fullName, config, invokableData) {
+function isKnownHelper(fullName, config, invokableData) {
   let { helpers, components } = invokableData;
   let isTelemetryData = !!(helpers || components);
 
@@ -337,11 +341,18 @@ function shouldIgnoreMustacheStatement(fullName, config, invokableData) {
   }
 
   if (isTelemetryData) {
+    let isComponent =
+      !config.helpers.includes(name) &&
+      [...(components || []), ...BUILT_IN_COMPONENTS].includes(name);
+
+    if (isComponent) {
+      return false;
+    }
+
     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('.');
+    return isHelper && !strName.includes('.');
   } else {
     return KNOWN_HELPERS.includes(name) || config.helpers.includes(name);
   }
@@ -363,7 +374,7 @@ function nodeHasPositionalParameters(node) {
   return false;
 }
 
-function transformNode(node, fileInfo, config) {
+function transformComponentNode(node, fileInfo, config) {
   if (
     hasValuelessDataParams(node.params) &&
     shouldSkipDataTestParams(node.params, config.includeValuelessDataTestAttributes)
@@ -425,6 +436,10 @@ function transformNode(node, fileInfo, config) {
   );
 }
 
+function transformHelperNode(node) {
+  return b.mustache(b.sexpr(node.path, node.params, node.hash));
+}
+
 function subExpressionToMustacheStatement(subExpression) {
   return b.mustache(subExpression.path, subExpression.params, subExpression.hash);
 }
@@ -457,22 +472,29 @@ function transformToAngleBracket(fileInfo, config, invokableData) {
    * Transform the attributes names & values properly
    */
   return {
-    MustacheStatement(node) {
+    MustacheStatement(node, walkerPath) {
       const tagName = `${node.path && node.path.original}`;
 
       if (config.components && !config.components.includes(tagName)) return;
 
+      const isTagKnownHelper = isKnownHelper(tagName, config, invokableData);
+
       // Don't change attribute statements
-      const isValidMustache =
-        node.loc.source !== '(synthetic)' &&
-        !shouldIgnoreMustacheStatement(tagName, config, invokableData);
+      const isValidMustacheComponent = node.loc.source !== '(synthetic)' && !isTagKnownHelper;
       const isNestedComponent = isNestedComponentTagName(tagName);
 
       if (
-        isValidMustache &&
+        isValidMustacheComponent &&
         (node.hash.pairs.length > 0 || node.params.length > 0 || isNestedComponent)
       ) {
-        return transformNode(node, fileInfo, config);
+        return transformComponentNode(node, fileInfo, config);
+      } else if (
+        isTagKnownHelper &&
+        node.path.type !== 'SubExpression' &&
+        walkerPath.parent.node.type !== 'AttrNode' &&
+        walkerPath.parent.node.type !== 'ConcatStatement'
+      ) {
+        return transformHelperNode(node, walkerPath);
       }
     },
     BlockStatement(node) {
@@ -480,11 +502,8 @@ function transformToAngleBracket(fileInfo, config, invokableData) {
 
       if (config.components && !config.components.includes(tagName)) return;
 
-      if (
-        !shouldIgnoreMustacheStatement(node.path.original, config, invokableData) ||
-        isWallStreet(tagName)
-      ) {
-        return transformNode(node, fileInfo, config);
+      if (!isKnownHelper(node.path.original, config, invokableData) || isWallStreet(tagName)) {
+        return transformComponentNode(node, fileInfo, config);
       }
     },
     AttrNode: {


Test patch:
diff --git a/test/fixtures/with-telemetry/input/.editorconfig b/test/fixtures/with-telemetry/input/.editorconfig
index 219985c..c35a002 100644
--- a/test/fixtures/with-telemetry/input/.editorconfig
+++ b/test/fixtures/with-telemetry/input/.editorconfig
@@ -4,7 +4,6 @@
 
 root = true
 
-
 [*]
 end_of_line = lf
 charset = utf-8
diff --git a/test/fixtures/with-telemetry/input/.eslintignore b/test/fixtures/with-telemetry/input/.eslintignore
index d9807d3..6e72a05 100644
--- a/test/fixtures/with-telemetry/input/.eslintignore
+++ b/test/fixtures/with-telemetry/input/.eslintignore
@@ -11,6 +11,9 @@
 
 # misc
 /coverage/
+!.*
+.*/
+.eslintcache
 
 # ember-try
 /.node_modules.ember-try/
diff --git a/test/fixtures/with-telemetry/input/.eslintrc.js b/test/fixtures/with-telemetry/input/.eslintrc.js
index a522eb9..41a1a43 100644
--- a/test/fixtures/with-telemetry/input/.eslintrc.js
+++ b/test/fixtures/with-telemetry/input/.eslintrc.js
@@ -1,39 +1,58 @@
+'use strict';
+
 module.exports = {
   root: true,
+  parser: 'babel-eslint',
   parserOptions: {
-    ecmaVersion: 2017,
-    sourceType: 'module'
+    ecmaVersion: 2018,
+    sourceType: 'module',
+    ecmaFeatures: {
+      legacyDecorators: true,
+    },
   },
-  plugins: [
-    'ember'
-  ],
+  plugins: ['ember'],
   extends: [
     'eslint:recommended',
-    'plugin:ember/recommended'
+    'plugin:ember/recommended',
+    'plugin:prettier/recommended',
   ],
   env: {
-    browser: true
-  },
-  rules: {
+    browser: true,
   },
+  rules: {},
   overrides: [
     // node files
     {
       files: [
-        'ember-cli-build.js',
-        'testem.js',
-        'blueprints/*/index.js',
-        'config/**/*.js',
-        'lib/*/index.js'
+        './.eslintrc.js',
+        './.prettierrc.js',
+        './.template-lintrc.js',
+        './ember-cli-build.js',
+        './testem.js',
+        './blueprints/*/index.js',
+        './config/**/*.js',
+        './lib/*/index.js',
+        './server/**/*.js',
       ],
       parserOptions: {
         sourceType: 'script',
-        ecmaVersion: 2015
       },
       env: {
         browser: false,
-        node: true
-      }
-    }
-  ]
+        node: true,
+      },
+      plugins: ['node'],
+      extends: ['plugin:node/recommended'],
+      rules: {
+        // this can be removed once the following is fixed
+        // https://github.com/mysticatea/eslint-plugin-node/issues/77
+        'node/no-unpublished-require': 'off',
+      },
+    },
+    {
+      // Test files:
+      files: ['tests/**/*-test.{js,ts}'],
+      extends: ['plugin:qunit/recommended'],
+    },
+  ],
 };
diff --git a/test/fixtures/with-telemetry/input/.gitignore b/test/fixtures/with-telemetry/input/.gitignore
index 29c9bc6..81fbed4 100644
--- a/test/fixtures/with-telemetry/input/.gitignore
+++ b/test/fixtures/with-telemetry/input/.gitignore
@@ -10,6 +10,7 @@
 
 # misc
 /.sass-cache
+/.eslintcache
 /connect.lock
 /coverage/
 /libpeerconnection.log
diff --git a/test/fixtures/with-telemetry/input/.prettierignore b/test/fixtures/with-telemetry/input/.prettierignore
new file mode 100644
index 0000000..9221655
--- /dev/null
+++ b/test/fixtures/with-telemetry/input/.prettierignore
@@ -0,0 +1,21 @@
+# unconventional js
+/blueprints/*/files/
+/vendor/
+
+# compiled output
+/dist/
+/tmp/
+
+# dependencies
+/bower_components/
+/node_modules/
+
+# misc
+/coverage/
+!.*
+.eslintcache
+
+# ember-try
+/.node_modules.ember-try/
+/bower.json.ember-try
+/package.json.ember-try
diff --git a/test/fixtures/with-telemetry/input/.prettierrc.js b/test/fixtures/with-telemetry/input/.prettierrc.js
new file mode 100644
index 0000000..534e6d3
--- /dev/null
+++ b/test/fixtures/with-telemetry/input/.prettierrc.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = {
+  singleQuote: true,
+};
diff --git a/test/fixtures/with-telemetry/input/.template-lintrc.js b/test/fixtures/with-telemetry/input/.template-lintrc.js
new file mode 100644
index 0000000..f35f61c
--- /dev/null
+++ b/test/fixtures/with-telemetry/input/.template-lintrc.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = {
+  extends: 'recommended',
+};
diff --git a/test/fixtures/with-telemetry/input/.travis.yml b/test/fixtures/with-telemetry/input/.travis.yml
index 54dcdb2..91f46e7 100644
--- a/test/fixtures/with-telemetry/input/.travis.yml
+++ b/test/fixtures/with-telemetry/input/.travis.yml
@@ -1,10 +1,9 @@
 ---
 language: node_js
 node_js:
-  - "6"
+  - "12"
 
-sudo: false
-dist: trusty
+dist: xenial
 
 addons:
   chrome: stable
@@ -18,9 +17,13 @@ env:
     # See https://git.io/vdao3 for details.
     - JOBS=1
 
+branches:
+  only:
+    - master
+
 before_install:
-  - npm config set spin false
+  - curl -o- -L https://yarnpkg.com/install.sh | bash
+  - export PATH=$HOME/.yarn/bin:$PATH
 
 script:
-  - npm run lint:js
-  - npm test
+  - yarn test
diff --git a/test/fixtures/with-telemetry/input/README.md b/test/fixtures/with-telemetry/input/README.md
index 01e693b..37c81d3 100644
--- a/test/fixtures/with-telemetry/input/README.md
+++ b/test/fixtures/with-telemetry/input/README.md
@@ -35,8 +35,8 @@ Make use of the many generators for code, try `ember help generate` for more det
 
 ### Linting
 
-* `npm run lint:js`
-* `npm run lint:js -- --fix`
+* `yarn lint`
+* `yarn lint:fix`
 
 ### Building
 
diff --git a/test/fixtures/with-telemetry/input/app/app.js b/test/fixtures/with-telemetry/input/app/app.js
index b3b2bd6..dcf416e 100644
--- a/test/fixtures/with-telemetry/input/app/app.js
+++ b/test/fixtures/with-telemetry/input/app/app.js
@@ -1,14 +1,12 @@
 import Application from '@ember/application';
-import Resolver from './resolver';
+import Resolver from 'ember-resolver';
 import loadInitializers from 'ember-load-initializers';
-import config from './config/environment';
+import config from 'app/config/environment';
 
-const App = Application.extend({
-  modulePrefix: config.modulePrefix,
-  podModulePrefix: config.podModulePrefix,
-  Resolver
-});
+export default class App extends Application {
+  modulePrefix = config.modulePrefix;
+  podModulePrefix = config.podModulePrefix;
+  Resolver = Resolver;
+}
 
 loadInitializers(App, config.modulePrefix);
-
-export default App;
diff --git a/test/fixtures/with-telemetry/input/app/controllers/application.js b/test/fixtures/with-telemetry/input/app/controllers/application.js
new file mode 100644
index 0000000..f3c7d13
--- /dev/null
+++ b/test/fixtures/with-telemetry/input/app/controllers/application.js
@@ -0,0 +1,6 @@
+import Controller from '@ember/controller';
+import { action } from '@ember/object';
+
+export default class ApplicationController extends Controller {
+  @action addModal() {}
+}
diff --git a/test/fixtures/with-telemetry/input/app/resolver.js b/test/fixtures/with-telemetry/input/app/resolver.js
deleted file mode 100644
index 2fb563d..0000000
--- a/test/fixtures/with-telemetry/input/app/resolver.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import Resolver from 'ember-resolver';
-
-export default Resolver;
diff --git a/test/fixtures/with-telemetry/input/app/router.js b/test/fixtures/with-telemetry/input/app/router.js
index d0bb009..7458feb 100644
--- a/test/fixtures/with-telemetry/input/app/router.js
+++ b/test/fixtures/with-telemetry/input/app/router.js
@@ -1,12 +1,9 @@
 import EmberRouter from '@ember/routing/router';
-import config from './config/environment';
+import config from 'app/config/environment';
 
-const Router = EmberRouter.extend({
-  location: config.locationType,
-  rootURL: config.rootURL
-});
+export default class Router extends EmberRouter {
+  location = config.locationType;
+  rootURL = config.rootURL;
+}
 
-Router.map(function() {
-});
-
-export default Router;
+Router.map(function () {});
diff --git a/test/fixtures/with-telemetry/input/app/templates/application.hbs b/test/fixtures/with-telemetry/input/app/templates/application.hbs
index b1a4f12..f665128 100644
--- a/test/fixtures/with-telemetry/input/app/templates/application.hbs
+++ b/test/fixtures/with-telemetry/input/app/templates/application.hbs
@@ -28,7 +28,7 @@
     {{/if}}
     {{outlet}}
 
-    {{#bs-button id="openModal" onClick=(action "addModal")}}Open{{/bs-button}}
+    {{#bs-button id="openModal" onClick=(action this.addModal)}}Open{{/bs-button}}
 
     {{#if hasModal}}
       {{#bs-modal-simple open=modal onHidden=(action "removeModal") title="Dynamic Dialog"}}
diff --git a/test/fixtures/with-telemetry/input/app/templates/components/.gitkeep b/test/fixtures/with-telemetry/input/app/templates/components/.gitkeep
deleted file mode 100644
index e69de29..0000000
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 65a8f74..b9c03c0 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,2 @@
 <div>this template has no js </div>
-<BsButton @type="primary" @onClick={{action "submit"}}>Primary</BsButton>
+{{#bs-button type="primary"}}Primary{{/bs-button}}
diff --git a/test/fixtures/with-telemetry/input/config/ember-cli-update.json b/test/fixtures/with-telemetry/input/config/ember-cli-update.json
new file mode 100644
index 0000000..c4895e6
--- /dev/null
+++ b/test/fixtures/with-telemetry/input/config/ember-cli-update.json
@@ -0,0 +1,20 @@
+{
+  "schemaVersion": "1.0.0",
+  "packages": [
+    {
+      "name": "ember-cli",
+      "version": "3.28.6",
+      "blueprints": [
+        {
+          "name": "app",
+          "outputRepo": "https://github.com/ember-cli/ember-new-output",
+          "codemodsSource": "ember-app-codemods-manifest@1",
+          "isBaseBlueprint": true,
+          "options": [
+            "--yarn"
+          ]
+        }
+      ]
+    }
+  ]
+}
diff --git a/test/fixtures/with-telemetry/input/config/environment.js b/test/fixtures/with-telemetry/input/config/environment.js
index 5fa181e..439371d 100644
--- a/test/fixtures/with-telemetry/input/config/environment.js
+++ b/test/fixtures/with-telemetry/input/config/environment.js
@@ -1,6 +1,6 @@
 'use strict';
 
-module.exports = function(environment) {
+module.exports = function (environment) {
   let ENV = {
     modulePrefix: 'app',
     environment,
@@ -13,14 +13,14 @@ module.exports = function(environment) {
       },
       EXTEND_PROTOTYPES: {
         // Prevent Ember Data from overriding Date.parse.
-        Date: false
-      }
+        Date: false,
+      },
     },
 
     APP: {
       // Here you can pass flags/options to your application instance
       // when it is created
-    }
+    },
   };
 
   if (environment === 'development') {
diff --git a/test/fixtures/with-telemetry/input/config/optional-features.json b/test/fixtures/with-telemetry/input/config/optional-features.json
index 21f1dc7..b26286e 100644
--- a/test/fixtures/with-telemetry/input/config/optional-features.json
+++ b/test/fixtures/with-telemetry/input/config/optional-features.json
@@ -1,3 +1,6 @@
 {
-  "jquery-integration": true
+  "application-template-wrapper": false,
+  "default-async-observers": true,
+  "jquery-integration": false,
+  "template-only-glimmer-components": true
 }
diff --git a/test/fixtures/with-telemetry/input/config/targets.js b/test/fixtures/with-telemetry/input/config/targets.js
index 8ffae36..3cd797a 100644
--- a/test/fixtures/with-telemetry/input/config/targets.js
+++ b/test/fixtures/with-telemetry/input/config/targets.js
@@ -3,16 +3,24 @@
 const browsers = [
   'last 1 Chrome versions',
   'last 1 Firefox versions',
-  'last 1 Safari versions'
+  'last 1 Safari versions',
 ];
 
-const isCI = !!process.env.CI;
-const isProduction = process.env.EMBER_ENV === 'production';
-
-if (isCI || isProduction) {
-  browsers.push('ie 11');
-}
+// Ember's browser support policy is changing, and IE11 support will end in
+// v4.0 onwards.
+//
+// See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy
+//
+// If you need IE11 support on a version of Ember that still offers support
+// for it, uncomment the code block below.
+//
+// const isCI = Boolean(process.env.CI);
+// const isProduction = process.env.EMBER_ENV === 'production';
+//
+// if (isCI || isProduction) {
+//   browsers.push('ie 11');
+// }
 
 module.exports = {
-  browsers
+  browsers,
 };
diff --git a/test/fixtures/with-telemetry/input/ember-cli-build.js b/test/fixtures/with-telemetry/input/ember-cli-build.js
index 1402bb2..d3bbd68 100644
--- a/test/fixtures/with-telemetry/input/ember-cli-build.js
+++ b/test/fixtures/with-telemetry/input/ember-cli-build.js
@@ -2,7 +2,7 @@
 
 const EmberApp = require('ember-cli/lib/broccoli/ember-app');
 
-module.exports = function(defaults) {
+module.exports = function (defaults) {
   let app = new EmberApp(defaults, {
     'ember-bootstrap': {
       'bootstrapVersion': 4,
diff --git a/test/fixtures/with-telemetry/input/package.json b/test/fixtures/with-telemetry/input/package.json
index 335bdc2..fa38ae1 100644
--- a/test/fixtures/with-telemetry/input/package.json
+++ b/test/fixtures/with-telemetry/input/package.json
@@ -11,44 +11,62 @@
   },
   "repository": "",
   "scripts": {
-    "build": "ember build",
-    "lint:js": "eslint .",
+    "build": "ember build --environment=production",
+    "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"",
+    "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix",
+    "lint:hbs:fix": "ember-template-lint . --fix",
+    "lint:js": "eslint . --cache",
+    "lint:js:fix": "eslint . --fix",
     "start": "ember serve",
-    "test": "ember test"
+    "test": "npm-run-all lint test:*",
+    "test:ember": "ember test"
   },
   "devDependencies": {
-    "@ember/jquery": "^0.6.0",
-    "@ember/optional-features": "^0.7.0",
+    "@ember/optional-features": "^2.0.0",
+    "@ember/test-helpers": "^2.6.0",
+    "@glimmer/component": "^1.0.4",
+    "@glimmer/tracking": "^1.0.4",
+    "babel-eslint": "^10.1.0",
     "bootstrap": "^4.3.1",
     "broccoli-asset-rev": "^3.0.0",
-    "ember-ajax": "^5.0.0",
-    "ember-bootstrap": "^2.8.1",
-    "ember-cli": "~3.10.1",
-    "ember-cli-app-version": "^3.2.0",
-    "ember-cli-babel": "^7.7.3",
-    "ember-cli-dependency-checker": "^3.1.0",
-    "ember-cli-eslint": "^5.1.0",
-    "ember-cli-htmlbars": "^3.0.1",
-    "ember-cli-htmlbars-inline-precompile": "^2.1.0",
-    "ember-cli-inject-live-reload": "^1.8.2",
+    "ember-auto-import": "^1.12.0",
+    "ember-bootstrap": "^4.0.0",
+    "ember-cli": "~3.28.6",
+    "ember-cli-app-version": "^5.0.0",
+    "ember-cli-babel": "^7.26.10",
+    "ember-cli-dependency-checker": "^3.2.0",
+    "ember-cli-htmlbars": "^5.7.2",
+    "ember-cli-inject-live-reload": "^2.1.0",
     "ember-cli-sri": "^2.1.1",
     "ember-cli-string-helpers": "^4.0.5",
-    "ember-cli-template-lint": "^1.0.0-beta.1",
-    "ember-cli-uglify": "^2.1.0",
-    "ember-data": "~3.10.0",
-    "ember-export-application-global": "^2.0.0",
-    "ember-load-initializers": "^2.0.0",
+    "ember-cli-terser": "^4.0.2",
+    "ember-data": "~3.28.6",
+    "ember-export-application-global": "^2.0.1",
+    "ember-fetch": "^8.1.1",
+    "ember-load-initializers": "^2.1.2",
     "ember-maybe-import-regenerator": "^0.1.6",
-    "ember-qunit": "^4.4.1",
-    "ember-resolver": "^5.0.1",
-    "ember-source": "~3.10.0",
-    "ember-welcome-page": "^4.0.0",
-    "eslint-plugin-ember": "^6.2.0",
-    "eslint-plugin-node": "^9.0.1",
+    "ember-page-title": "^6.2.2",
+    "ember-qunit": "^5.1.5",
+    "ember-resolver": "^8.0.3",
+    "ember-source": "~3.28.8",
+    "ember-template-lint": "^3.15.0",
+    "ember-welcome-page": "^4.1.0",
+    "eslint": "^7.32.0",
+    "eslint-config-prettier": "^8.3.0",
+    "eslint-plugin-ember": "^10.5.8",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-prettier": "^3.4.1",
+    "eslint-plugin-qunit": "^6.2.0",
     "loader.js": "^4.7.0",
-    "qunit-dom": "^0.8.4"
+    "npm-run-all": "^4.1.5",
+    "prettier": "^2.5.1",
+    "qunit": "^2.17.2",
+    "qunit-dom": "^1.6.0"
   },
   "engines": {
-    "node": "6.* || 8.* || >= 10.*"
+    "node": "12.* || 14.* || >= 16"
+  },
+  "ember": {
+    "edition": "octane"
   }
 }
diff --git a/test/fixtures/with-telemetry/input/testem.js b/test/fixtures/with-telemetry/input/testem.js
index 726d187..ed2f371 100644
--- a/test/fixtures/with-telemetry/input/testem.js
+++ b/test/fixtures/with-telemetry/input/testem.js
@@ -1,25 +1,23 @@
+'use strict';
+
 module.exports = {
   test_page: 'tests/index.html?hidepassed',
   disable_watching: true,
-  launch_in_ci: [
-    'Chrome'
-  ],
-  launch_in_dev: [
-    'Chrome'
-  ],
+  launch_in_ci: ['Chrome'],
+  launch_in_dev: ['Chrome'],
+  browser_start_timeout: 120,
   browser_args: {
     Chrome: {
       ci: [
         // --no-sandbox is needed when running Chrome inside a container
         process.env.CI ? '--no-sandbox' : null,
         '--headless',
-        '--disable-gpu',
         '--disable-dev-shm-usage',
         '--disable-software-rasterizer',
         '--mute-audio',
         '--remote-debugging-port=0',
-        '--window-size=1440,900'
-      ].filter(Boolean)
-    }
-  }
+        '--window-size=1440,900',
+      ].filter(Boolean),
+    },
+  },
 };
diff --git a/test/fixtures/with-telemetry/input/tests/index.html b/test/fixtures/with-telemetry/input/tests/index.html
index cd1f0c9..6d60dd0 100644
--- a/test/fixtures/with-telemetry/input/tests/index.html
+++ b/test/fixtures/with-telemetry/input/tests/index.html
@@ -21,7 +21,14 @@
     {{content-for "body"}}
     {{content-for "test-body"}}
 
-    <script src="/testem.js" integrity=""></script>
+    <div id="qunit"></div>
+    <div id="qunit-fixture">
+      <div id="ember-testing-container">
+        <div id="ember-testing"></div>
+      </div>
+    </div>
+
+    <script src="/testem.js" integrity="" data-embroider-ignore></script>
     <script src="{{rootURL}}assets/vendor.js"></script>
     <script src="{{rootURL}}assets/test-support.js"></script>
     <script src="{{rootURL}}assets/app.js"></script>
diff --git a/test/fixtures/with-telemetry/input/tests/test-helper.js b/test/fixtures/with-telemetry/input/tests/test-helper.js
index 0382a84..c54b36a 100644
--- a/test/fixtures/with-telemetry/input/tests/test-helper.js
+++ b/test/fixtures/with-telemetry/input/tests/test-helper.js
@@ -1,8 +1,12 @@
-import Application from '../app';
-import config from '../config/environment';
+import Application from 'app/app';
+import config from 'app/config/environment';
+import * as QUnit from 'qunit';
 import { setApplication } from '@ember/test-helpers';
+import { setup } from 'qunit-dom';
 import { start } from 'ember-qunit';
 
 setApplication(Application.create(config.APP));
 
+setup(QUnit.assert);
+
 start();
diff --git a/test/fixtures/with-telemetry/output/app/app.js b/test/fixtures/with-telemetry/output/app/app.js
index b3b2bd6..dcf416e 100644
--- a/test/fixtures/with-telemetry/output/app/app.js
+++ b/test/fixtures/with-telemetry/output/app/app.js
@@ -1,14 +1,12 @@
 import Application from '@ember/application';
-import Resolver from './resolver';
+import Resolver from 'ember-resolver';
 import loadInitializers from 'ember-load-initializers';
-import config from './config/environment';
+import config from 'app/config/environment';
 
-const App = Application.extend({
-  modulePrefix: config.modulePrefix,
-  podModulePrefix: config.podModulePrefix,
-  Resolver
-});
+export default class App extends Application {
+  modulePrefix = config.modulePrefix;
+  podModulePrefix = config.podModulePrefix;
+  Resolver = Resolver;
+}
 
 loadInitializers(App, config.modulePrefix);
-
-export default App;
diff --git a/test/fixtures/with-telemetry/output/app/controllers/application.js b/test/fixtures/with-telemetry/output/app/controllers/application.js
new file mode 100644
index 0000000..f3c7d13
--- /dev/null
+++ b/test/fixtures/with-telemetry/output/app/controllers/application.js
@@ -0,0 +1,6 @@
+import Controller from '@ember/controller';
+import { action } from '@ember/object';
+
+export default class ApplicationController extends Controller {
+  @action addModal() {}
+}
diff --git a/test/fixtures/with-telemetry/output/app/resolver.js b/test/fixtures/with-telemetry/output/app/resolver.js
deleted file mode 100644
index 2fb563d..0000000
--- a/test/fixtures/with-telemetry/output/app/resolver.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import Resolver from 'ember-resolver';
-
-export default Resolver;
diff --git a/test/fixtures/with-telemetry/output/app/router.js b/test/fixtures/with-telemetry/output/app/router.js
index d0bb009..7458feb 100644
--- a/test/fixtures/with-telemetry/output/app/router.js
+++ b/test/fixtures/with-telemetry/output/app/router.js
@@ -1,12 +1,9 @@
 import EmberRouter from '@ember/routing/router';
-import config from './config/environment';
+import config from 'app/config/environment';
 
-const Router = EmberRouter.extend({
-  location: config.locationType,
-  rootURL: config.rootURL
-});
+export default class Router extends EmberRouter {
+  location = config.locationType;
+  rootURL = config.rootURL;
+}
 
-Router.map(function() {
-});
-
-export default Router;
+Router.map(function () {});
diff --git a/test/fixtures/with-telemetry/output/app/templates/application.hbs b/test/fixtures/with-telemetry/output/app/templates/application.hbs
index ddf4852..077434d 100644
--- a/test/fixtures/with-telemetry/output/app/templates/application.hbs
+++ b/test/fixtures/with-telemetry/output/app/templates/application.hbs
@@ -14,9 +14,9 @@
     </BsNav>
   </div>
   <div class="col-sm-8 col-sm-pull-4 col-md-9 col-md-pull-3">
-    {{utils/bee-bop}}
-    {{-wat-wat}}
-    {{utils/-wat-wat}}
+    {{(utils/bee-bop)}}
+    {{(-wat-wat)}}
+    {{(utils/-wat-wat)}}
     {{#if this.isDetailPage}}
       <h1>
         {{currentComponent.title}}
@@ -24,11 +24,11 @@
       <p class="lead">
         {{currentComponent.description}}
       </p>
-      {{api-reference component=this.currentComponent}}
+      <ApiReference @component={{this.currentComponent}} />
     {{/if}}
     {{outlet}}
 
-    <BsButton @id="openModal" @onClick={{action "addModal"}}>Open</BsButton>
+    <BsButton @id="openModal" @onClick={{action this.addModal}}>Open</BsButton>
 
     {{#if hasModal}}
       <BsModalSimple @open={{modal}} @onHidden={{action "removeModal"}} @title="Dynamic Dialog">
diff --git a/test/fixtures/with-telemetry/output/app/templates/components/.gitkeep b/test/fixtures/with-telemetry/output/app/templates/components/.gitkeep
deleted file mode 100644
index e69de29..0000000
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 65a8f74..4c50d5d 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,2 @@
 <div>this template has no js </div>
-<BsButton @type="primary" @onClick={{action "submit"}}>Primary</BsButton>
+<BsButton @type="primary">Primary</BsButton>
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 47fb0ec..44dd7b8 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 @@
 <button>
-  {{titleize "string helpers for ember"}}
+  {{(titleize "string helpers for ember")}}
 </button>
-{{biz-baz  canConvert="no" why="helper" where="local"}}
+{{(biz-baz canConvert="no" why="helper" where="local")}}
diff --git a/transforms/angle-brackets/transform.test.js b/transforms/angle-brackets/transform.test.js
index 86256cf..15daa3f 100644
--- a/transforms/angle-brackets/transform.test.js
+++ b/transforms/angle-brackets/transform.test.js
@@ -288,7 +288,6 @@ test('deeply-nested-sub', () => {
       )
     }}
   `;
-
   /**
    * NOTE: An issue has been opened in `ember-template-recast` (https://github.com/ember-template-lint/ember-template-recast/issues/82)
    * regarding to create an API to allow a transform to customize the whitespace for newly created nodes.
@@ -477,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}}
@@ -555,8 +554,8 @@ test('link-to-inline', () => {
         <LinkTo @route=\\"apps.segments\\" class=\\"tabs__discrete-tab\\" @activeClass=\\"o__selected\\" @current-when=\\"apps.segments\\" data-test-segment-link=\\"segments\\">Segments</LinkTo>
         <LinkTo @route={{this.dynamicPath}} class=\\"tabs__discrete-tab\\" @activeClass=\\"o__selected\\" @current-when=\\"apps.segments\\" data-test-segment-link=\\"segments\\">Segments</LinkTo>
         <LinkTo @route=\\"apps.app.companies.segments.segment\\" @model={{segment}} class=\\"t__em-link\\">{{segment.name}}</LinkTo>
-        <LinkTo @route=\\"flight\\" @model={{event.flight.id}} class=\\"btn btn-default btn-sm pull-right\\">{{t \\"show\\"}}</LinkTo>
-        <LinkTo @route=\\"user\\" @model={{if linkActor event.actor.id event.user.id}}>{{t \\"show\\"}}</LinkTo>
+        <LinkTo @route=\\"flight\\" @model={{event.flight.id}} class=\\"btn btn-default btn-sm pull-right\\">{{(t \\"show\\")}}</LinkTo>
+        <LinkTo @route=\\"user\\" @model={{if linkActor event.actor.id event.user.id}}>{{(t \\"show\\")}}</LinkTo>
         <LinkTo @route=\\"user\\" @models={{array this.first this.second}}>Show</LinkTo>
         <LinkTo @route=\\"user\\" @models={{array this.first this.second}} @query={{hash foo=\\"baz\\"}}>Show</LinkTo>
         <LinkTo @route=\\"user\\" @model={{this.first}}>Show</LinkTo>
@@ -840,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)}}
       "
   `);
 });
@@ -879,7 +878,7 @@ test('tilde', () => {
   expect(runTest('tilde.hbs', input)).toMatchInlineSnapshot(`
     "
         {{#if foo~}}
-          <SomeComponent />
+          {{some-component}}
         {{/if}}
       "
   `);
@@ -978,7 +977,7 @@ test('skip-default-helpers', () => {
   expect(runTest('skip-default-helpers.hbs', input, options)).toMatchInlineSnapshot(`
     "
         <div id=\\"main-container\\">
-          {{liquid-outlet}}
+          {{(liquid-outlet)}}
         </div>
         <button {{action \\"toggle\\" \\"showA\\"}}>
           Toggle A/B</button>
@@ -998,11 +997,11 @@ test('skip-default-helpers', () => {
             Two
           </div>
         {{/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)}}
         <SomeComponent @foo={{true}} />
-        {{some-helper1 foo=true}}
-        {{some-helper2 foo=true}}
+        {{(some-helper1 foo=true)}}
+        {{(some-helper2 foo=true)}}
       "
   `);
 });
@@ -1040,7 +1039,7 @@ test('skip-default-helpers (no-config)', () => {
   expect(runTest('skip-default-helpers.hbs', input)).toMatchInlineSnapshot(`
     "
         <div id=\\"main-container\\">
-          {{liquid-outlet}}
+          {{(liquid-outlet)}}
         </div>
         <button {{action \\"toggle\\" \\"showA\\"}}>
           Toggle A/B</button>
@@ -1060,8 +1059,8 @@ test('skip-default-helpers (no-config)', () => {
             Two
           </div>
         {{/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)}}
         <SomeComponent @foo={{true}} />
         <SomeHelper1 @foo={{true}} />
         <SomeHelper2 @foo={{true}} />
@@ -1087,8 +1086,8 @@ test('custom-options', () => {
   expect(runTest('custom-options.hbs', input, options)).toMatchInlineSnapshot(`
     "
         <SomeComponent @foo={{true}} />
-        {{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}}
@@ -1181,7 +1180,7 @@ test('preserve arguments', () => {
     "
         <FooBar @class=\\"baz\\" />
         {{foo-bar data-baz class=\\"baz\\"}}
-        <LinkTo @route=\\"flight\\" @model={{event.flight.id}} class=\\"btn btn-default btn-sm pull-right\\">{{t \\"show\\"}}</LinkTo>
+        <LinkTo @route=\\"flight\\" @model={{event.flight.id}} class=\\"btn btn-default btn-sm pull-right\\">{{(t \\"show\\")}}</LinkTo>
       "
   `);
 });
@@ -1278,7 +1277,6 @@ test('wallstreet-telemetry', () => {
   let input = `
     {{nested$helper}}
     {{nested::helper}}
-    {{nested$helper param="cool!"}}
     {{nested::helper param="yeah!"}}
     {{helper-1}}
   `;
@@ -1286,10 +1284,27 @@ test('wallstreet-telemetry', () => {
   expect(runTest('wallstreet-telemetry.hbs', input)).toMatchInlineSnapshot(`
     "
         {{nested$helper}}
-        {{nested::helper}}
-        {{nested$helper param=\\"cool!\\"}}
-        {{nested::helper param=\\"yeah!\\"}}
-        {{helper-1}}
+        {{(nested::helper)}}
+        {{(nested::helper param=\\"yeah!\\")}}
+        {{(helper-1)}}
+      "
+  `);
+});
+
+test('wrapping-helpers-with-parens', () => {
+  let input = `
+    {{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\\")}}
+        {{foounknownhelper}}
       "
   `);
 });
@@ -1335,3 +1350,39 @@ test('No telemetry', () => {
       "
   `);
 });
+
+test('pipe', () => {
+  let input = `<Foo @title={{concat ">"}} as |bar|></Foo>`;
+
+  expect(runTestWithData('pipe.hbs', input, {}, {})).toMatchInlineSnapshot(
+    `"<Foo @title={{concat \\">\\"}} as |bar|></Foo>"`
+  );
+});
+
+test('outlet', () => {
+  let input = `{{outlet}}`;
+
+  expect(runTestWithData('pipe.hbs', input, {}, {})).toMatchInlineSnapshot(`"{{outlet}}"`);
+});
+
+test('yield', () => {
+  let input = `{{yield}}`;
+
+  expect(runTestWithData('pipe.hbs', input, {}, {})).toMatchInlineSnapshot(`"{{yield}}"`);
+});
+
+test('unknown helper with args', () => {
+  let input = `
+    {{api-reference component=this.currentComponent}}
+    {{api-reference someArg}}
+    {{api-reference}}
+  `;
+
+  expect(runTestWithData('pipe.hbs', input, {}, {})).toMatchInlineSnapshot(`
+    "
+        <ApiReference @component={{this.currentComponent}} />
+        {{api-reference someArg}}
+        {{api-reference}}
+      "
+  `);
+});