@codefast/cli 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +97 -12
  3. package/dist/arrange/domain/ast/translator.js +42 -28
  4. package/dist/arrange/simplify/process-file.d.ts +1 -1
  5. package/dist/audit/assertions/cli-result.d.ts +13 -0
  6. package/dist/audit/assertions/cli-result.js +22 -0
  7. package/dist/audit/assertions/cli-schema.d.ts +18 -0
  8. package/dist/audit/assertions/cli-schema.js +12 -0
  9. package/dist/audit/assertions/domain/double-assertion.d.ts +20 -0
  10. package/dist/audit/assertions/domain/double-assertion.js +122 -0
  11. package/dist/audit/assertions/output.d.ts +7 -0
  12. package/dist/audit/assertions/output.js +21 -0
  13. package/dist/audit/assertions/prepare.d.ts +16 -0
  14. package/dist/audit/assertions/prepare.js +12 -0
  15. package/dist/audit/assertions/run.d.ts +14 -0
  16. package/dist/audit/assertions/run.js +40 -0
  17. package/dist/audit/command.js +45 -1
  18. package/dist/audit/constants/cli-result.d.ts +13 -0
  19. package/dist/audit/constants/cli-result.js +22 -0
  20. package/dist/audit/constants/cli-schema.d.ts +18 -0
  21. package/dist/audit/constants/cli-schema.js +12 -0
  22. package/dist/audit/constants/domain/constants.d.ts +8 -0
  23. package/dist/audit/constants/domain/constants.js +66 -0
  24. package/dist/audit/constants/output.d.ts +7 -0
  25. package/dist/audit/constants/output.js +21 -0
  26. package/dist/audit/constants/prepare.d.ts +16 -0
  27. package/dist/audit/constants/prepare.js +37 -0
  28. package/dist/audit/constants/run.d.ts +14 -0
  29. package/dist/audit/constants/run.js +64 -0
  30. package/dist/audit/display-names/domain/display-names.js +1 -9
  31. package/dist/audit/domain/types.d.ts +85 -0
  32. package/dist/audit/imports/domain/import-policy.js +4 -18
  33. package/dist/audit/publish/cli-result.d.ts +1 -1
  34. package/dist/audit/publish/cli-result.js +6 -3
  35. package/dist/audit/publish/domain/stylesheet-sources.d.ts +22 -0
  36. package/dist/audit/publish/domain/stylesheet-sources.js +64 -0
  37. package/dist/audit/publish/output.js +12 -3
  38. package/dist/audit/publish/run.d.ts +2 -1
  39. package/dist/audit/publish/run.js +30 -1
  40. package/dist/audit/publish/shipped-files.d.ts +21 -0
  41. package/dist/audit/publish/shipped-files.js +36 -0
  42. package/dist/core/config/schema.d.ts +5 -0
  43. package/dist/core/config/schema.js +2 -0
  44. package/dist/core/filesystem/filesystem.d.ts +3 -4
  45. package/dist/core/filesystem/node.js +1 -7
  46. package/dist/core/oxc-node.d.ts +32 -0
  47. package/dist/core/oxc-node.js +25 -0
  48. package/dist/core/source-position.d.ts +15 -0
  49. package/dist/core/source-position.js +26 -0
  50. package/dist/mirror/dist-filesystem-node.js +4 -14
  51. package/dist/pack-slim/run.js +1 -1
  52. package/dist/tag/domain/version-summary.d.ts +5 -2
  53. package/dist/tag/writer/since-writer.js +2 -4
  54. package/package.json +4 -4
  55. package/dist/mirror/domain/dirent-guard.d.ts +0 -10
  56. package/dist/mirror/domain/dirent-guard.js +0 -15
