Skip to content

Commit bf2827a

Browse files
committed
CJS & ESM static analysis
1 parent 7010f60 commit bf2827a

16 files changed

+1359
-65
lines changed

Diff for: packages/cli/src/problemUtils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const problemFlags = {
99
CJSResolvesToESM: "cjs-resolves-to-esm",
1010
FallbackCondition: "fallback-condition",
1111
CJSOnlyExportsDefault: "cjs-only-exports-default",
12-
CJSNamedExports: "cjs-named-exports",
12+
NamedExports: "named-exports",
1313
FalseExportDefault: "false-export-default",
1414
MissingExportEquals: "missing-export-equals",
1515
UnexpectedModuleSyntax: "unexpected-module-syntax",

Diff for: packages/core/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
},
2626
"type": "module",
2727
"imports": {
28-
"#internal/*": "./dist/internal/*"
28+
"#*": "./dist/*"
2929
},
3030
"exports": {
3131
".": {
@@ -51,6 +51,7 @@
5151
},
5252
"dependencies": {
5353
"@andrewbranch/untar.js": "^1.0.3",
54+
"acorn": "^8.11.3",
5455
"cjs-module-lexer": "^1.2.3",
5556
"fflate": "^0.8.2",
5657
"semver": "^7.5.4",

Diff for: packages/core/src/internal/checks/cjsNamedExports.ts

-55
This file was deleted.

Diff for: packages/core/src/internal/checks/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import cjsOnlyExportsDefault from "./cjsOnlyExportsDefault.js";
2-
import cjsNamedExports from "./cjsNamedExports.js";
2+
import namedExports from "./namedExports.js";
33
import entrypointResolutions from "./entrypointResolutions.js";
44
import exportDefaultDisagreement from "./exportDefaultDisagreement.js";
55
import internalResolutionError from "./internalResolutionError.js";
@@ -10,7 +10,7 @@ export default [
1010
entrypointResolutions,
1111
moduleKindDisagreement,
1212
exportDefaultDisagreement,
13-
cjsNamedExports,
13+
namedExports,
1414
cjsOnlyExportsDefault,
1515
unexpectedModuleSyntax,
1616
internalResolutionError,

Diff for: packages/core/src/internal/checks/namedExports.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import ts from "typescript";
2+
import { defineCheck } from "../defineCheck.js";
3+
import { getEsmModuleNamespace } from "../esm/esmNamespace.js";
4+
5+
export default defineCheck({
6+
name: "NamedExports",
7+
dependencies: ({ entrypoints, subpath, resolutionKind }) => {
8+
const entrypoint = entrypoints[subpath].resolutions[resolutionKind];
9+
const typesFileName = entrypoint.resolution?.fileName;
10+
const implementationFileName = entrypoint.implementationResolution?.fileName;
11+
return [implementationFileName, typesFileName, resolutionKind];
12+
},
13+
execute: ([implementationFileName, typesFileName, resolutionKind], context) => {
14+
if (!implementationFileName || !typesFileName || resolutionKind !== "node16-esm") {
15+
return;
16+
}
17+
18+
// Get declared exported names from TypeScript
19+
const host = context.hosts.findHostForFiles([typesFileName])!;
20+
const typesSourceFile = host.getSourceFile(typesFileName)!;
21+
const typeChecker = host.createAuxiliaryProgram([typesFileName]).getTypeChecker();
22+
const typesExports = typeChecker.getExportsAndPropertiesOfModule(typesSourceFile.symbol);
23+
const expectedNames = typesExports
24+
.flatMap((node) => [...(node.declarations?.values() ?? [])])
25+
.filter((node) => !ts.isTypeAlias(node) && !ts.isTypeDeclaration(node) && !ts.isNamespaceBody(node))
26+
.map((declaration) => declaration.symbol.escapedName);
27+
28+
// Get actual exported names as seen by nodejs
29+
const exports = getEsmModuleNamespace(context.pkg, implementationFileName);
30+
const missing = expectedNames.filter((name) => !exports.includes(String(name))).map(String);
31+
if (missing.length > 0) {
32+
console.log("🚨", implementationFileName, missing);
33+
return {
34+
kind: "NamedExports",
35+
implementationFileName,
36+
typesFileName,
37+
missing,
38+
};
39+
}
40+
},
41+
});

Diff for: packages/core/src/internal/esm/cjsBindings.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { Exports } from "cjs-module-lexer";
2+
import { init, parse as cjsParse } from "cjs-module-lexer";
3+
4+
await init();
5+
6+
export function getCjsModuleBindings(sourceText: string): Exports {
7+
return cjsParse(sourceText);
8+
}

Diff for: packages/core/src/internal/esm/cjsNamespace.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Package } from "../../createPackage.js";
2+
import { getCjsModuleBindings } from "./cjsBindings.js";
3+
import { cjsResolve } from "./cjsResolve.js";
4+
5+
export function getCjsModuleNamespace(fs: Package, file: URL, seen = new Set<string>()) {
6+
seen.add(file.pathname);
7+
const { exports, reexports } = getCjsModuleBindings(fs.readFile(file.pathname));
8+
9+
// CJS always exports `default`
10+
if (!exports.includes("default")) {
11+
exports.push("default");
12+
}
13+
14+
// Additionally, resolve facade reexports
15+
const lastResolvableReexport = (() => {
16+
for (const source of reexports.reverse()) {
17+
try {
18+
return cjsResolve(fs, source, file);
19+
} catch {}
20+
}
21+
})();
22+
if (
23+
lastResolvableReexport &&
24+
lastResolvableReexport.format === "commonjs" &&
25+
!seen.has(lastResolvableReexport.resolved.pathname)
26+
) {
27+
const extra = getCjsModuleNamespace(fs, lastResolvableReexport.resolved, seen);
28+
exports.push(...extra.filter((name) => !exports.includes(name)));
29+
}
30+
31+
return exports;
32+
}

0 commit comments

Comments
 (0)