@codefast/cli 0.8.1 → 0.10.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.
- package/CHANGELOG.md +596 -0
- package/LICENSE +1 -1
- package/README.md +335 -129
- package/dist/arrange/analyze.d.ts +10 -0
- package/dist/arrange/cli-schema.d.ts +50 -0
- package/dist/arrange/command.d.ts +7 -0
- package/dist/arrange/domain/analyze-service.d.ts +18 -0
- package/dist/arrange/domain/ast/ast-node.d.ts +394 -0
- package/dist/arrange/domain/ast/collectors-cn.d.ts +26 -0
- package/dist/arrange/domain/ast/collectors-jsx.d.ts +8 -0
- package/dist/arrange/domain/ast/collectors-tv.d.ts +34 -0
- package/dist/arrange/domain/ast/helpers.d.ts +36 -0
- package/dist/arrange/domain/ast/helpers.js +1 -0
- package/dist/arrange/domain/ast/simplify-targets.d.ts +22 -0
- package/dist/arrange/domain/ast/targets.d.ts +20 -0
- package/dist/arrange/domain/constants.d.ts +111 -0
- package/dist/arrange/domain/grouping-service.d.ts +100 -0
- package/dist/arrange/domain/grouping.d.ts +21 -0
- package/dist/arrange/domain/imports.d.ts +14 -0
- package/dist/arrange/domain/source-text-formatters.d.ts +33 -0
- package/dist/arrange/domain/tailwind-token.d.ts +24 -0
- package/dist/arrange/domain/token-classifier.d.ts +47 -0
- package/dist/arrange/domain/types.d.ts +208 -0
- package/dist/arrange/output.d.ts +26 -0
- package/dist/arrange/process-file.d.ts +11 -0
- package/dist/arrange/resolve-target.d.ts +10 -0
- package/dist/arrange/resolve-target.js +3 -16
- package/dist/arrange/scan-target.d.ts +7 -0
- package/dist/arrange/simplify-process-file.d.ts +11 -0
- package/dist/arrange/simplify-sync.d.ts +13 -0
- package/dist/arrange/source-parse.d.ts +7 -0
- package/dist/arrange/suggest.d.ts +8 -0
- package/dist/arrange/sync.d.ts +11 -0
- package/dist/arrange/typescript-ast-translator.d.ts +31 -0
- package/dist/arrange/workspace.d.ts +13 -0
- package/dist/arrange/workspace.js +2 -2
- package/dist/audit/cli-schema.d.ts +93 -0
- package/dist/audit/cli-schema.js +14 -3
- package/dist/audit/command.d.ts +8 -0
- package/dist/audit/command.js +52 -12
- package/dist/audit/domain/audit-file.d.ts +7 -0
- package/dist/audit/domain/comment-content.d.ts +26 -0
- package/dist/audit/domain/comment-dividers.d.ts +62 -0
- package/dist/audit/domain/comment-dividers.js +48 -20
- package/dist/audit/domain/display-names.d.ts +11 -0
- package/dist/audit/domain/display-names.js +71 -0
- package/dist/audit/domain/import-policy.d.ts +34 -0
- package/dist/audit/domain/import-policy.js +147 -0
- package/dist/audit/domain/link-references.d.ts +40 -0
- package/dist/audit/domain/mappings.d.ts +45 -0
- package/dist/audit/domain/markdown-links.d.ts +44 -0
- package/dist/audit/domain/since-versions.d.ts +26 -0
- package/dist/audit/domain/tokenize.d.ts +14 -0
- package/dist/audit/domain/tsdoc-syntax.d.ts +20 -0
- package/dist/audit/domain/types.d.ts +171 -0
- package/dist/audit/output.d.ts +91 -0
- package/dist/audit/output.js +52 -11
- package/dist/audit/prepare.d.ts +70 -0
- package/dist/audit/prepare.js +41 -10
- package/dist/audit/run-comments.d.ts +17 -0
- package/dist/audit/run-comments.js +22 -18
- package/dist/audit/run-display-names.d.ts +14 -0
- package/dist/audit/run-display-names.js +60 -0
- package/dist/audit/run-imports.d.ts +14 -0
- package/dist/audit/{run-react.js → run-imports.js} +15 -5
- package/dist/audit/run-links.d.ts +14 -0
- package/dist/audit/run.d.ts +14 -0
- package/dist/bin.d.ts +2 -0
- package/dist/cli.d.ts +6 -0
- package/dist/core/cli/format-error.d.ts +7 -0
- package/dist/core/cli/global-options.d.ts +15 -0
- package/dist/core/cli/positional.d.ts +6 -0
- package/dist/core/cli/result-handle.d.ts +19 -0
- package/dist/core/config/define-config.d.ts +7 -0
- package/dist/core/config/define-config.js +8 -0
- package/dist/core/config/loader.d.ts +18 -0
- package/dist/core/config/loader.js +2 -7
- package/dist/core/config/schema.d.ts +99 -0
- package/dist/core/config/schema.js +7 -75
- package/dist/core/config/warnings.d.ts +6 -0
- package/dist/core/config.d.ts +12 -0
- package/dist/core/errors.d.ts +25 -0
- package/dist/core/exit-codes.d.ts +18 -0
- package/dist/core/filesystem/node.d.ts +7 -0
- package/dist/core/filesystem/node.js +1 -0
- package/dist/core/filesystem/port.d.ts +44 -0
- package/dist/core/glob.d.ts +19 -0
- package/dist/core/logger.d.ts +9 -0
- package/dist/core/result.d.ts +30 -0
- package/dist/core/schema-parse.d.ts +9 -0
- package/dist/core/source-text-edit.d.ts +33 -0
- package/dist/core/verbose-diagnostics.d.ts +6 -0
- package/dist/core/workspace/ancestor-directories.d.ts +12 -0
- package/dist/core/workspace/ancestor-directories.js +30 -0
- package/dist/core/workspace/markdown-walk.d.ts +7 -0
- package/dist/core/workspace/markdown-walk.js +2 -20
- package/dist/core/workspace/package-version.d.ts +9 -0
- package/dist/core/workspace/package-version.js +8 -12
- package/dist/core/workspace/resolver.d.ts +39 -0
- package/dist/core/workspace/resolver.js +58 -75
- package/dist/core/workspace/skip-directories.d.ts +6 -0
- package/dist/core/workspace/source-walk.d.ts +16 -0
- package/dist/core/workspace/source-walk.js +14 -20
- package/dist/core/workspace/typescript-walk.d.ts +7 -0
- package/dist/core/workspace/typescript-walk.js +2 -23
- package/dist/core/workspace/walk-files.d.ts +7 -0
- package/dist/core/workspace/walk-files.js +27 -0
- package/dist/core/workspace/well-known-files.d.ts +18 -0
- package/dist/core/workspace/well-known-files.js +18 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/mirror/cli-result.d.ts +13 -0
- package/dist/mirror/cli-schema.d.ts +8 -0
- package/dist/mirror/command.d.ts +7 -0
- package/dist/mirror/dist-filesystem-impl.d.ts +8 -0
- package/dist/mirror/domain/constants.d.ts +18 -0
- package/dist/mirror/domain/constants.js +0 -12
- package/dist/mirror/domain/dirent-guard.d.ts +10 -0
- package/dist/mirror/domain/dist-filesystem.d.ts +9 -0
- package/dist/mirror/domain/errors.d.ts +24 -0
- package/dist/mirror/domain/exports.d.ts +36 -0
- package/dist/mirror/domain/package-display-name.d.ts +8 -0
- package/dist/mirror/domain/path-normalizer.d.ts +6 -0
- package/dist/mirror/domain/types.d.ts +131 -0
- package/dist/mirror/output.d.ts +23 -0
- package/dist/mirror/package-path.d.ts +19 -0
- package/dist/mirror/prepare.d.ts +15 -0
- package/dist/mirror/prepare.js +2 -2
- package/dist/mirror/supplement-exports.d.ts +27 -0
- package/dist/mirror/supplement-exports.js +2 -2
- package/dist/mirror/sync-reporter.d.ts +60 -0
- package/dist/mirror/sync-reporter.js +4 -0
- package/dist/mirror/sync-types.d.ts +43 -0
- package/dist/mirror/sync-workspace-package.d.ts +9 -0
- package/dist/mirror/sync-workspace-package.js +3 -3
- package/dist/mirror/sync.d.ts +12 -0
- package/dist/mirror/sync.js +5 -3
- package/dist/mirror/write-exports.d.ts +15 -0
- package/dist/pack-slim/cli-result.d.ts +13 -0
- package/dist/pack-slim/cli-schema.d.ts +17 -0
- package/dist/pack-slim/command.d.ts +7 -0
- package/dist/pack-slim/command.js +5 -4
- package/dist/pack-slim/domain/transform.d.ts +69 -0
- package/dist/pack-slim/domain/transform.js +141 -15
- package/dist/pack-slim/domain/types.d.ts +46 -0
- package/dist/pack-slim/output.d.ts +15 -0
- package/dist/pack-slim/output.js +10 -1
- package/dist/pack-slim/sync.d.ts +23 -0
- package/dist/pack-slim/sync.js +14 -8
- package/dist/pack-slim/working-tree.d.ts +20 -0
- package/dist/tag/cli-result.d.ts +7 -0
- package/dist/tag/cli-schema.d.ts +8 -0
- package/dist/tag/command.d.ts +7 -0
- package/dist/tag/domain/types.d.ts +111 -0
- package/dist/tag/output.d.ts +17 -0
- package/dist/tag/prepare.d.ts +13 -0
- package/dist/tag/prepare.js +2 -2
- package/dist/tag/resolve-target-path.d.ts +10 -0
- package/dist/tag/since-writer.d.ts +32 -0
- package/dist/tag/sync.d.ts +42 -0
- package/dist/tag/target-candidates.d.ts +8 -0
- package/dist/tag/target-candidates.js +1 -1
- package/dist/tag/target-runner.d.ts +8 -0
- package/dist/tag/version-resolver.d.ts +7 -0
- package/package.json +16 -33
- package/dist/audit/domain/react-imports.js +0 -91
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DomainSourceFile } from "#/arrange/domain/ast/ast-node";
|
|
2
|
+
/**
|
|
3
|
+
* Parses source text into a domain source file via the shared `oxc-parser` translator.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.3.16-canary.0
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseDomainSourceFile(filePath: string, sourceText: string): DomainSourceFile;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ArrangeSuggestGroupsRequest } from "#/arrange/cli-schema";
|
|
2
|
+
import type { ArrangeSuggestGroupsOutput } from "#/arrange/domain/types";
|
|
3
|
+
/**
|
|
4
|
+
* Formats the suggested grouping for an inline class string as `arrange group` output lines.
|
|
5
|
+
*
|
|
6
|
+
* @since 0.3.16-canary.0
|
|
7
|
+
*/
|
|
8
|
+
export declare function suggestCnGroupsFromCli(request: ArrangeSuggestGroupsRequest): ArrangeSuggestGroupsOutput;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ArrangeSyncRunRequest } from "#/arrange/cli-schema";
|
|
2
|
+
import type { ArrangeRunResult } from "#/arrange/domain/types";
|
|
3
|
+
import type { AppError } from "#/core/errors";
|
|
4
|
+
import type { FilesystemPort } from "#/core/filesystem/port";
|
|
5
|
+
import type { Result } from "#/core/result";
|
|
6
|
+
/**
|
|
7
|
+
* Runs the grouping pipeline over every target file and returns the aggregated result.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.3.16-canary.0
|
|
10
|
+
*/
|
|
11
|
+
export declare function runArrangeSync(fs: FilesystemPort, request: ArrangeSyncRunRequest): Promise<Result<ArrangeRunResult, AppError>>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-corruption layer: maps the oxc-parser ESTree AST to the arrange domain AST.
|
|
3
|
+
* This is the only place that parses TypeScript source for arrange AST translation.
|
|
4
|
+
*/
|
|
5
|
+
import type { DomainSourceFile } from "#/arrange/domain/ast/ast-node";
|
|
6
|
+
/**
|
|
7
|
+
* The translator mapping the `oxc-parser` ESTree AST to the arrange domain AST.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.3.16-canary.0
|
|
10
|
+
*/
|
|
11
|
+
export declare class TypeScriptAstTranslator {
|
|
12
|
+
translateSourceFile(filePath: string, sourceText: string): DomainSourceFile;
|
|
13
|
+
/**
|
|
14
|
+
* Own child nodes in source-declaration order, mirroring `ts.forEachChild`:
|
|
15
|
+
* recurses into nested node objects and arrays of nodes, skipping primitives
|
|
16
|
+
* and plain records (e.g. a template element's `value`) that carry no `type`.
|
|
17
|
+
*/
|
|
18
|
+
private childNodesOf;
|
|
19
|
+
private translateUnknown;
|
|
20
|
+
private stringLiteralText;
|
|
21
|
+
private noSubstitutionTemplateText;
|
|
22
|
+
private mapBinaryOperator;
|
|
23
|
+
private translateImportDeclaration;
|
|
24
|
+
private buildImportClause;
|
|
25
|
+
private buildNamespaceImport;
|
|
26
|
+
private buildNamedImports;
|
|
27
|
+
private buildImportSpecifier;
|
|
28
|
+
private translateIdentifier;
|
|
29
|
+
private translateNode;
|
|
30
|
+
private parseDomainSourceFile;
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ArrangeTargetWorkspaceAndConfig } from "#/arrange/domain/types";
|
|
2
|
+
import { AppError } from "#/core/errors";
|
|
3
|
+
import type { FilesystemPort } from "#/core/filesystem/port";
|
|
4
|
+
import type { Result } from "#/core/result";
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the arrange target, repo root, and loaded config an arrange run needs.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.3.16-canary.0
|
|
9
|
+
*/
|
|
10
|
+
export declare function prepareArrangeWorkspace(fs: FilesystemPort, args: {
|
|
11
|
+
readonly currentWorkingDirectory: string;
|
|
12
|
+
readonly rawTarget: string | undefined;
|
|
13
|
+
}): Promise<Result<ArrangeTargetWorkspaceAndConfig, AppError>>;
|
|
@@ -3,7 +3,7 @@ import { loadCodefastConfig } from "#/core/config";
|
|
|
3
3
|
import { AppError } from "#/core/errors";
|
|
4
4
|
import { messageFrom } from "#/core/errors";
|
|
5
5
|
import { err, ok } from "#/core/result";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveProjectRoot } from "#/core/workspace/resolver";
|
|
7
7
|
/**
|
|
8
8
|
* Resolves the arrange target, repo root, and loaded config an arrange run needs.
|
|
9
9
|
*
|
|
@@ -19,7 +19,7 @@ export async function prepareArrangeWorkspace(fs, args) {
|
|
|
19
19
|
}
|
|
20
20
|
let rootDir;
|
|
21
21
|
try {
|
|
22
|
-
rootDir =
|
|
22
|
+
rootDir = resolveProjectRoot(args.currentWorkingDirectory, fs).rootDir;
|
|
23
23
|
}
|
|
24
24
|
catch (caughtError) {
|
|
25
25
|
return err(new AppError("INFRA_FAILURE", messageFrom(caughtError), caughtError));
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Resolved request for a single RTL audit run.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.5.0-canary.6
|
|
6
|
+
*/
|
|
7
|
+
export type RtlAuditRunRequest = {
|
|
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 RtlAuditRunRequest}.
|
|
15
|
+
*
|
|
16
|
+
* @since 0.5.0-canary.6
|
|
17
|
+
*/
|
|
18
|
+
export declare const rtlAuditRunRequestSchema: z.ZodType<RtlAuditRunRequest>;
|
|
19
|
+
/**
|
|
20
|
+
* Resolved request for a single link audit run.
|
|
21
|
+
*
|
|
22
|
+
* @since 0.5.0
|
|
23
|
+
*/
|
|
24
|
+
export type LinkAuditRunRequest = {
|
|
25
|
+
readonly rootDir: string;
|
|
26
|
+
readonly targetPath: string;
|
|
27
|
+
readonly allowlist?: ReadonlyArray<string> | undefined;
|
|
28
|
+
readonly json: boolean;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Zod schema for {@link LinkAuditRunRequest}.
|
|
32
|
+
*
|
|
33
|
+
* @since 0.5.0
|
|
34
|
+
*/
|
|
35
|
+
export declare const linkAuditRunRequestSchema: z.ZodType<LinkAuditRunRequest>;
|
|
36
|
+
/**
|
|
37
|
+
* Resolved request for a single comment-divider audit run.
|
|
38
|
+
*
|
|
39
|
+
* @since 0.6.0
|
|
40
|
+
*/
|
|
41
|
+
export type CommentAuditRunRequest = {
|
|
42
|
+
readonly rootDir: string;
|
|
43
|
+
readonly targetPath: string;
|
|
44
|
+
readonly allowlist?: ReadonlyArray<string> | undefined;
|
|
45
|
+
readonly fix: boolean;
|
|
46
|
+
readonly json: boolean;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Zod schema for {@link CommentAuditRunRequest}.
|
|
50
|
+
*
|
|
51
|
+
* @since 0.6.0
|
|
52
|
+
*/
|
|
53
|
+
export declare const commentAuditRunRequestSchema: z.ZodType<CommentAuditRunRequest>;
|
|
54
|
+
/**
|
|
55
|
+
* Resolved request for a single import-policy audit run.
|
|
56
|
+
*
|
|
57
|
+
* @since 0.10.0
|
|
58
|
+
*/
|
|
59
|
+
export type ImportsAuditRunRequest = {
|
|
60
|
+
readonly rootDir: string;
|
|
61
|
+
readonly targetPath: string;
|
|
62
|
+
readonly allowlist?: ReadonlyArray<string> | undefined;
|
|
63
|
+
readonly json: boolean;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Zod schema for {@link ImportsAuditRunRequest}.
|
|
67
|
+
*
|
|
68
|
+
* @since 0.10.0
|
|
69
|
+
*/
|
|
70
|
+
export declare const importsAuditRunRequestSchema: z.ZodType<ImportsAuditRunRequest>;
|
|
71
|
+
/**
|
|
72
|
+
* Resolved request for a single display-name audit run.
|
|
73
|
+
*
|
|
74
|
+
* @since 0.9.0
|
|
75
|
+
*/
|
|
76
|
+
export type DisplayNameAuditRunRequest = {
|
|
77
|
+
readonly rootDir: string;
|
|
78
|
+
readonly targetPath: string;
|
|
79
|
+
readonly allowlist?: ReadonlyArray<string> | undefined;
|
|
80
|
+
readonly json: boolean;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Zod schema for {@link DisplayNameAuditRunRequest}.
|
|
84
|
+
*
|
|
85
|
+
* @since 0.9.0
|
|
86
|
+
*/
|
|
87
|
+
export declare const displayNameAuditRunRequestSchema: z.ZodType<DisplayNameAuditRunRequest>;
|
|
88
|
+
/**
|
|
89
|
+
* Resolves a path that may be absolute or relative to `rootDir`.
|
|
90
|
+
*
|
|
91
|
+
* @since 0.5.0-canary.6
|
|
92
|
+
*/
|
|
93
|
+
export declare function resolveRepoRelativePath(rootDir: string, maybeRelative: string): string;
|
package/dist/audit/cli-schema.js
CHANGED
|
@@ -35,11 +35,22 @@ export const commentAuditRunRequestSchema = z.object({
|
|
|
35
35
|
json: z.boolean(),
|
|
36
36
|
});
|
|
37
37
|
/**
|
|
38
|
-
* Zod schema for {@link
|
|
38
|
+
* Zod schema for {@link ImportsAuditRunRequest}.
|
|
39
39
|
*
|
|
40
|
-
* @since 0.
|
|
40
|
+
* @since 0.10.0
|
|
41
41
|
*/
|
|
42
|
-
export const
|
|
42
|
+
export const importsAuditRunRequestSchema = z.object({
|
|
43
|
+
rootDir: z.string().min(1),
|
|
44
|
+
targetPath: z.string().min(1),
|
|
45
|
+
allowlist: z.array(z.string()).optional(),
|
|
46
|
+
json: z.boolean(),
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Zod schema for {@link DisplayNameAuditRunRequest}.
|
|
50
|
+
*
|
|
51
|
+
* @since 0.9.0
|
|
52
|
+
*/
|
|
53
|
+
export const displayNameAuditRunRequestSchema = z.object({
|
|
43
54
|
rootDir: z.string().min(1),
|
|
44
55
|
targetPath: z.string().min(1),
|
|
45
56
|
allowlist: z.array(z.string()).optional(),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Top-level `audit` command — the source scans. Every one of them reports by default; only
|
|
4
|
+
* `comments --fix` writes, and only where the rewrite discards nothing a person wrote.
|
|
5
|
+
*
|
|
6
|
+
* @since 0.5.0-canary.6
|
|
7
|
+
*/
|
|
8
|
+
export declare function createAuditCommand(): Command;
|
package/dist/audit/command.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import { commentAuditRunRequestSchema, linkAuditRunRequestSchema,
|
|
4
|
-
import { exitCodeForCommentAuditResult, exitCodeForLinkAuditResult,
|
|
5
|
-
import { prepareCommentAudit, prepareLinkAudit,
|
|
3
|
+
import { commentAuditRunRequestSchema, linkAuditRunRequestSchema, importsAuditRunRequestSchema, rtlAuditRunRequestSchema, displayNameAuditRunRequestSchema, } from "#/audit/cli-schema";
|
|
4
|
+
import { exitCodeForCommentAuditResult, exitCodeForLinkAuditResult, exitCodeForImportsAuditResult, exitCodeForRtlAuditResult, exitCodeForDisplayNameAuditResult, formatCommentAuditJsonOutput, formatLinkAuditJsonOutput, formatImportsAuditJsonOutput, formatRtlAuditJsonOutput, formatDisplayNameAuditJsonOutput, presentCommentAuditResult, presentLinkAuditResult, presentImportsAuditResult, presentRtlAuditResult, presentDisplayNameAuditResult, } from "#/audit/output";
|
|
5
|
+
import { prepareCommentAudit, prepareLinkAudit, prepareImportsAudit, prepareRtlAudit, prepareDisplayNameAudit, } from "#/audit/prepare";
|
|
6
6
|
import { runRtlAudit } from "#/audit/run";
|
|
7
7
|
import { runCommentAudit } from "#/audit/run-comments";
|
|
8
|
+
import { runDisplayNameAudit } from "#/audit/run-display-names";
|
|
9
|
+
import { runImportsAudit } from "#/audit/run-imports";
|
|
8
10
|
import { runLinkAudit } from "#/audit/run-links";
|
|
9
|
-
import { runReactAudit } from "#/audit/run-react";
|
|
10
11
|
import { readOptionalPositionalArg } from "#/core/cli/positional";
|
|
11
12
|
import { consumeCliAppError } from "#/core/cli/result-handle";
|
|
12
13
|
import { nodeFilesystem } from "#/core/filesystem/node";
|
|
@@ -99,12 +100,12 @@ export function createAuditCommand() {
|
|
|
99
100
|
process.exitCode = exitCodeForLinkAuditResult(outcome.value);
|
|
100
101
|
});
|
|
101
102
|
cmd
|
|
102
|
-
.command("
|
|
103
|
-
.description("Report React
|
|
103
|
+
.command("imports")
|
|
104
|
+
.description("Report banned import forms (React by-name, Zod namespace in front-end packages, …)")
|
|
104
105
|
.argument("[target]", "Directory or file to scan (default: the repo root)")
|
|
105
106
|
.option("--json", "Print one JSON summary on stdout", false)
|
|
106
107
|
.action(async (target, opts) => {
|
|
107
|
-
const prelude = await
|
|
108
|
+
const prelude = await prepareImportsAudit(nodeFilesystem, {
|
|
108
109
|
currentWorkingDirectory: process.cwd(),
|
|
109
110
|
rawTarget: readOptionalPositionalArg(target),
|
|
110
111
|
});
|
|
@@ -112,7 +113,7 @@ export function createAuditCommand() {
|
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
const { rootDir, targetPath, allowlist } = prelude.value;
|
|
115
|
-
const parsed = parseWithSchema(
|
|
116
|
+
const parsed = parseWithSchema(importsAuditRunRequestSchema, {
|
|
116
117
|
rootDir,
|
|
117
118
|
targetPath,
|
|
118
119
|
allowlist,
|
|
@@ -121,7 +122,7 @@ export function createAuditCommand() {
|
|
|
121
122
|
if (!consumeCliAppError(parsed)) {
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
|
-
const outcome =
|
|
125
|
+
const outcome = runImportsAudit(nodeFilesystem, {
|
|
125
126
|
rootDir: parsed.value.rootDir,
|
|
126
127
|
targetPath: parsed.value.targetPath,
|
|
127
128
|
allowlist: parsed.value.allowlist ?? [],
|
|
@@ -130,12 +131,51 @@ export function createAuditCommand() {
|
|
|
130
131
|
return;
|
|
131
132
|
}
|
|
132
133
|
if (parsed.value.json) {
|
|
133
|
-
logger.out(
|
|
134
|
+
logger.out(formatImportsAuditJsonOutput(outcome.value, rootDir));
|
|
134
135
|
}
|
|
135
136
|
else {
|
|
136
|
-
|
|
137
|
+
presentImportsAuditResult(outcome.value);
|
|
137
138
|
}
|
|
138
|
-
process.exitCode =
|
|
139
|
+
process.exitCode = exitCodeForImportsAuditResult(outcome.value);
|
|
140
|
+
});
|
|
141
|
+
cmd
|
|
142
|
+
.command("display-names")
|
|
143
|
+
.description("Report token(), tag() and module display names that break the <namespace>:<Name> convention")
|
|
144
|
+
.argument("[target]", "Directory or file to scan (default: the repo root)")
|
|
145
|
+
.option("--json", "Print one JSON summary on stdout", false)
|
|
146
|
+
.action(async (target, opts) => {
|
|
147
|
+
const prelude = await prepareDisplayNameAudit(nodeFilesystem, {
|
|
148
|
+
currentWorkingDirectory: process.cwd(),
|
|
149
|
+
rawTarget: readOptionalPositionalArg(target),
|
|
150
|
+
});
|
|
151
|
+
if (!consumeCliAppError(prelude)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const { rootDir, targetPath, allowlist } = prelude.value;
|
|
155
|
+
const parsed = parseWithSchema(displayNameAuditRunRequestSchema, {
|
|
156
|
+
rootDir,
|
|
157
|
+
targetPath,
|
|
158
|
+
allowlist,
|
|
159
|
+
json: !!opts.json,
|
|
160
|
+
});
|
|
161
|
+
if (!consumeCliAppError(parsed)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const outcome = runDisplayNameAudit(nodeFilesystem, {
|
|
165
|
+
rootDir: parsed.value.rootDir,
|
|
166
|
+
targetPath: parsed.value.targetPath,
|
|
167
|
+
allowlist: parsed.value.allowlist ?? [],
|
|
168
|
+
});
|
|
169
|
+
if (!consumeCliAppError(outcome)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (parsed.value.json) {
|
|
173
|
+
logger.out(formatDisplayNameAuditJsonOutput(outcome.value, rootDir));
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
presentDisplayNameAuditResult(outcome.value);
|
|
177
|
+
}
|
|
178
|
+
process.exitCode = exitCodeForDisplayNameAuditResult(outcome.value);
|
|
139
179
|
});
|
|
140
180
|
cmd
|
|
141
181
|
.command("comments")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognises comment content the repo bans outright: repo-document pointers and JSDoc type syntax.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Why a comment's content fails the convention. None is mechanical — the writer restates the
|
|
6
|
+
* invariant, lets the type carry the type, or moves the tag.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.6.0
|
|
9
|
+
*/
|
|
10
|
+
export type CommentContentDefectKind = "detached-doc" | "doc-pointer" | "jsdoc-type" | "param-coverage" | "param-hyphen" | "since-order" | "stacked-doc";
|
|
11
|
+
/**
|
|
12
|
+
* One banned fragment found inside a comment.
|
|
13
|
+
*
|
|
14
|
+
* @since 0.6.0
|
|
15
|
+
*/
|
|
16
|
+
export interface CommentContentFinding {
|
|
17
|
+
readonly line: number;
|
|
18
|
+
readonly raw: string;
|
|
19
|
+
readonly defect: CommentContentDefectKind;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Scans a source file's comments for banned content, in source order.
|
|
23
|
+
*
|
|
24
|
+
* @since 0.6.0
|
|
25
|
+
*/
|
|
26
|
+
export declare function scanCommentContent(content: string, language: "css" | "js"): Array<CommentContentFinding>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognises section dividers in source text and renders them in the one form the repo allows.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The column every divider's rule ends at — `oxfmt`'s `printWidth`, indentation included.
|
|
6
|
+
*
|
|
7
|
+
* @since 0.6.0
|
|
8
|
+
*/
|
|
9
|
+
export declare const DIVIDER_COLUMN = 120;
|
|
10
|
+
/**
|
|
11
|
+
* Which comment syntax a divider is written in.
|
|
12
|
+
*
|
|
13
|
+
* @since 0.6.0
|
|
14
|
+
*/
|
|
15
|
+
export type DividerLanguage = "css" | "ignore" | "js";
|
|
16
|
+
/**
|
|
17
|
+
* Why a divider fails the convention. Both kinds are mechanical, so every report is one `--fix` away.
|
|
18
|
+
*
|
|
19
|
+
* @since 0.6.0
|
|
20
|
+
*/
|
|
21
|
+
export type DividerDefectKind = "bad-width" | "legacy-form";
|
|
22
|
+
/**
|
|
23
|
+
* One divider found in a file, with the verdict on its form.
|
|
24
|
+
*
|
|
25
|
+
* @since 0.6.0
|
|
26
|
+
*/
|
|
27
|
+
export interface DividerRegion {
|
|
28
|
+
readonly startLine: number;
|
|
29
|
+
readonly endLine: number;
|
|
30
|
+
readonly indent: string;
|
|
31
|
+
readonly title: string;
|
|
32
|
+
readonly raw: string;
|
|
33
|
+
readonly defect: DividerDefectKind | null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Renders a divider in the canonical form.
|
|
37
|
+
*
|
|
38
|
+
* @param indent - leading whitespace copied from the site, counted toward the column
|
|
39
|
+
* @param title - the section name, already trimmed
|
|
40
|
+
* @param language - which comment syntax to draw the divider in
|
|
41
|
+
*
|
|
42
|
+
* @since 0.6.0
|
|
43
|
+
*/
|
|
44
|
+
export declare function renderDivider(indent: string, title: string, language: DividerLanguage): string;
|
|
45
|
+
/**
|
|
46
|
+
* Every section divider in a file, canonical ones included, in source order.
|
|
47
|
+
*
|
|
48
|
+
* @remarks A rule-framed comment that carries prose is a doc block, not a divider, and never
|
|
49
|
+
* appears here — the two are different things and only one of them has a fixed form.
|
|
50
|
+
*
|
|
51
|
+
* @since 0.6.0
|
|
52
|
+
*/
|
|
53
|
+
export declare function scanCommentDividers(content: string, language: DividerLanguage): Array<DividerRegion>;
|
|
54
|
+
/**
|
|
55
|
+
* Rewrites every off-convention divider into the canonical form.
|
|
56
|
+
*
|
|
57
|
+
* @since 0.6.0
|
|
58
|
+
*/
|
|
59
|
+
export declare function applyCommentDividerFixes(content: string, language: DividerLanguage): {
|
|
60
|
+
readonly content: string;
|
|
61
|
+
readonly fixedCount: number;
|
|
62
|
+
};
|
|
@@ -11,12 +11,31 @@ const RULE_GLYPH = "─";
|
|
|
11
11
|
const LEAD_GLYPHS = "──";
|
|
12
12
|
/** Every glyph a divider has historically been drawn with, so legacy forms are recognised too. */
|
|
13
13
|
const RULE_CHARACTER_CLASS = String.raw `[-=─_*~#]`;
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
// The comment lead each syntax opens a divider with — `#` for ignore files, slash forms for code.
|
|
15
|
+
const commentLeadByLanguage = {
|
|
16
|
+
css: String.raw `\/\/|\/\*|\*`,
|
|
17
|
+
ignore: String.raw `#`,
|
|
18
|
+
js: String.raw `\/\/|\/\*|\*`,
|
|
19
|
+
};
|
|
20
|
+
// One compiled pattern set per language — the comment lead is the only part that varies.
|
|
21
|
+
const patternsByLanguage = new Map();
|
|
22
|
+
function patternsFor(language) {
|
|
23
|
+
const cached = patternsByLanguage.get(language);
|
|
24
|
+
if (cached !== undefined) {
|
|
25
|
+
return cached;
|
|
26
|
+
}
|
|
27
|
+
const lead = commentLeadByLanguage[language];
|
|
28
|
+
const built = {
|
|
29
|
+
ruleOnly: new RegExp(String.raw `^(?<indent>[ \t]*)(?:${lead})[ \t]*${RULE_CHARACTER_CLASS}{4,}[ \t]*(?:\*\/)?[ \t]*$`),
|
|
30
|
+
titled: new RegExp(String.raw `^(?<indent>[ \t]*)(?:${lead})[ \t]*${RULE_CHARACTER_CLASS}{2,}[ \t]+(?<title>.*?)[ \t]+${RULE_CHARACTER_CLASS}{2,}[ \t]*(?:\*\/)?[ \t]*$`),
|
|
31
|
+
commentLine: new RegExp(String.raw `^[ \t]*(?:${lead})`),
|
|
32
|
+
bareRuleClose: new RegExp(String.raw `^[ \t]*${RULE_CHARACTER_CLASS}{4,}[ \t]*\*\/[ \t]*$`),
|
|
33
|
+
commentPrefix: new RegExp(String.raw `^[ \t]*(?:${lead})[ \t]?`),
|
|
34
|
+
};
|
|
35
|
+
patternsByLanguage.set(language, built);
|
|
36
|
+
return built;
|
|
37
|
+
}
|
|
18
38
|
const rulesOnlyPattern = new RegExp(String.raw `^(?:${RULE_CHARACTER_CLASS}|[ \t])*$`);
|
|
19
|
-
const commentPrefixPattern = /^[ \t]*(?:\/\/|\/\*|\*)[ \t]?/;
|
|
20
39
|
const commentSuffixPattern = /[ \t]*\*\/[ \t]*$/;
|
|
21
40
|
/** A banner spanning more lines than this is prose that happens to start with a rule, not a divider. */
|
|
22
41
|
const MAX_BANNER_SPAN = 16;
|
|
@@ -36,7 +55,8 @@ export function renderDivider(indent, title, language) {
|
|
|
36
55
|
const head = `${indent}/* ${LEAD_GLYPHS} ${title} `;
|
|
37
56
|
return `${head}${RULE_GLYPH.repeat(Math.max(2, DIVIDER_COLUMN - head.length - 3))} */`;
|
|
38
57
|
}
|
|
39
|
-
const
|
|
58
|
+
const prefix = language === "ignore" ? "#" : "//";
|
|
59
|
+
const head = `${indent}${prefix} ${LEAD_GLYPHS} ${title} `;
|
|
40
60
|
return `${head}${RULE_GLYPH.repeat(Math.max(2, DIVIDER_COLUMN - head.length))}`;
|
|
41
61
|
}
|
|
42
62
|
/**
|
|
@@ -80,7 +100,8 @@ export function applyCommentDividerFixes(content, language) {
|
|
|
80
100
|
}
|
|
81
101
|
function readRegionAt(lines, index, language) {
|
|
82
102
|
const line = lines[index];
|
|
83
|
-
const
|
|
103
|
+
const patterns = patternsFor(language);
|
|
104
|
+
const titled = patterns.titled.exec(line);
|
|
84
105
|
const title = titled?.groups?.title?.trim();
|
|
85
106
|
if (titled !== null && title !== undefined && title.length > 0 && !rulesOnlyPattern.test(title)) {
|
|
86
107
|
const indent = titled.groups?.indent ?? "";
|
|
@@ -94,26 +115,29 @@ function readRegionAt(lines, index, language) {
|
|
|
94
115
|
defect: line === canonical ? null : usesCanonicalGlyphs(line) ? "bad-width" : "legacy-form",
|
|
95
116
|
};
|
|
96
117
|
}
|
|
97
|
-
const ruleOnly =
|
|
118
|
+
const ruleOnly = patterns.ruleOnly.exec(line);
|
|
98
119
|
if (ruleOnly === null) {
|
|
99
120
|
return null;
|
|
100
121
|
}
|
|
101
|
-
return readBannerAt(lines, index, ruleOnly.groups?.indent ?? "");
|
|
122
|
+
return readBannerAt(lines, index, ruleOnly.groups?.indent ?? "", language);
|
|
102
123
|
}
|
|
103
|
-
function readBannerAt(lines, index, indent) {
|
|
124
|
+
function readBannerAt(lines, index, indent, language) {
|
|
104
125
|
const line = lines[index];
|
|
126
|
+
const patterns = patternsFor(language);
|
|
105
127
|
// An unterminated `/*` opens a block whose body needs no per-line marker, so the run ends at `*/`.
|
|
106
128
|
const insideBlock = line.trimStart().startsWith("/*") && !line.includes("*/");
|
|
107
129
|
const body = [];
|
|
108
130
|
let cursor = index + 1;
|
|
109
|
-
while (cursor < lines.length &&
|
|
110
|
-
|
|
131
|
+
while (cursor < lines.length &&
|
|
132
|
+
cursor - index <= MAX_BANNER_SPAN &&
|
|
133
|
+
!isBannerClose(lines[cursor], insideBlock, language)) {
|
|
134
|
+
if (!insideBlock && !patterns.commentLine.test(lines[cursor])) {
|
|
111
135
|
break;
|
|
112
136
|
}
|
|
113
|
-
body.push(stripCommentPrefix(lines[cursor]));
|
|
137
|
+
body.push(stripCommentPrefix(lines[cursor], language));
|
|
114
138
|
cursor++;
|
|
115
139
|
}
|
|
116
|
-
const closed = cursor < lines.length && cursor - index <= MAX_BANNER_SPAN && isBannerClose(lines[cursor], insideBlock);
|
|
140
|
+
const closed = cursor < lines.length && cursor - index <= MAX_BANNER_SPAN && isBannerClose(lines[cursor], insideBlock, language);
|
|
117
141
|
const meaningful = body.filter((entry) => entry.length > 0);
|
|
118
142
|
// A frame around prose is a doc block, and an unclosed rule is prose formatting inside one.
|
|
119
143
|
if (!closed || meaningful.length !== 1 || !looksLikeTitle(meaningful[0])) {
|
|
@@ -131,15 +155,19 @@ function readBannerAt(lines, index, indent) {
|
|
|
131
155
|
function looksLikeTitle(text) {
|
|
132
156
|
return text.length <= MAX_TITLE_LENGTH && !text.endsWith(".");
|
|
133
157
|
}
|
|
134
|
-
function isBannerClose(line, insideBlock) {
|
|
135
|
-
|
|
158
|
+
function isBannerClose(line, insideBlock, language) {
|
|
159
|
+
const patterns = patternsFor(language);
|
|
160
|
+
if (patterns.ruleOnly.test(line)) {
|
|
136
161
|
return true;
|
|
137
162
|
}
|
|
138
|
-
return insideBlock &&
|
|
163
|
+
return insideBlock && patterns.bareRuleClose.test(line);
|
|
139
164
|
}
|
|
140
165
|
function usesCanonicalGlyphs(line) {
|
|
141
|
-
|
|
166
|
+
const trimmed = line.trimStart();
|
|
167
|
+
return (trimmed.startsWith(`// ${LEAD_GLYPHS} `) ||
|
|
168
|
+
trimmed.startsWith(`/* ${LEAD_GLYPHS} `) ||
|
|
169
|
+
trimmed.startsWith(`# ${LEAD_GLYPHS} `));
|
|
142
170
|
}
|
|
143
|
-
function stripCommentPrefix(line) {
|
|
144
|
-
return line.replace(
|
|
171
|
+
function stripCommentPrefix(line, language) {
|
|
172
|
+
return line.replace(patternsFor(language).commentPrefix, "").replace(commentSuffixPattern, "").trim();
|
|
145
173
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The display-name convention: a name is spelled like the TS symbol it stands for, under its owner's namespace. */
|
|
2
|
+
import type { DisplayNameViolation } from "#/audit/domain/types";
|
|
3
|
+
/**
|
|
4
|
+
* Scans one source or markdown text for `token()`, `tag()` and module display names that break the convention.
|
|
5
|
+
*
|
|
6
|
+
* @remarks Runs on markdown as well as TypeScript because a doc sample is what a reader copies:
|
|
7
|
+
* a convention the docs break is not one the docs teach.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.9.0
|
|
10
|
+
*/
|
|
11
|
+
export declare function auditDisplayNames(sourceText: string): Array<DisplayNameViolation>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** The owner: a kebab-case package, app or feature slug, or a scoped package name. */
|
|
2
|
+
const NAMESPACE = /^(?:@[a-z0-9-]+\/)?[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3
|
+
/** A token or module stands for a type or a unit of composition. */
|
|
4
|
+
const PASCAL_CASE = /^[A-Z][A-Za-z0-9]*$/;
|
|
5
|
+
/** A tag key stands for an attribute a request selects on, so it reads like a property. */
|
|
6
|
+
const CAMEL_CASE = /^[a-z][A-Za-z0-9]*$/;
|
|
7
|
+
/**
|
|
8
|
+
* A bare `token(…)` / `tag(…)` call, or a module factory call, whose first argument is a string literal.
|
|
9
|
+
*
|
|
10
|
+
* @remarks Generic arguments are skipped up to four levels of nesting, which is deeper than any
|
|
11
|
+
* declaration in the repo. Template literals are not matched: an interpolated name has no fixed
|
|
12
|
+
* text to check. The closing parenthesis is kept when the name is the only argument, so the
|
|
13
|
+
* reported text reads as the call was written.
|
|
14
|
+
*/
|
|
15
|
+
const DISPLAY_NAME_CALL = /(?<![\w$.])(?:(token|tag)\s*(?:<(?:[^<>]|<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)*>)?|((?:Sync|Async)?Module)\s*\.\s*create(?:Async)?)\s*\(\s*(["'])((?:(?!\3)[^\\\n]|\\.)*)\3(\s*\))?/g;
|
|
16
|
+
const KIND_LABEL = {
|
|
17
|
+
token: "token display name",
|
|
18
|
+
tag: "tag key",
|
|
19
|
+
module: "module name",
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Scans one source or markdown text for `token()`, `tag()` and module display names that break the convention.
|
|
23
|
+
*
|
|
24
|
+
* @remarks Runs on markdown as well as TypeScript because a doc sample is what a reader copies:
|
|
25
|
+
* a convention the docs break is not one the docs teach.
|
|
26
|
+
*
|
|
27
|
+
* @since 0.9.0
|
|
28
|
+
*/
|
|
29
|
+
export function auditDisplayNames(sourceText) {
|
|
30
|
+
const violations = [];
|
|
31
|
+
for (const match of sourceText.matchAll(DISPLAY_NAME_CALL)) {
|
|
32
|
+
const kind = match[1] === undefined ? "module" : match[1];
|
|
33
|
+
const reason = reasonFor(kind, match[4]);
|
|
34
|
+
if (reason === null) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
violations.push({ line: lineOfOffset(sourceText, match.index), raw: match[0], reason });
|
|
38
|
+
}
|
|
39
|
+
return violations;
|
|
40
|
+
}
|
|
41
|
+
/** Why a display name breaks the convention, or `null` when it holds. */
|
|
42
|
+
function reasonFor(kind, name) {
|
|
43
|
+
const segments = name.split(":");
|
|
44
|
+
const label = KIND_LABEL[kind];
|
|
45
|
+
if (segments.length < 2) {
|
|
46
|
+
return `${label} '${name}' has no namespace — write '<namespace>:${name}'`;
|
|
47
|
+
}
|
|
48
|
+
const local = segments.at(-1);
|
|
49
|
+
for (const namespace of segments.slice(0, -1)) {
|
|
50
|
+
if (!NAMESPACE.test(namespace)) {
|
|
51
|
+
return `namespace '${namespace}' in '${name}' is not kebab-case (or a scoped package name)`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (kind === "tag") {
|
|
55
|
+
return CAMEL_CASE.test(local)
|
|
56
|
+
? null
|
|
57
|
+
: `${label} '${local}' in '${name}' is not camelCase — a tag key names an attribute`;
|
|
58
|
+
}
|
|
59
|
+
return PASCAL_CASE.test(local)
|
|
60
|
+
? null
|
|
61
|
+
: `${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
|
+
}
|