@@ -0,0 +1,16 @@
1
+ import type { AuditCommandPrelude } from "#audit/prepare";
2
+ import type { AppError } from "#core/errors";
3
+ import type { Filesystem } from "#core/filesystem/filesystem";
4
+ import type { Result } from "#core/result";
5
+ /**
6
+ * Loads config and resolves the scan target for `audit assertions`.
7
+ *
8
+ * @remarks Defaults to the repo root, tests included: a double assertion in a test silences the
9
+ * compiler on the code the test is meant to hold to its types.
10
+ *
11
+ * @since 0.13.0
12
+ */
13
+ export declare function prepareAssertionAudit(fs: Filesystem, args: {
14
+ readonly currentWorkingDirectory: string;
15
+ readonly rawTarget: string | undefined;
16
+ }): Promise<Result<AuditCommandPrelude, AppError>>;
@@ -0,0 +1,12 @@
1
+ import { prepareRepoRootAudit } from "#audit/prepare";
2
+ /**
3
+ * Loads config and resolves the scan target for `audit assertions`.
4
+ *
5
+ * @remarks Defaults to the repo root, tests included: a double assertion in a test silences the
6
+ * compiler on the code the test is meant to hold to its types.
7
+ *
8
+ * @since 0.13.0
9
+ */
10
+ export async function prepareAssertionAudit(fs, args) {
11
+ return prepareRepoRootAudit(fs, args, (config) => config.audit?.assertions?.allowlist ?? []);
12
+ }
@@ -0,0 +1,14 @@
1
+ import type { AssertionAuditResult } from "#audit/domain/types";
2
+ import { AppError } from "#core/errors";
3
+ import type { Filesystem } from "#core/filesystem/filesystem";
4
+ import type { Result } from "#core/result";
5
+ /**
6
+ * Scans a target path for double assertions through `unknown` or `any`, tests included.
7
+ *
8
+ * @since 0.13.0
9
+ */
10
+ export declare function runAssertionAudit(fs: Filesystem, args: {
11
+ readonly rootDir: string;
12
+ readonly targetPath: string;
13
+ readonly allowlist: ReadonlyArray<string>;
14
+ }): Result<AssertionAuditResult, AppError>;
@@ -0,0 +1,40 @@
1
+ import path from "node:path";
2
+ import { auditDoubleAssertionSource } from "#audit/assertions/domain/double-assertion";
3
+ import { AppError, messageFrom } from "#core/errors";
4
+ import { err, ok } from "#core/result";
5
+ import { walkTsxFiles } from "#core/workspace/typescript-walk";
6
+ /**
7
+ * Scans a target path for double assertions through `unknown` or `any`, tests included.
8
+ *
9
+ * @since 0.13.0
10
+ */
11
+ export function runAssertionAudit(fs, args) {
12
+ try {
13
+ const allowlist = new Set(args.allowlist);
14
+ const { rootDir, targetPath } = args;
15
+ const filesToScan = fs.statSync(targetPath).isFile() ? [targetPath] : walkTsxFiles(targetPath, fs);
16
+ const files = [];
17
+ let violationCount = 0;
18
+ let allowlistedCount = 0;
19
+ for (const absolutePath of filesToScan) {
20
+ const relativePath = path.relative(rootDir, absolutePath).split(path.sep).join("/");
21
+ const content = fs.readFileSync(absolutePath, "utf8");
22
+ const remaining = auditDoubleAssertionSource(absolutePath, content).filter(({ raw }) => {
23
+ const isAllowed = allowlist.has(raw) || allowlist.has(`${relativePath}:${raw}`);
24
+ if (isAllowed) {
25
+ allowlistedCount++;
26
+ }
27
+ return !isAllowed;
28
+ });
29
+ if (remaining.length === 0) {
30
+ continue;
31
+ }
32
+ violationCount += remaining.length;
33
+ files.push({ relativePath, violations: remaining });
34
+ }
35
+ return ok({ files, violationCount, allowlistedCount, scannedFileCount: filesToScan.length });
36
+ }
37
+ catch (caughtError) {
38
+ return err(new AppError("INFRA_FAILURE", messageFrom(caughtError), caughtError));
39
+ }
40
+ }
@@ -1,9 +1,19 @@
1
1
  import { Command } from "commander";
2
+ import { exitCodeForAssertionAuditResult, formatAssertionAuditJsonOutput } from "#audit/assertions/cli-result";
3
+ import { assertionAuditRunRequestSchema } from "#audit/assertions/cli-schema";
4
+ import { presentAssertionAuditResult } from "#audit/assertions/output";
5
+ import { prepareAssertionAudit } from "#audit/assertions/prepare";
6
+ import { runAssertionAudit } from "#audit/assertions/run";
2
7
  import { exitCodeForCommentAuditResult, formatCommentAuditJsonOutput } from "#audit/comments/cli-result";
3
8
  import { commentAuditRunRequestSchema } from "#audit/comments/cli-schema";
