Skip to content

Commit 8d27fb2

Browse files
AlexanderKarandreyfus9243081j
authored
feat: add core-js check (#182)
* Add option for JSON output file * Moved back to JSON output * Adding Core-JS detection to CLI * Update core-js.ts * Fixed lint issues in test * Update src/analyze/core-js.ts Co-authored-by: paul valladares <85648028+dreyfus92@users.noreply.github.com> * Move to filesystem * Change target * Added comment * More detection * Import * Removed build check * Install * Vitest Version Missmatch * Merged in vitest fix * Removed lock * Fixes * Updated package * Updated files filter --------- Co-authored-by: paul valladares <85648028+dreyfus92@users.noreply.github.com> Co-authored-by: James Garbutt <43081j@users.noreply.github.com>
1 parent dc8629e commit 8d27fb2

8 files changed

Lines changed: 497 additions & 20 deletions

File tree

package-lock.json

Lines changed: 46 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"obug": "^2.1.1",
5858
"package-manager-detector": "^1.6.0",
5959
"publint": "^0.3.18",
60+
"core-js-compat": "^3.48.0",
6061
"semver": "^7.7.4",
6162
"tinyglobby": "^0.2.16"
6263
},

src/analyze/core-js.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {glob} from 'tinyglobby';
2+
import {minVersion} from 'semver';
3+
import {relative, join} from 'path';
4+
import type {AnalysisContext, ReportPluginResult} from '../types.js';
5+
6+
import coreJsCompat from 'core-js-compat';
7+
8+
const BROAD_IMPORTS = new Set([
9+
'core-js',
10+
'core-js/stable',
11+
'core-js/actual',
12+
'core-js/full'
13+
]);
14+
15+
const SOURCE_GLOB = ['**/*.{js,ts,mjs,cjs,jsx,tsx}'];
16+
const SOURCE_IGNORE = [
17+
'**/node_modules/**',
18+
'**/dist/**',
19+
'**/build/**',
20+
'**/coverage/**',
21+
'**/lib/**'
22+
];
23+
24+
const IMPORT_RE =
25+
/(?:import\s+(?:.*\s+from\s+)?|require\s*\()\s*['"]([^'"]+)['"]/g;
26+
27+
export async function runCoreJsAnalysis(
28+
context: AnalysisContext
29+
): Promise<ReportPluginResult> {
30+
const messages: ReportPluginResult['messages'] = [];
31+
const pkg = context.packageFile;
32+
33+
const hasCoreJs =
34+
'core-js' in (pkg.dependencies ?? {}) ||
35+
'core-js' in (pkg.devDependencies ?? {}) ||
36+
'core-js-pure' in (pkg.dependencies ?? {}) ||
37+
'core-js-pure' in (pkg.devDependencies ?? {});
38+
39+
if (!hasCoreJs) {
40+
return {messages};
41+
}
42+
43+
const nodeRange = pkg.engines?.node;
44+
let targetVersion = 'current';
45+
if (nodeRange) {
46+
const floor = minVersion(nodeRange);
47+
if (floor) {
48+
targetVersion = floor.version;
49+
}
50+
}
51+
52+
const {list: unnecessaryForTarget} = coreJsCompat.compat({
53+
targets: {node: targetVersion},
54+
inverse: true
55+
});
56+
const unnecessarySet = new Set(unnecessaryForTarget);
57+
58+
const srcGlobs = context.options?.src;
59+
const patterns = srcGlobs && srcGlobs.length > 0 ? srcGlobs : SOURCE_GLOB;
60+
const allFiles = await glob(patterns, {
61+
cwd: context.root,
62+
ignore: SOURCE_IGNORE
63+
});
64+
// filter out any paths that escaped context.root via ../
65+
const files = allFiles.filter(
66+
(f) => !relative(context.root, join(context.root, f)).startsWith('..')
67+
);
68+
69+
for (const filePath of files) {
70+
let source: string;
71+
try {
72+
source = await context.fs.readFile(filePath);
73+
} catch {
74+
continue;
75+
}
76+
77+
for (const [, specifier] of source.matchAll(IMPORT_RE)) {
78+
if (BROAD_IMPORTS.has(specifier)) {
79+
messages.push({
80+
severity: 'warning',
81+
score: 0,
82+
message: `Broad core-js import "${specifier}" in ${filePath} loads all polyfills at once. Import only the specific modules you need.`
83+
});
84+
} else if (specifier.startsWith('core-js/modules/')) {
85+
const moduleName = specifier.slice('core-js/modules/'.length);
86+
if (unnecessarySet.has(moduleName)) {
87+
messages.push({
88+
severity: 'suggestion',
89+
score: 0,
90+
message: `core-js polyfill "${moduleName}" imported in ${filePath} is unnecessary — your Node.js target (>= ${targetVersion}) already supports this natively.`
91+
});
92+
}
93+
}
94+
}
95+
}
96+
97+
return {messages};
98+
}

src/analyze/report.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import {runPlugins} from '../plugin-runner.js';
1616
import {getPackageJson, detectLockfile} from '../utils/package-json.js';
1717
import {parse as parseLockfile} from 'lockparse';
1818
import {runDuplicateDependencyAnalysis} from './duplicate-dependencies.js';
19+
import {runCoreJsAnalysis} from './core-js.js';
1920

2021
const plugins: ReportPlugin[] = [
2122
runPublint,
2223
runReplacements,
2324
runDependencyAnalysis,
24-
runDuplicateDependencyAnalysis
25+
runDuplicateDependencyAnalysis,
26+
runCoreJsAnalysis
2527
];
2628

2729
async function computeInfo(fileSystem: FileSystem) {

src/commands/analyze.meta.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ export const meta = {
3939
default: false,
4040
description:
4141
'Output results as JSON to stdout (messages follow --quiet or resolved --report-level)'
42+
},
43+
src: {
44+
type: 'string',
45+
multiple: true,
46+
description:
47+
'Glob pattern(s) for source files to scan for imports (e.g. "src/**/*.ts"). Defaults to scanning all JS/TS files from the project root.'
4248
}
4349
}
4450
} as const;

src/commands/analyze.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,12 @@ export async function run(ctx: CommandContext<typeof meta>) {
101101
}
102102

103103
const customManifests = ctx.values['manifest'];
104+
const srcDirs = ctx.values['src'];
104105

105106
const {stats, messages} = await report({
106107
root,
107108
manifest: customManifests,
109+
src: srcDirs,
108110
categories: parsedCategories
109111
});
110112

0 commit comments

Comments
 (0)