diff --git a/packages/cli/package.json b/packages/cli/package.json index 9ecff67..9307719 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -20,7 +20,7 @@ "directory": "packages/cli" }, "files": [ - "dist" + "dist/**/*.js" ], "bin": { "attw": "./dist/index.js" diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 01edf54..f1aa8c3 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -32,6 +32,10 @@ export interface Opts { configPath?: string; ignoreRules?: string[]; format: Format; + + entrypoints?: string[]; + includeEntrypoints?: string[]; + excludeEntrypoints?: string[]; } program @@ -49,17 +53,28 @@ particularly ESM-related module resolution issues.` "[file-directory-or-package-spec]", "the packed .tgz, or directory containing package.json with --pack, or package spec with --from-npm" ) - .option("-P, --pack", "run `npm pack` in the specified directory and delete the resulting .tgz file afterwards") - .option("-p, --from-npm", "read from the npm registry instead of a local file") - .addOption(new Option("-f, --format ", "specify the print format").choices(formats).default("table")) - .option("-q, --quiet", "don't print anything to STDOUT (overrides all other options)") + .option("-P, --pack", "Run `npm pack` in the specified directory and delete the resulting .tgz file afterwards") + .option("-p, --from-npm", "Read from the npm registry instead of a local file") + .addOption(new Option("-f, --format ", "Specify the print format").choices(formats).default("table")) + .option("-q, --quiet", "Don't print anything to STDOUT (overrides all other options)") + .option( + "--entrypoints ", + "Specify an exhaustive list of entrypoints to check. " + + 'The package root is `"." Specifying this option disables automatic entrypoint discovery, ' + + "and overrides the `--include-entrypoints` and `--exclude-entrypoints` options." + ) + .option( + "--include-entrypoints ", + "Specify entrypoints to check in addition to automatically discovered ones." + ) + .option("--exclude-entrypoints ", "Specify entrypoints to exclude from checking.") .addOption( - new Option("--ignore-rules ", "specify rules to ignore").choices(Object.values(problemFlags)).default([]) + new Option("--ignore-rules ", "Specify rules to ignore").choices(Object.values(problemFlags)).default([]) ) - .option("--summary, --no-summary", "whether to print summary information about the different errors") - .option("--emoji, --no-emoji", "whether to use any emojis") - .option("--color, --no-color", "whether to use any colors (the FORCE_COLOR env variable is also available)") - .option("--config-path ", "path to config file (default: ./.attw.json)") + .option("--summary, --no-summary", "Whether to print summary information about the different errors") + .option("--emoji, --no-emoji", "Whether to use any emojis") + .option("--color, --no-color", "Whether to use any colors (the FORCE_COLOR env variable is also available)") + .option("--config-path ", "Path to config file (default: ./.attw.json)") .action(async (fileOrDirectory = ".") => { const opts = program.opts(); await readConfig(program, opts.configPath); @@ -87,7 +102,12 @@ particularly ESM-related module resolution issues.` program.error(result.error); } else { analysis = await core.checkPackage( - await core.createPackageFromNpm(`${result.data.name}@${result.data.version}`) + await core.createPackageFromNpm(`${result.data.name}@${result.data.version}`), + { + entrypoints: opts.entrypoints, + includeEntrypoints: opts.includeEntrypoints, + excludeEntrypoints: opts.excludeEntrypoints, + } ); } } catch (error) { @@ -136,7 +156,11 @@ particularly ESM-related module resolution issues.` } const file = await readFile(fileName); const data = new Uint8Array(file); - analysis = await core.checkPackage(await core.createPackageFromTarballData(data)); + analysis = await core.checkPackage(await core.createPackageFromTarballData(data), { + entrypoints: opts.entrypoints, + includeEntrypoints: opts.includeEntrypoints, + excludeEntrypoints: opts.excludeEntrypoints, + }); } catch (error) { handleError(error, "checking file"); } diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 5f2503b..4b3ae9b 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -9,7 +9,7 @@ ], "outDir": "./dist", "declarationDir": "./lib", - "sourceMap": false, + "sourceMap": true, }, "include": [ "src" diff --git a/packages/core/src/checkPackage.ts b/packages/core/src/checkPackage.ts index ebb331a..b0698c6 100644 --- a/packages/core/src/checkPackage.ts +++ b/packages/core/src/checkPackage.ts @@ -6,7 +6,24 @@ import type { Package } from "./createPackage.js"; import { createCompilerHosts, type CompilerHosts, CompilerHostWrapper } from "./multiCompilerHost.js"; import type { CheckResult, EntrypointInfo, EntrypointResolutionAnalysis, Resolution, ResolutionKind } from "./types.js"; -export async function checkPackage(pkg: Package): Promise { +export interface CheckPackageOptions { + /** + * Exhaustive list of entrypoints to check. The package root is `"."`. + * Specifying this option disables automatic entrypoint discovery, + * and overrides the `includeEntrypoints` and `excludeEntrypoints` options. + */ + entrypoints?: string[]; + /** + * Entrypoints to check in addition to automatically discovered ones. + */ + includeEntrypoints?: string[]; + /** + * Entrypoints to exclude from checking. + */ + excludeEntrypoints?: (string | RegExp)[]; +} + +export async function checkPackage(pkg: Package, options?: CheckPackageOptions): Promise { const files = pkg.listFiles(); const types = files.some(ts.hasTSFileExtension) ? "included" : false; const parts = files[0].split("/"); @@ -21,7 +38,7 @@ export async function checkPackage(pkg: Package): Promise { } const hosts = createCompilerHosts(pkg); - const entrypointResolutions = getEntrypointInfo(packageName, pkg, hosts); + const entrypointResolutions = getEntrypointInfo(packageName, pkg, hosts, options); const entrypointResolutionProblems = getEntrypointResolutionProblems(entrypointResolutions, hosts); const resolutionBasedFileProblems = getResolutionBasedFileProblems(packageName, entrypointResolutions, hosts); const fileProblems = getFileProblems(entrypointResolutions, hosts); @@ -35,6 +52,46 @@ export async function checkPackage(pkg: Package): Promise { }; } +function getEntrypoints(fs: Package, exportsObject: any, options: CheckPackageOptions | undefined): string[] { + if (options?.entrypoints) { + return options.entrypoints.map((e) => formatEntrypointString(e, fs.packageName)); + } + if (exportsObject === undefined && fs) { + return getProxyDirectories(`/node_modules/${fs.packageName}`, fs); + } + const detectedSubpaths = getSubpaths(exportsObject); + if (detectedSubpaths.length === 0) { + detectedSubpaths.push("."); + } + const included = Array.from( + new Set([ + ...detectedSubpaths, + ...(options?.includeEntrypoints?.map((e) => formatEntrypointString(e, fs.packageName)) ?? []), + ]) + ); + if (!options?.excludeEntrypoints) { + return included; + } + return included.filter((entrypoint) => { + return !options.excludeEntrypoints!.some((exclusion) => { + if (typeof exclusion === "string") { + return formatEntrypointString(exclusion, fs.packageName) === entrypoint; + } + return exclusion.test(entrypoint); + }); + }); +} + +function formatEntrypointString(path: string, packageName: string) { + return ( + path === "." || path.startsWith("./") + ? path + : path.startsWith(`${packageName}/`) + ? `.${path.slice(packageName.length)}` + : `./${path}` + ).trim(); +} + function getSubpaths(exportsObject: any): string[] { if (!exportsObject || typeof exportsObject !== "object" || Array.isArray(exportsObject)) { return []; @@ -62,13 +119,14 @@ function getProxyDirectories(rootDir: string, fs: Package) { .filter((f) => f !== "./"); } -function getEntrypointInfo(packageName: string, fs: Package, hosts: CompilerHosts): Record { +function getEntrypointInfo( + packageName: string, + fs: Package, + hosts: CompilerHosts, + options: CheckPackageOptions | undefined +): Record { const packageJson = JSON.parse(fs.readFile(`/node_modules/${packageName}/package.json`)); - const subpaths = getSubpaths(packageJson.exports); - const entrypoints = subpaths.length ? subpaths : ["."]; - if (!packageJson.exports) { - entrypoints.push(...getProxyDirectories(`/node_modules/${packageName}`, fs)); - } + const entrypoints = getEntrypoints(fs, packageJson.exports, options); const result: Record = {}; for (const entrypoint of entrypoints) { const resolutions: Record = {