4
9
  import { presentCommentAuditResult } from "#audit/comments/output";
5
10
  import { prepareCommentAudit } from "#audit/comments/prepare";
6
11
  import { runCommentAudit } from "#audit/comments/run";
12
+ import { exitCodeForConstantAuditResult, formatConstantAuditJsonOutput } from "#audit/constants/cli-result";
13
+ import { constantAuditRunRequestSchema } from "#audit/constants/cli-schema";
14
+ import { presentConstantAuditResult } from "#audit/constants/output";
15
+ import { prepareConstantAudit } from "#audit/constants/prepare";
16
+ import { runConstantAudit } from "#audit/constants/run";
7
17
  import { exitCodeForDisplayNameAuditResult, formatDisplayNameAuditJsonOutput } from "#audit/display-names/cli-result";
8
18
  import { displayNameAuditRunRequestSchema } from "#audit/display-names/cli-schema";
9
19
  import { presentDisplayNameAuditResult } from "#audit/display-names/output";
@@ -79,6 +89,22 @@ const importsCheck = {
79
89
  formatJson: formatImportsAuditJsonOutput,
80
90
  exitCode: exitCodeForImportsAuditResult,
81
91
  };
92
+ const assertionsCheck = {
93
+ name: "assertions",
94
+ description: "Report double type assertions through unknown or any (x as unknown as T), tests included",
95
+ targetHelp: "Directory or file to scan (default: the repo root)",
96
+ schema: assertionAuditRunRequestSchema,
97
+ prepare: prepareAssertionAudit,
98
+ buildRequest: baseAuditRequest,
99
+ run: (fs, request) => runAssertionAudit(fs, {
100
+ rootDir: request.rootDir,
101
+ targetPath: request.targetPath,
102
+ allowlist: request.allowlist ?? [],
103
+ }),
104
+ present: presentAssertionAuditResult,
105
+ formatJson: formatAssertionAuditJsonOutput,
106
+ exitCode: exitCodeForAssertionAuditResult,
107
+ };
82
108
  const displayNamesCheck = {
83
109
  name: "display-names",
84
110
  description: "Report token(), tag() and module display names that break the <namespace>:<Name> convention",
@@ -95,6 +121,22 @@ const displayNamesCheck = {
95
121
  formatJson: formatDisplayNameAuditJsonOutput,
96
122
  exitCode: exitCodeForDisplayNameAuditResult,
97
123
  };
124
+ const constantsCheck = {
125
+ name: "constants",
126
+ description: "Report numeric constants whose comment names none of the three kinds a number may be",
127
+ targetHelp: "Directory or file to scan (default: audit.constants.target from config)",
128
+ schema: constantAuditRunRequestSchema,
129
+ prepare: prepareConstantAudit,
130
+ buildRequest: baseAuditRequest,
131
+ run: (fs, request) => runConstantAudit(fs, {
132
+ rootDir: request.rootDir,
133
+ targetPath: request.targetPath,
134
+ allowlist: request.allowlist ?? [],
135
+ }),
136
+ present: presentConstantAuditResult,
137
+ formatJson: formatConstantAuditJsonOutput,
138
+ exitCode: exitCodeForConstantAuditResult,
139
+ };
98
140
  const commentsCheck = {
99
141
  name: "comments",
100
142
  description: "Report section dividers that are not in the repo's one allowed form",
@@ -117,7 +159,7 @@ const commentsCheck = {
117
159
  };
118
160
  const publishCheck = {
119
161
  name: "publish",
120
- description: "Report what breaks a consumer's install: #/ imports and exports/imports targets not shipped",
162
+ description: "Report what breaks a consumer's install: #/ imports, unshipped exports/imports targets, and stylesheet @source paths reaching nothing shipped",
121
163
  targetHelp: "Directory or file to scan (default: the repo root)",
122
164
  schema: publishAuditRunRequestSchema,
123
165
  prepare: preparePublishAudit,
@@ -156,7 +198,9 @@ export function createAuditCommand() {
156
198
  registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(rtlCheck));
157
199
  registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(linksCheck));
158
200
  registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(importsCheck));
201
+ registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(assertionsCheck));
159
202
  registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(displayNamesCheck));
203
+ registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(constantsCheck));
160
204
  registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(commentsCheck));
161
205
  registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(publishCheck));
162
206
  return cmd;
