Skip to content

Commit

Permalink
WIP: cjs named exports
Browse files Browse the repository at this point in the history
  • Loading branch information
laverdet committed Mar 20, 2024
1 parent 28c9e5e commit c445010
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 7 deletions.
1 change: 1 addition & 0 deletions packages/cli/src/problemUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const problemFlags = {
CJSResolvesToESM: "cjs-resolves-to-esm",
FallbackCondition: "fallback-condition",
CJSOnlyExportsDefault: "cjs-only-exports-default",
CJSNamedExports: "cjs-named-exports",
FalseExportDefault: "false-export-default",
MissingExportEquals: "missing-export-equals",
UnexpectedModuleSyntax: "unexpected-module-syntax",
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
},
"dependencies": {
"@andrewbranch/untar.js": "^1.0.3",
"cjs-module-lexer": "^1.2.3",
"fflate": "^0.8.2",
"semver": "^7.5.4",
"ts-expose-internals-conditionally": "1.0.0-empty.0",
Expand Down
55 changes: 55 additions & 0 deletions packages/core/src/internal/checks/cjsNamedExports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import ts from "typescript";
import type { Package } from "../../createPackage.js";
import { init, parse } from "cjs-module-lexer";
import { defineCheck } from "../defineCheck.js";

await init();

function* crawlExports(pkg: Package, fileName: string, seen = new Set<string>()): Iterable<string> {
if (seen.has(fileName)) {
return;
}
seen.add(fileName);
const result = parse(pkg.readFile(fileName));
yield* result.exports;
for (const relativeName of result.reexports) {
if (relativeName.startsWith(".")) {
const resolvedName = new URL(relativeName, `cjs://${fileName}`);
yield* crawlExports(pkg, String(resolvedName).slice(6), seen);
}
}
}

export default defineCheck({
name: "CJSNamedExports",
dependencies: ({ entrypoints, subpath, resolutionKind }) => {
const entrypoint = entrypoints[subpath].resolutions[resolutionKind];
const moduleType = entrypoint.implementationResolution?.isCommonJS ? ("cjs" as const) : undefined;
const typesFileName = entrypoint.resolution?.fileName;
const implementationFileName = entrypoint.implementationResolution?.fileName;
return [implementationFileName, typesFileName, resolutionKind, moduleType];
},
execute: ([implementationFileName, typesFileName, resolutionKind, moduleType], context) => {
if (!implementationFileName || !typesFileName || resolutionKind !== "node16-esm" || moduleType !== "cjs") {
return;
}
const exports = [...crawlExports(context.pkg, implementationFileName)];
const host = context.hosts.findHostForFiles([typesFileName])!;
const typesSourceFile = host.getSourceFile(typesFileName)!;
const typeChecker = host.createAuxiliaryProgram([typesFileName]).getTypeChecker();
const typesExports = typeChecker.getExportsOfModule(typesSourceFile.symbol);
const expectedNames = typesExports
.flatMap((node) => [...(node.declarations?.values() ?? [])])
.filter((node) => !ts.isTypeDeclaration(node))
.map((declaration) => declaration.symbol.escapedName);
const missingNames = expectedNames.filter((name) => !exports.includes(String(name)));
if (missingNames.length > 0) {
console.log("missing", missingNames);
return {
kind: "CJSNamedExports",
implementationFileName,
typesFileName,
};
}
},
});
2 changes: 2 additions & 0 deletions packages/core/src/internal/checks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import cjsOnlyExportsDefault from "./cjsOnlyExportsDefault.js";
import cjsNamedExports from "./cjsNamedExports.js";
import entrypointResolutions from "./entrypointResolutions.js";
import exportDefaultDisagreement from "./exportDefaultDisagreement.js";
import internalResolutionError from "./internalResolutionError.js";
Expand All @@ -9,6 +10,7 @@ export default [
entrypointResolutions,
moduleKindDisagreement,
exportDefaultDisagreement,
cjsNamedExports,
cjsOnlyExportsDefault,
unexpectedModuleSyntax,
internalResolutionError,
Expand Down
23 changes: 16 additions & 7 deletions packages/core/src/internal/getEntrypointInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ function getEntrypoints(fs: Package, exportsObject: unknown, options: CheckPacka
const proxies = getProxyDirectories(rootDir, fs);
if (proxies.length === 0) {
if (options?.entrypointsLegacy) {
return fs.listFiles()
.filter(f => !ts.isDeclarationFileName(f) && extensions.has(f.slice(f.lastIndexOf("."))))
.map(f => "." + f.slice(rootDir.length));
return fs
.listFiles()
.filter((f) => !ts.isDeclarationFileName(f) && extensions.has(f.slice(f.lastIndexOf("."))))
.map((f) => "." + f.slice(rootDir.length));
}
return ["."];
}
Expand Down Expand Up @@ -96,6 +97,7 @@ export function getEntrypointInfo(
options: CheckPackageOptions | undefined,
): Record<string, EntrypointInfo> {
const packageJson = JSON.parse(fs.readFile(`/node_modules/${packageName}/package.json`));
const typeIsModule = packageJson.type === "module";
let entrypoints = getEntrypoints(fs, packageJson.exports, options);
if (fs.typesPackage) {
const typesPackageJson = JSON.parse(fs.readFile(`/node_modules/${fs.typesPackage.packageName}/package.json`));
Expand All @@ -105,10 +107,10 @@ export function getEntrypointInfo(
const result: Record<string, EntrypointInfo> = {};
for (const entrypoint of entrypoints) {
const resolutions: Record<ResolutionKind, EntrypointResolutionAnalysis> = {
node10: getEntrypointResolution(packageName, hosts.node10, "node10", entrypoint),
"node16-cjs": getEntrypointResolution(packageName, hosts.node16, "node16-cjs", entrypoint),
"node16-esm": getEntrypointResolution(packageName, hosts.node16, "node16-esm", entrypoint),
bundler: getEntrypointResolution(packageName, hosts.bundler, "bundler", entrypoint),
node10: getEntrypointResolution(packageName, typeIsModule, hosts.node10, "node10", entrypoint),
"node16-cjs": getEntrypointResolution(packageName, typeIsModule, hosts.node16, "node16-cjs", entrypoint),
"node16-esm": getEntrypointResolution(packageName, typeIsModule, hosts.node16, "node16-esm", entrypoint),
bundler: getEntrypointResolution(packageName, typeIsModule, hosts.bundler, "bundler", entrypoint),
};
result[entrypoint] = {
subpath: entrypoint,
Expand All @@ -121,6 +123,7 @@ export function getEntrypointInfo(
}
function getEntrypointResolution(
packageName: string,
typeIsModule: boolean,
host: CompilerHostWrapper,
resolutionKind: ResolutionKind,
entrypoint: string,
Expand Down Expand Up @@ -167,6 +170,12 @@ function getEntrypointResolution(

return {
fileName,
isESM:
resolution.resolvedModule.extension === ts.Extension.Mjs ||
(typeIsModule && resolution.resolvedModule.extension === ts.Extension.Js),
isCommonJS:
resolution.resolvedModule.extension === ts.Extension.Cjs ||
(!typeIsModule && resolution.resolvedModule.extension === ts.Extension.Js),
isJson: resolution.resolvedModule.extension === ts.Extension.Json,
isTypeScript: ts.hasTSFileExtension(resolution.resolvedModule.resolvedFileName),
trace,
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/problems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export const problemKindInfo: Record<ProblemKind, ProblemKindInfo> = {
description: "Import resolved to an ESM type declaration file, but a CommonJS JavaScript file.",
docsUrl: "https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md",
},
CJSNamedExports: {
emoji: "🕵️",
title: "Types include CJS named exports which are not present in the implementation",
shortDescription: "Named CJS types",
description: "docs",
docsUrl: "docs",
},
CJSResolvesToESM: {
emoji: "⚠️",
title: "Entrypoint is ESM-only",
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export interface EntrypointResolutionAnalysis {
export interface Resolution {
fileName: string;
isTypeScript: boolean;
isESM: boolean;
isCommonJS: boolean;
isJson: boolean;
trace: string[];
}
Expand Down Expand Up @@ -126,6 +128,10 @@ export interface CJSResolvesToESMProblem extends EntrypointResolutionProblem {
kind: "CJSResolvesToESM";
}

export interface CJSNamedExportsProblem extends FilePairProblem {
kind: "CJSNamedExports";
}

export interface FallbackConditionProblem extends EntrypointResolutionProblem {
kind: "FallbackCondition";
}
Expand Down Expand Up @@ -162,6 +168,7 @@ export type Problem =
| FalseESMProblem
| FalseCJSProblem
| CJSResolvesToESMProblem
| CJSNamedExportsProblem
| FallbackConditionProblem
| FalseExportDefaultProblem
| MissingExportEqualsProblem
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c445010

Please sign in to comment.