Skip to content

Commit c11b89a

Browse files
Only auto-fix non-built-in invokables when package is in project package.json
Co-authored-by: NullVoxPopuli <[email protected]>
1 parent e2747b4 commit c11b89a

4 files changed

Lines changed: 14193 additions & 16 deletions

File tree

lib/rules/template-missing-invokable.js

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,58 @@
1+
'use strict';
2+
3+
const path = require('node:path');
4+
const fs = require('node:fs');
5+
6+
// Packages that ship with Ember/Glimmer are always available to auto-fix.
7+
function isBuiltinPackage(moduleName) {
8+
return moduleName.startsWith('@ember/') || moduleName.startsWith('@glimmer/');
9+
}
10+
11+
// Returns the root package name from a module specifier, e.g.
12+
// 'ember-truth-helpers' -> 'ember-truth-helpers'
13+
// 'ember-truth-helpers/helpers' -> 'ember-truth-helpers'
14+
// '@scope/pkg/deep' -> '@scope/pkg'
15+
function rootPackageName(moduleName) {
16+
if (moduleName.startsWith('@')) {
17+
const parts = moduleName.split('/');
18+
return parts.slice(0, 2).join('/');
19+
}
20+
return moduleName.split('/')[0];
21+
}
22+
23+
// Walk up the directory tree from startDir to find the nearest package.json.
24+
function findNearestPackageJson(startDir) {
25+
let dir = startDir;
26+
let parent = path.dirname(dir);
27+
while (dir !== parent) {
28+
const candidate = path.join(dir, 'package.json');
29+
if (fs.existsSync(candidate)) {
30+
return candidate;
31+
}
32+
dir = parent;
33+
parent = path.dirname(dir);
34+
}
35+
return null;
36+
}
37+
38+
function isPackageInProjectDeps(moduleName, fileDir) {
39+
try {
40+
const pkgPath = findNearestPackageJson(fileDir);
41+
if (!pkgPath) {
42+
return false;
43+
}
44+
const packageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
45+
const pkg = rootPackageName(moduleName);
46+
return Boolean(
47+
(packageJson.dependencies && pkg in packageJson.dependencies) ||
48+
(packageJson.devDependencies && pkg in packageJson.devDependencies) ||
49+
(packageJson.peerDependencies && pkg in packageJson.peerDependencies)
50+
);
51+
} catch {
52+
return false;
53+
}
54+
}
55+
156
/** @type {import('eslint').Rule.RuleModule} */
257
module.exports = {
358
meta: {
@@ -41,14 +96,23 @@ module.exports = {
4196
if (!isBound(node.path.head, sourceCode.getScope(node.path))) {
4297
const matched = context.options[0]?.invokables?.[node.path.head.name];
4398
if (matched) {
44-
const [name, module] = matched;
45-
const importStatement = buildImportStatement(node.path.head.name, name, module);
99+
const [name, moduleName] = matched;
100+
const fileDir = path.dirname(
101+
path.resolve(context.getPhysicalFilename?.() ?? context.getFilename())
102+
);
103+
const canAutoFix =
104+
isBuiltinPackage(moduleName) ||
105+
isPackageInProjectDeps(moduleName, fileDir);
106+
107+
const importStatement = buildImportStatement(node.path.head.name, name, moduleName);
46108
context.report({
47109
node: node.path,
48110
messageId: 'missing-invokable',
49-
fix(fixer) {
50-
return fixer.insertTextBeforeRange([0, 0], `${importStatement};\n`);
51-
},
111+
fix: canAutoFix
112+
? function (fixer) {
113+
return fixer.insertTextBeforeRange([0, 0], `${importStatement};\n`);
114+
}
115+
: null,
52116
});
53117
}
54118
}

0 commit comments

Comments
 (0)