@@ -0,0 +1,13 @@
1
+ import type { ConstantAuditResult } from "#audit/domain/types";
2
+ /**
3
+ * Exit `1` when any non-allowlisted numeric constant is unlabelled.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export declare function exitCodeForConstantAuditResult(result: ConstantAuditResult): number;
8
+ /**
9
+ * Machine-readable numeric-constant summary for `--json`.
10
+ *
11
+ * @since 0.11.0
12
+ */
13
+ export declare function formatConstantAuditJsonOutput(result: ConstantAuditResult, rootDir: string): string;
@@ -0,0 +1,22 @@
1
+ import { CLI_EXIT_GENERAL_ERROR, CLI_EXIT_SUCCESS } from "#core/exit-codes";
2
+ /**
3
+ * Exit `1` when any non-allowlisted numeric constant is unlabelled.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export function exitCodeForConstantAuditResult(result) {
8
+ return result.violationCount > 0 ? CLI_EXIT_GENERAL_ERROR : CLI_EXIT_SUCCESS;
9
+ }
10
+ /**
11
+ * Machine-readable numeric-constant summary for `--json`.
12
+ *
13
+ * @since 0.11.0
14
+ */
15
+ export function formatConstantAuditJsonOutput(result, rootDir) {
16
+ return JSON.stringify({
17
+ schemaVersion: 1,
18
+ ok: result.violationCount === 0,
19
+ cwd: rootDir,
20
+ result,
21
+ });
22
+ }
@@ -0,0 +1,18 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * Resolved request for a single numeric-constant audit run.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export type ConstantAuditRunRequest = {
8
+ readonly rootDir: string;
9
+ readonly targetPath: string;
10
+ readonly allowlist?: ReadonlyArray<string> | undefined;
11
+ readonly json: boolean;
12
+ };
13
+ /**
14
+ * Zod schema for {@link ConstantAuditRunRequest}.
15
+ *
16
+ * @since 0.11.0
17
+ */
18
+ export declare const constantAuditRunRequestSchema: z.ZodType<ConstantAuditRunRequest>;
@@ -0,0 +1,12 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * Zod schema for {@link ConstantAuditRunRequest}.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export const constantAuditRunRequestSchema = z.object({
8
+ rootDir: z.string().min(1),
9
+ targetPath: z.string().min(1),
10
+ allowlist: z.array(z.string()).optional(),
11
+ json: z.boolean(),
12
+ });
@@ -0,0 +1,8 @@
1
+ /** The numeric-constant convention: a `const NAME = <number>` in a source tree names the kind of number it is. */
2
+ import type { ConstantViolation } from "#audit/domain/types";
3
+ /**
4
+ * Scans one TypeScript source for numeric constants whose comment names none of the three kinds.
5
+ *
6
+ * @since 0.11.0
7
+ */
8
+ export declare function auditNumericConstants(sourceText: string): Array<ConstantViolation>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The three kinds a numeric constant may be, each named by one phrase in the comment above it.
3
+ *
4
+ * @remarks A width of the machine, a value the contract fixes, or a figure derived from the data a
5
+ * bind hands in. A count that merely looks reasonable is none of them, and the audit says so.
6
+ */
7
+ const KIND_PHRASES = [
8
+ "a constant of the machine",
9
+ "a value the contract fixes",
10
+ "derived from bind-time data",
11
+ ];
12
+ /** An upper-case `const` bound to a numeric literal, with an optional type annotation. */
13
+ const NUMERIC_CONST = /^\s*(?:export\s+)?const\s+([A-Z][A-Z0-9_]*)\s*(?::[^=]+)?=\s*(-?\d[\d_]*(?:\.\d+)?)(?![\w.])/;
14
+ /** Values that stand for absence or identity, never for a tuned size: any distinct value would do. */
15
+ const SENTINEL_VALUES = new Set(["0", "1", "-1"]);
16
+ /**
17
+ * Scans one TypeScript source for numeric constants whose comment names none of the three kinds.
18
+ *
19
+ * @since 0.11.0
20
+ */
21
+ export function auditNumericConstants(sourceText) {
22
+ const lines = sourceText.split("\n");
23
+ const violations = [];
24
+ for (let index = 0; index < lines.length; index += 1) {
25
+ const match = NUMERIC_CONST.exec(lines[index]);
26
+ if (match === null || SENTINEL_VALUES.has(match[2])) {
27
+ continue;
28
+ }
29
+ const comment = commentAbove(lines, index);
30
+ if (KIND_PHRASES.some((phrase) => comment.includes(phrase))) {
31
+ continue;
32
+ }
33
+ violations.push({
34
+ line: index + 1,
35
+ raw: `${match[1]} = ${match[2]}`,
36
+ reason: `no kind named above it — say which it is: ${KIND_PHRASES.join(" · ")}`,
37
+ });
38
+ }
39
+ return violations;
40
+ }
41
+ /** The doc block or run of `//` lines directly above a line, as one text; empty when there is none. */
42
+ function commentAbove(lines, index) {
43
+ let cursor = index - 1;
44
+ const gathered = [];
45
+ while (cursor >= 0) {
46
+ const line = lines[cursor].trim();
47
+ if (line.startsWith("//")) {
48
+ gathered.unshift(line);
49
+ cursor -= 1;
50
+ continue;
51
+ }
52
+ if (line.endsWith("*/")) {
53
+ // Walk back to the opening of the block, collecting it whole.
54
+ while (cursor >= 0) {
55
+ gathered.unshift(lines[cursor].trim());
56
+ if (lines[cursor].trim().startsWith("/*")) {
57
+ break;
58
+ }
59
+ cursor -= 1;
60
+ }
61
+ break;
62
+ }
63
+ break;
64
+ }
65
+ return gathered.join(" ");
66
+ }
@@ -0,0 +1,7 @@
1
+ import type { ConstantAuditResult } from "#audit/domain/types";
2
+ /**
3
+ * Human-readable numeric-constant report.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export declare function presentConstantAuditResult(result: ConstantAuditResult): void;
@@ -0,0 +1,21 @@
1
+ import { logger } from "#core/logger";
2
+ /**
3
+ * Human-readable numeric-constant report.
4
+ *
5
+ * @since 0.11.0
6
+ */
7
+ export function presentConstantAuditResult(result) {
8
+ for (const file of result.files) {
9
+ logger.out(`\n${file.relativePath}`);
10
+ for (const { line, raw, reason } of file.violations) {
11
+ logger.out(` ${line}: ${raw} → ${reason}`);
12
+ }
13
+ }
14
+ const allowlistSuffix = result.allowlistedCount > 0 ? ` (${result.allowlistedCount} allowlisted)` : "";
15
+ if (result.violationCount > 0) {
16
+ logger.out(`\n✖ ${result.violationCount} numeric constant(s) with no kind named${allowlistSuffix}`);
17
+ }
18
+ else {
19
+ logger.out(`✓ Every numeric constant names its kind across ${result.scannedFileCount} source file(s)${allowlistSuffix}`);
20
+ }
21
+ }
@@ -0,0 +1,16 @@
1
+ import type { AuditCommandPrelude } from "#audit/prepare";
2
+ import { AppError } from "#core/errors";
3
+ import type { Filesystem } from "#core/filesystem/filesystem";
4
+ import type { Result } from "#core/result";
5
+ /**
6
+ * Loads config and resolves the scan target for `audit constants`.
7
+ *
8
+ * @remarks The target is the library tree the convention holds over, from `audit.constants.target`
9
+ * unless the command names one.
10
+ *
11
+ * @since 0.11.0
12
+ */
13
+ export declare function prepareConstantAudit(fs: Filesystem, args: {
14
+ readonly currentWorkingDirectory: string;
15
+ readonly rawTarget: string | undefined;
16
+ }): Promise<Result<AuditCommandPrelude, AppError>>;
@@ -0,0 +1,37 @@
1
+ import { resolveRepoRelativePath } from "#audit/prepare";
2
+ import { loadCodefastConfig } from "#core/config";
3
+ import { AppError, messageFrom } from "#core/errors";
4
+ import { err, ok } from "#core/result";
5
+ import { resolveProjectRoot } from "#core/workspace/resolver";
6
+ /**
7
+ * Loads config and resolves the scan target for `audit constants`.
8
+ *
9
+ * @remarks The target is the library tree the convention holds over, from `audit.constants.target`
10
+ * unless the command names one.
11
+ *
12
+ * @since 0.11.0
13
+ */
14
+ export async function prepareConstantAudit(fs, args) {
15
+ let rootDir;
16
+ try {
17
+ rootDir = fs.canonicalPathSync(resolveProjectRoot(args.currentWorkingDirectory, fs).rootDir);
18
+ }
19
+ catch (caughtError) {
20
+ return err(new AppError("INFRA_FAILURE", messageFrom(caughtError), caughtError));
21
+ }
22
+ const loadedOutcome = await loadCodefastConfig(rootDir, fs);
23
+ if (!loadedOutcome.ok) {
24
+ return loadedOutcome;
25
+ }
26
+ const { config } = loadedOutcome.value;
27
+ const constantsConfig = config.audit?.constants ?? {};
28
+ const resolvedTargetInput = args.rawTarget ?? constantsConfig.target;
29
+ if (resolvedTargetInput === undefined) {
30
+ return err(new AppError("VALIDATION_ERROR", "audit constants needs a target: pass one, or set audit.constants.target in codefast.config"));
31
+ }
32
+ const targetPath = resolveRepoRelativePath(args.rawTarget !== undefined ? args.currentWorkingDirectory : rootDir, resolvedTargetInput);
33
+ if (!fs.existsSync(targetPath)) {
34
+ return err(new AppError("NOT_FOUND", `Not found: ${targetPath}`));
35
+ }
36
+ return ok({ rootDir, targetPath: fs.canonicalPathSync(targetPath), allowlist: constantsConfig.allowlist ?? [] });
37
+ }
@@ -0,0 +1,14 @@
1
+ import type { ConstantAuditResult } from "#audit/domain/types";
2
+ import { AppError } from "#core/errors";
3
+ import type { Filesystem } from "#core/filesystem/filesystem";
4
+ import type { Result } from "#core/result";
5
+ /**
6
+ * Scans a target path's library sources for numeric constants that name none of the three kinds.
7
+ *
8
+ * @since 0.11.0
9
+ */
10
+ export declare function runConstantAudit(fs: Filesystem, args: {
11
+ readonly rootDir: string;
12
+ readonly targetPath: string;
13
+ readonly allowlist: ReadonlyArray<string>;
14
+ }): Result<ConstantAuditResult, AppError>;
@@ -0,0 +1,64 @@
1
+ import path from "node:path";
2
+ import { auditNumericConstants } from "#audit/constants/domain/constants";
3
+ import { AppError, messageFrom } from "#core/errors";
4
+ import { err, ok } from "#core/result";
5
+ import { walkTsxFiles } from "#core/workspace/typescript-walk";
6
+ /**
7
+ * Trees the convention does not reach: a test fixes a value to observe it, a benchmark sizes a
8
+ * workload, and an app or example is a consumer, not the library.
9
+ */
10
+ const SKIPPED_SEGMENTS = new Set([
11
+ "tests",
12
+ "benchmarks",
13
+ "apps",
14
+ "examples",
15
+ "node_modules",
16
+ "dist",
17
+ ]);
18
+ /**
19
+ * Scans a target path's library sources for numeric constants that name none of the three kinds.
20
+ *
21
+ * @since 0.11.0
22
+ */
23
+ export function runConstantAudit(fs, args) {
24
+ try {
25
+ const allowlist = new Set(args.allowlist);
26
+ const { rootDir, targetPath } = args;
27
+ const filesToScan = collectScanPaths(fs, rootDir, targetPath);
28
+ const files = [];
29
+ let violationCount = 0;
30
+ let allowlistedCount = 0;
31
+ for (const absolutePath of filesToScan) {
32
+ const relativePath = toPosixPath(path.relative(rootDir, absolutePath));
33
+ const content = fs.readFileSync(absolutePath, "utf8");
34
+ const remaining = auditNumericConstants(content).filter(({ raw }) => {
35
+ const name = raw.split(" ")[0];
36
+ const isAllowed = allowlist.has(name) || allowlist.has(`${relativePath}:${name}`);
37
+ if (isAllowed) {
38
+ allowlistedCount++;
39
+ }
40
+ return !isAllowed;
41
+ });
42
+ if (remaining.length === 0) {
43
+ continue;
44
+ }
45
+ violationCount += remaining.length;
46
+ files.push({ relativePath, violations: remaining });
47
+ }
48
+ return ok({ files, violationCount, allowlistedCount, scannedFileCount: filesToScan.length });
49
+ }
50
+ catch (error) {
51
+ return err(new AppError("INFRA_FAILURE", messageFrom(error), error));
52
+ }
53
+ }
54
+ /** The `.ts` files under a `src` directory of a library package, below the target. */
55
+ function collectScanPaths(fs, rootDir, targetPath) {
56
+ const candidates = fs.statSync(targetPath).isDirectory() ? walkTsxFiles(targetPath, fs) : [targetPath];
57
+ return candidates.filter((absolutePath) => {
58
+ const segments = toPosixPath(path.relative(rootDir, absolutePath)).split("/");
59
+ return segments.includes("src") && !segments.some((segment) => SKIPPED_SEGMENTS.has(segment));
60
+ });
61
+ }
62
+ function toPosixPath(filePath) {
63
+ return filePath.split(path.sep).join("/");
64
+ }
@@ -1,3 +1,4 @@
1
+ import { lineOfOffset } from "#core/source-position";
1
2
  /** The owner: a kebab-case package, app or feature slug, or a scoped package name. */
