Skip to content

Commit

Permalink
Implement entrypoint customization
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewbranch committed Jul 9, 2023
1 parent 445b1f2 commit 14c73d3
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 21 deletions.
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"directory": "packages/cli"
},
"files": [
"dist"
"dist/**/*.js"
],
"bin": {
"attw": "./dist/index.js"
Expand Down
46 changes: 35 additions & 11 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface Opts {
configPath?: string;
ignoreRules?: string[];
format: Format;

entrypoints?: string[];
includeEntrypoints?: string[];
excludeEntrypoints?: string[];
}

program
Expand All @@ -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 <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 <format>", "Specify the print format").choices(formats).default("table"))
.option("-q, --quiet", "Don't print anything to STDOUT (overrides all other options)")
.option(
"--entrypoints <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 <entrypoints...>",
"Specify entrypoints to check in addition to automatically discovered ones."
)
.option("--exclude-entrypoints <entrypoints...>", "Specify entrypoints to exclude from checking.")
.addOption(
new Option("--ignore-rules <rules...>", "specify rules to ignore").choices(Object.values(problemFlags)).default([])
new Option("--ignore-rules <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>", "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>", "Path to config file (default: ./.attw.json)")
.action(async (fileOrDirectory = ".") => {
const opts = program.opts<Opts>();
await readConfig(program, opts.configPath);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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");
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
],
"outDir": "./dist",
"declarationDir": "./lib",
"sourceMap": false,
"sourceMap": true,
},
"include": [
"src"
Expand Down
74 changes: 66 additions & 8 deletions packages/core/src/checkPackage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CheckResult> {
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<CheckResult> {
const files = pkg.listFiles();
const types = files.some(ts.hasTSFileExtension) ? "included" : false;
const parts = files[0].split("/");
Expand All @@ -21,7 +38,7 @@ export async function checkPackage(pkg: Package): Promise<CheckResult> {
}

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);
Expand All @@ -35,6 +52,46 @@ export async function checkPackage(pkg: Package): Promise<CheckResult> {
};
}

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 [];
Expand Down Expand Up @@ -62,13 +119,14 @@ function getProxyDirectories(rootDir: string, fs: Package) {
.filter((f) => f !== "./");
}

function getEntrypointInfo(packageName: string, fs: Package, hosts: CompilerHosts): Record<string, EntrypointInfo> {
function getEntrypointInfo(
packageName: string,
fs: Package,
hosts: CompilerHosts,
options: CheckPackageOptions | undefined
): Record<string, EntrypointInfo> {
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<string, EntrypointInfo> = {};
for (const entrypoint of entrypoints) {
const resolutions: Record<ResolutionKind, EntrypointResolutionAnalysis> = {
Expand Down

0 comments on commit 14c73d3

Please sign in to comment.