diff --git a/.gitignore b/.gitignore
index 3e88eb2cce..bb90e4af47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,6 @@ npm-debug.log
# eslint-remote-tester
eslint-remote-tester-results
+
+# npm lock file (project uses pnpm)
+package-lock.json
diff --git a/lib/rules/template-missing-invokable.js b/lib/rules/template-missing-invokable.js
index c13e00cff0..4024a2c62b 100644
--- a/lib/rules/template-missing-invokable.js
+++ b/lib/rules/template-missing-invokable.js
@@ -1,3 +1,25 @@
+'use strict';
+
+// Invokables that are available in every Ember project without any extra
+// packages. User-provided `invokables` config is merged on top of these so
+// any entry here can be overridden by the consuming project.
+const BUILTIN_INVOKABLES = {
+ fn: ['fn', '@ember/helper'],
+ get: ['get', '@ember/helper'],
+ hash: ['hash', '@ember/helper'],
+ array: ['array', '@ember/helper'],
+ concat: ['concat', '@ember/helper'],
+ htmlSafe: ['htmlSafe', '@ember/template'],
+ trustedHTML: ['trustedHTML', '@ember/template'],
+ LinkTo: ['LinkTo', '@ember/routing'],
+ on: ['on', '@ember/modifier'],
+ trackedArray: ['trackedArray', '@ember/reactive/collections'],
+ trackedObject: ['trackedObject', '@ember/reactive/collections'],
+ trackedSet: ['trackedSet', '@ember/reactive/collections'],
+ trackedWeakSet: ['trackedWeakSet', '@ember/reactive/collections'],
+ trackedWeakMap: ['trackedWeakMap', '@ember/reactive/collections'],
+};
+
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
@@ -34,15 +56,16 @@ module.exports = {
create: (context) => {
const sourceCode = context.sourceCode;
+ const invokables = { ...BUILTIN_INVOKABLES, ...context.options[0]?.invokables };
// takes a node with a `.path` property
function checkInvokable(node) {
if (node.path.type === 'GlimmerPathExpression' && node.path.tail.length === 0) {
if (!isBound(node.path.head, sourceCode.getScope(node.path))) {
- const matched = context.options[0]?.invokables?.[node.path.head.name];
+ const matched = invokables[node.path.head.name];
if (matched) {
- const [name, module] = matched;
- const importStatement = buildImportStatement(node.path.head.name, name, module);
+ const [name, moduleName] = matched;
+ const importStatement = buildImportStatement(node.path.head.name, name, moduleName);
context.report({
node: node.path,
messageId: 'missing-invokable',
diff --git a/tests/fixtures/projects/has-ember-truth-helpers/package.json b/tests/fixtures/projects/has-ember-truth-helpers/package.json
new file mode 100644
index 0000000000..aa8a8d0bd3
--- /dev/null
+++ b/tests/fixtures/projects/has-ember-truth-helpers/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "has-ember-truth-helpers",
+ "dependencies": {
+ "ember-truth-helpers": "*"
+ }
+}
diff --git a/tests/lib/rules/template-missing-invokable.js b/tests/lib/rules/template-missing-invokable.js
index 0aa1706021..ddcb806901 100644
--- a/tests/lib/rules/template-missing-invokable.js
+++ b/tests/lib/rules/template-missing-invokable.js
@@ -65,10 +65,24 @@ ruleTester.run('template-missing-invokable', rule, {
`,
+
+ // Built-in invokables are not reported when already imported
+ `
+ import { fn } from '@ember/helper';
+
+ {{fn myFunc 1}}
+
+ `,
+ `
+ import { LinkTo } from '@ember/routing';
+
+ Home
+
+ `,
],
invalid: [
- // Subexpression invocations
+ // Subexpression invocations — always auto-fixes when invokable is configured
{
code: `
@@ -96,7 +110,7 @@ ruleTester.run('template-missing-invokable', rule, {
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},
- // Mustache Invocations
+ // Mustache Invocations — always auto-fixes when invokable is configured
{
code: `
@@ -142,7 +156,7 @@ ruleTester.run('template-missing-invokable', rule, {
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},
- // Modifier Inovcations
+ // Modifier Invocations — always auto-fixes when invokable is configured
{
code: `
function doSomething() {}
@@ -243,5 +257,96 @@ ruleTester.run('template-missing-invokable', rule, {
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},
+
+ // Built-in: fn — auto-fixes without any user config
+ {
+ code: `
+
+ {{fn myFunc 1}}
+
+ `,
+ output: `import { fn } from '@ember/helper';
+
+
+ {{fn myFunc 1}}
+
+ `,
+ errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
+ },
+
+ // Built-in: hash — auto-fixes without any user config
+ {
+ code: `
+
+
+
+ `,
+ output: `import { hash } from '@ember/helper';
+
+
+
+
+ `,
+ errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
+ },
+
+ // Built-in: on modifier — auto-fixes without any user config
+ {
+ code: `
+ function doSomething() {}
+
+
+
+ `,
+ output: `import { on } from '@ember/modifier';
+
+ function doSomething() {}
+
+
+
+ `,
+ errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
+ },
+
+ // Built-in: LinkTo — auto-fixes without any user config
+ {
+ code: `
+
+ Home
+
+ `,
+ output: `import { LinkTo } from '@ember/routing';
+
+
+ Home
+
+ `,
+ errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
+ },
+
+ // User config overrides a built-in
+ {
+ code: `
+ function doSomething() {}
+
+
+
+ `,
+ output: `import { on } from 'my-custom-modifier-package';
+
+ function doSomething() {}
+
+
+
+ `,
+ options: [
+ {
+ invokables: {
+ on: ['on', 'my-custom-modifier-package'],
+ },
+ },
+ ],
+ errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
+ },
],
});