2
3
  const NAMESPACE = /^(?:@[a-z0-9-]+\/)?[a-z0-9]+(?:-[a-z0-9]+)*$/;
3
4
  /** A token or module stands for a type or a unit of composition. */
@@ -59,13 +60,4 @@ function reasonFor(kind, name) {
59
60
  return PASCAL_CASE.test(local)
60
61
  ? null
61
62
  : `${label} '${local}' in '${name}' is not PascalCase — a ${kind} stands for a ${kind === "token" ? "type" : "unit of composition"}`;
62
- }
63
- function lineOfOffset(sourceText, offset) {
64
- let line = 1;
65
- for (let index = 0; index < offset; index++) {
66
- if (sourceText.charCodeAt(index) === 10) {
67
- line++;
68
- }
69
- }
70
- return line;
71
63
  }
@@ -73,6 +73,37 @@ export type ImportsAuditResult = {
73
73
  readonly allowlistedCount: number;
74
74
  readonly scannedFileCount: number;
75
75
  };
76
+ /**
77
+ * A double assertion through `unknown` or `any`, or a directive that keeps one without cause.
78
+ *
79
+ * @since 0.13.0
80
+ */
81
+ export type AssertionViolation = {
82
+ readonly line: number;
83
+ /** The assertion or the directive as written, up to its first line break. */
84
+ readonly raw: string;
85
+ readonly reason: string;
86
+ };
87
+ /**
88
+ * The type-assertion violations found in one file.
89
+ *
90
+ * @since 0.13.0
91
+ */
92
+ export type AssertionFileViolations = {
93
+ readonly relativePath: string;
94
+ readonly violations: Array<AssertionViolation>;
95
+ };
96
+ /**
97
+ * Outcome of one `audit assertions` run.
98
+ *
99
+ * @since 0.13.0
100
+ */
101
+ export type AssertionAuditResult = {
102
+ readonly files: Array<AssertionFileViolations>;
103
+ readonly violationCount: number;
104
+ readonly allowlistedCount: number;
105
+ readonly scannedFileCount: number;
106
+ };
76
107
  /**
77
108
  * A `token()`, `tag()` or module display name that breaks the display-name convention.
78
109
  *
@@ -104,6 +135,37 @@ export type DisplayNameAuditResult = {
104
135
  readonly allowlistedCount: number;
105
136
  readonly scannedFileCount: number;
106
137
  };
138
+ /**
139
+ * A numeric constant whose comment names none of the three kinds.
140
+ *
141
+ * @since 0.11.0
142
+ */
143
+ export type ConstantViolation = {
144
+ readonly line: number;
145
+ /** The declaration's name and value, as `NAME = 32`. */
146
+ readonly raw: string;
147
+ readonly reason: string;
148
+ };
149
+ /**
150
+ * The numeric-constant violations found in one file.
151
+ *
152
+ * @since 0.11.0
153
+ */
154
+ export type ConstantFileViolations = {
155
+ readonly relativePath: string;
156
+ readonly violations: Array<ConstantViolation>;
157
+ };
158
+ /**
159
+ * Outcome of one `audit constants` run.
160
+ *
161
+ * @since 0.11.0
162
+ */
163
+ export type ConstantAuditResult = {
164
+ readonly files: Array<ConstantFileViolations>;
165
+ readonly violationCount: number;
166
+ readonly allowlistedCount: number;
167
+ readonly scannedFileCount: number;
168
+ };
107
169
  /**
108
170
  * A broken link or anchor found by the link audit.
109
171
  *
@@ -200,6 +262,28 @@ export type UnshippedTargetViolation = {
200
262
  readonly subpath: string;
201
263
  readonly target: string;
202
264
  };
265
+ /**
266
+ * A path a stylesheet registers with Tailwind's `@source`, as written.
267
+ *
268
+ * @since 0.13.0
269
+ */
270
+ export type StylesheetSource = {
271
+ readonly line: number;
272
+ readonly pattern: string;
273
+ };
274
+ /**
275
+ * A shipped stylesheet whose `@source` paths reach no file its package's tarball ships.
276
+ *
277
+ * @since 0.13.0
278
+ */
279
+ export type UnreachableStylesheetViolation = {
280
+ readonly packageName: string;
281
+ /** Repo-relative path of the stylesheet. */
282
+ readonly stylesheet: string;
283
+ readonly sources: Array<StylesheetSource>;
284
+ /** The `files` entries missing on disk, which is how an unbuilt `dist` shows up. */
285
+ readonly missingFilesEntries: Array<string>;
286
+ };
203
287
  /**
204
288
  * Outcome of one `audit publish` run.
205
289
  *
@@ -208,6 +292,7 @@ export type UnshippedTargetViolation = {
208
292
  export type PublishAuditResult = {
209
293
  readonly legacyImportFiles: Array<LegacySubpathFile>;
210
294
  readonly unshipped: Array<UnshippedTargetViolation>;
295
+ readonly unreachableStylesheets: Array<UnreachableStylesheetViolation>;
211
296
  readonly legacyImportCount: number;
212
297
  readonly scannedFileCount: number;
213
298
  readonly packageCount: number;
@@ -1,4 +1,6 @@
1
1
  import { parseSync } from "oxc-parser";
2
+ import { isOxcNode, programStatements } from "#core/oxc-node";
3
+ import { firstLineOf, lineOfOffset } from "#core/source-position";
2
4
  /**
3
5
  * The import policies enforced across the monorepo: React members by name (never a namespace,
4
6
  * default, or implicit `React.*` UMD global), and Zod as a namespace repo-wide — the house form,
@@ -20,9 +22,6 @@ export const defaultImportPolicyRules = [
20
22
  "locale set into any bundle that reaches it)",
21
23
  },
22
24
  ];
23
- function isOxcNode(value) {
24
- return typeof value === "object" && value !== null && typeof value.type === "string";
25
- }
26
25
  function isIdentifierNamed(node, name) {
27
26
  return isOxcNode(node) && node.type === "Identifier" && node.name === name;
28
27
  }
@@ -41,7 +40,7 @@ function importedName(specifier) {
41
40
  */
42
41
  export function auditImportPolicySource(filePath, sourceText, rules) {
43
42
  const { program } = parseSync(filePath, sourceText);
44
- const statements = program.body;
43
+ const statements = programStatements(program);
45
44
  const violations = [];
46
45
  const boundUmdNames = new Set();
47
46
  for (const rule of rules) {
@@ -89,7 +88,7 @@ export function auditImportPolicySource(filePath, sourceText, rules) {
89
88
  }
90
89
  }
91
90
  for (const rule of rules) {
92
- if (rule.umdGlobal !== undefined && !boundUmdNames.has(rule.umdGlobal)) {
91
+ if (rule.umdGlobal !== undefined && !boundUmdNames.has(rule.umdGlobal) && isOxcNode(program)) {
93
92
  collectUmdGlobalReferences(program, sourceText, rule, violations);
94
93
  }
95
94
  }
@@ -124,17 +123,4 @@ function collectUmdGlobalReferences(node, sourceText, rule, violations) {
124
123
  collectUmdGlobalReferences(value, sourceText, rule, violations);
125
124
  }
126
125
  }
127
- }
128
- function lineOfOffset(sourceText, offset) {
129
- let line = 1;
130
- for (let index = 0; index < offset; index++) {
131
- if (sourceText.charCodeAt(index) === 10) {
132
- line++;
133
- }
134
- }
135
- return line;
136
- }
137
- function firstLineOf(text) {
138
- const newlineIndex = text.indexOf("\n");
139
- return newlineIndex === -1 ? text : text.slice(0, newlineIndex);
140
126
  }