@trackunit/repo-checks 0.0.0-stage → 0.0.2
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 +3 -0
- package/LICENSE.txt +191 -0
- package/README.md +319 -2
- package/bin/index.d.ts +2 -0
- package/bin/index.js +34 -0
- package/migrations/entry.js +3 -0
- package/migrations.json +3 -0
- package/package.json +34 -3
- package/src/cli.d.ts +27 -0
- package/src/cli.js +109 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +21 -0
- package/src/lib/config-error.d.ts +8 -0
- package/src/lib/config-error.js +16 -0
- package/src/lib/config.d.ts +22 -0
- package/src/lib/config.js +61 -0
- package/src/lib/doc-references/case-fix.d.ts +21 -0
- package/src/lib/doc-references/case-fix.js +66 -0
- package/src/lib/doc-references/check-doc-references.d.ts +28 -0
- package/src/lib/doc-references/check-doc-references.js +147 -0
- package/src/lib/doc-references/classify.d.ts +49 -0
- package/src/lib/doc-references/classify.js +59 -0
- package/src/lib/doc-references/discover.d.ts +6 -0
- package/src/lib/doc-references/discover.js +84 -0
- package/src/lib/doc-references/extract.d.ts +29 -0
- package/src/lib/doc-references/extract.js +46 -0
- package/src/lib/doc-references/finding.d.ts +24 -0
- package/src/lib/doc-references/finding.js +11 -0
- package/src/lib/doc-references/fs-errors.d.ts +14 -0
- package/src/lib/doc-references/fs-errors.js +25 -0
- package/src/lib/doc-references/headings.d.ts +79 -0
- package/src/lib/doc-references/headings.js +219 -0
- package/src/lib/doc-references/parse.d.ts +72 -0
- package/src/lib/doc-references/parse.js +72 -0
- package/src/lib/doc-references/placeholders.d.ts +74 -0
- package/src/lib/doc-references/placeholders.js +176 -0
- package/src/lib/doc-references/repo-root.d.ts +18 -0
- package/src/lib/doc-references/repo-root.js +36 -0
- package/src/lib/doc-references/resolver.d.ts +96 -0
- package/src/lib/doc-references/resolver.js +430 -0
- package/src/lib/doc-references/settings.d.ts +163 -0
- package/src/lib/doc-references/settings.js +167 -0
- package/src/lib/doc-references/suppressions.d.ts +8 -0
- package/src/lib/doc-references/suppressions.js +24 -0
- package/src/lib/format.d.ts +13 -0
- package/src/lib/format.js +33 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseDocsSettings = exports.DEFAULT_DOCS_SETTINGS = exports.formatConfigIssues = exports.docReferencesConfigSchema = exports.strictObjectNamingKeys = exports.DEFAULT_BARE_FILENAMES = exports.DEFAULT_DIRECTORIES = exports.DEFAULT_CONSTRUCTS = exports.DEFAULT_IGNORE = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const config_error_1 = require("../config-error");
|
|
6
|
+
const placeholders_1 = require("./placeholders");
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for the doc-references check, validated with zod so a bad config fails with a
|
|
9
|
+
* message naming the offending key. Every default-bearing list takes `{ extend: [...] }` (add to
|
|
10
|
+
* the defaults) or `{ replace: [...] }` (swap them out, `[]` empties). A bare array is rejected
|
|
11
|
+
* because it can't say which of the two the author meant.
|
|
12
|
+
*/
|
|
13
|
+
/** Conventional generated-output locations, matched against the repo-relative path. */
|
|
14
|
+
exports.DEFAULT_IGNORE = [
|
|
15
|
+
"**/dist/**",
|
|
16
|
+
"**/build/**",
|
|
17
|
+
"**/node_modules/**",
|
|
18
|
+
"**/coverage/**",
|
|
19
|
+
"**/*.{zip,tgz,tar.gz,jar,whl}",
|
|
20
|
+
];
|
|
21
|
+
const LIST_OVERRIDE_FORMS = "{ extend: [...] } to add to the defaults or { replace: [...] } to swap them out";
|
|
22
|
+
const listOverrideSchema = (item) => zod_1.z
|
|
23
|
+
.object({
|
|
24
|
+
extend: zod_1.z.array(item).optional(),
|
|
25
|
+
replace: zod_1.z.array(item).optional(),
|
|
26
|
+
}, {
|
|
27
|
+
errorMap: (issue, ctx) => issue.code === "invalid_type"
|
|
28
|
+
? {
|
|
29
|
+
message: `expected ${LIST_OVERRIDE_FORMS}${Array.isArray(ctx.data) ? " (a bare array is ambiguous)" : ""}`,
|
|
30
|
+
}
|
|
31
|
+
: { message: ctx.defaultError },
|
|
32
|
+
})
|
|
33
|
+
.strict()
|
|
34
|
+
.refine(value => (value.extend === undefined) !== (value.replace === undefined), {
|
|
35
|
+
message: `expected exactly one of extend or replace: ${LIST_OVERRIDE_FORMS}`,
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* A named convention, or a custom regex whose single capture group is the placeholder name. A regex
|
|
39
|
+
* that matches plain words, such as `(\w+)`, turns every segment into a wildcard and so disables
|
|
40
|
+
* the check.
|
|
41
|
+
*/
|
|
42
|
+
const placeholderConventionSchema = zod_1.z.string().superRefine((convention, ctx) => {
|
|
43
|
+
if (placeholders_1.NAMED_PLACEHOLDERS.some(named => named === convention))
|
|
44
|
+
return;
|
|
45
|
+
const groups = (0, placeholders_1.countCaptureGroups)(convention);
|
|
46
|
+
if (groups !== 1) {
|
|
47
|
+
ctx.addIssue({
|
|
48
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
49
|
+
message: `"${convention}" must be one of ${placeholders_1.NAMED_PLACEHOLDERS.join(", ")}, or a regex with exactly one capture group (${groups === undefined ? "it does not compile" : `found ${groups}`})`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* Links, images, and definitions are checked; inline code only on opt-in, since measured on manager
|
|
55
|
+
* about 96% of inline-code findings were false positives, mostly paths relative to an implied
|
|
56
|
+
* project root. A `constructs` setting overrides only the keys it names.
|
|
57
|
+
*/
|
|
58
|
+
exports.DEFAULT_CONSTRUCTS = { links: true, images: true, definitions: true, inlineCode: false };
|
|
59
|
+
/** Provisional: whether extensionless references with a `/` (`libs/react/components/`) are checked. */
|
|
60
|
+
exports.DEFAULT_DIRECTORIES = false;
|
|
61
|
+
/**
|
|
62
|
+
* Provisional: whether inline code without a `/` (`` `README.md` ``) is checked. Link, image, and
|
|
63
|
+
* definition URLs without a `/` (`[x](README.md)`) are always checked; see `classify.ts`.
|
|
64
|
+
*/
|
|
65
|
+
exports.DEFAULT_BARE_FILENAMES = false;
|
|
66
|
+
/** `'a', 'b'` for the keys of an `unrecognized_keys` issue. */
|
|
67
|
+
const quoteKeys = (keys) => keys.map(key => `'${key}'`).join(", ");
|
|
68
|
+
/** `a, b, or c`. */
|
|
69
|
+
const orList = (items) => items.length > 1 ? `${items.slice(0, -1).join(", ")}, or ${items.at(-1)}` : items.join("");
|
|
70
|
+
/**
|
|
71
|
+
* A strict object whose unknown keys are reported as `unknown <noun> 'key'; expected <valid keys>`,
|
|
72
|
+
* and whose wrong type as `expected <expected>`.
|
|
73
|
+
*/
|
|
74
|
+
const strictObjectNamingKeys = (shape, { noun, expected }) => zod_1.z
|
|
75
|
+
.object(shape, {
|
|
76
|
+
errorMap: (issue, ctx) => {
|
|
77
|
+
if (issue.code === "unrecognized_keys") {
|
|
78
|
+
return { message: `unknown ${noun} ${quoteKeys(issue.keys)}; expected ${orList(Object.keys(shape))}` };
|
|
79
|
+
}
|
|
80
|
+
return issue.code === "invalid_type" ? { message: `expected ${expected}` } : { message: ctx.defaultError };
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
.strict();
|
|
84
|
+
exports.strictObjectNamingKeys = strictObjectNamingKeys;
|
|
85
|
+
/** A repo-relative glob: a leading `/` or `./` or a trailing `/` would silently match no document. */
|
|
86
|
+
const excludeGlobSchema = zod_1.z.string().superRefine((glob, ctx) => {
|
|
87
|
+
if (glob.startsWith("/") || glob.startsWith("./")) {
|
|
88
|
+
ctx.addIssue({
|
|
89
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
90
|
+
message: `"${glob}" must not start with / or ./; globs are repo-relative, e.g. docs/sessions/**`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else if (glob.endsWith("/")) {
|
|
94
|
+
ctx.addIssue({
|
|
95
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
96
|
+
message: `"${glob}" ends with /, which matches no document; use "${glob}**"`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
/** The `docReferences` section of `repo-checks.config.json`, as written. */
|
|
101
|
+
exports.docReferencesConfigSchema = (0, exports.strictObjectNamingKeys)({
|
|
102
|
+
rootDir: zod_1.z.string().min(1).optional(),
|
|
103
|
+
ignore: listOverrideSchema(zod_1.z.string()).optional(),
|
|
104
|
+
exclude: zod_1.z
|
|
105
|
+
.array(excludeGlobSchema, {
|
|
106
|
+
errorMap: (issue, ctx) => issue.code === "invalid_type"
|
|
107
|
+
? {
|
|
108
|
+
message: "expected an array of globs; exclude has no defaults, so it takes no { extend } or { replace }",
|
|
109
|
+
}
|
|
110
|
+
: { message: ctx.defaultError },
|
|
111
|
+
})
|
|
112
|
+
.optional(),
|
|
113
|
+
placeholders: listOverrideSchema(placeholderConventionSchema).optional(),
|
|
114
|
+
constructs: (0, exports.strictObjectNamingKeys)({
|
|
115
|
+
links: zod_1.z.boolean().optional(),
|
|
116
|
+
images: zod_1.z.boolean().optional(),
|
|
117
|
+
definitions: zod_1.z.boolean().optional(),
|
|
118
|
+
inlineCode: zod_1.z.boolean().optional(),
|
|
119
|
+
}, { noun: "construct", expected: "an object of booleans, e.g. { links, images, definitions, inlineCode }" }).optional(),
|
|
120
|
+
directories: zod_1.z.boolean().optional(),
|
|
121
|
+
bareFilenames: zod_1.z.boolean().optional(),
|
|
122
|
+
}, { noun: "setting", expected: "an object of doc-references settings" });
|
|
123
|
+
/** Every issue as `path: message`, `;`-separated, with `(root)` for the top level. */
|
|
124
|
+
const formatConfigIssues = (error) => error.issues.map(issue => `${issue.path.length > 0 ? issue.path.join(".") : "(root)"}: ${issue.message}`).join("; ");
|
|
125
|
+
exports.formatConfigIssues = formatConfigIssues;
|
|
126
|
+
const applyOverride = (defaults, override) => {
|
|
127
|
+
if (override?.replace) {
|
|
128
|
+
return override.replace;
|
|
129
|
+
}
|
|
130
|
+
return [...defaults, ...(override?.extend ?? [])];
|
|
131
|
+
};
|
|
132
|
+
/** The resolved settings when no config is given. */
|
|
133
|
+
exports.DEFAULT_DOCS_SETTINGS = {
|
|
134
|
+
rootDir: undefined,
|
|
135
|
+
ignore: exports.DEFAULT_IGNORE,
|
|
136
|
+
exclude: [],
|
|
137
|
+
placeholders: placeholders_1.DEFAULT_PLACEHOLDERS,
|
|
138
|
+
constructs: exports.DEFAULT_CONSTRUCTS,
|
|
139
|
+
directories: exports.DEFAULT_DIRECTORIES,
|
|
140
|
+
bareFilenames: exports.DEFAULT_BARE_FILENAMES,
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Validate a raw doc-references config and resolve it onto the defaults. `undefined` yields
|
|
144
|
+
* {@link DEFAULT_DOCS_SETTINGS}.
|
|
145
|
+
*
|
|
146
|
+
* @throws {ConfigError} naming every offending key, when the config is invalid
|
|
147
|
+
*/
|
|
148
|
+
const parseDocsSettings = (raw) => {
|
|
149
|
+
if (raw === undefined) {
|
|
150
|
+
return exports.DEFAULT_DOCS_SETTINGS;
|
|
151
|
+
}
|
|
152
|
+
const parsed = exports.docReferencesConfigSchema.safeParse(raw);
|
|
153
|
+
if (!parsed.success) {
|
|
154
|
+
throw new config_error_1.ConfigError(`Invalid doc-references config: ${(0, exports.formatConfigIssues)(parsed.error)}`);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
rootDir: parsed.data.rootDir,
|
|
158
|
+
ignore: applyOverride(exports.DEFAULT_IGNORE, parsed.data.ignore),
|
|
159
|
+
exclude: parsed.data.exclude ?? [],
|
|
160
|
+
placeholders: applyOverride(placeholders_1.DEFAULT_PLACEHOLDERS, parsed.data.placeholders),
|
|
161
|
+
constructs: { ...exports.DEFAULT_CONSTRUCTS, ...parsed.data.constructs },
|
|
162
|
+
directories: parsed.data.directories ?? exports.DEFAULT_DIRECTORIES,
|
|
163
|
+
bareFilenames: parsed.data.bareFilenames ?? exports.DEFAULT_BARE_FILENAMES,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
exports.parseDocsSettings = parseDocsSettings;
|
|
167
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type MarkdownNode } from "./parse";
|
|
2
|
+
/**
|
|
3
|
+
* The 1-based line whose references an ignore-next-line comment suppresses, the line after the
|
|
4
|
+
* comment ends, or `undefined` when `node` isn't one. In `.md` the comment is an HTML comment that
|
|
5
|
+
* is the whole `html` node; in `.mdx`, which has no HTML comments (one fails to parse), it is a
|
|
6
|
+
* comment-only `{/* ... *\/}` expression. Code blocks are leaves, so a comment inside one is text.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ignoredNextLine: (node: MarkdownNode) => number | undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ignoredNextLine = void 0;
|
|
4
|
+
const parse_1 = require("./parse");
|
|
5
|
+
/** `<!-- doc-references-ignore-next-line -->`, optionally with `: reason` before the `-->`. */
|
|
6
|
+
const HTML_COMMENT_RE = /^<!--\s*doc-references-ignore-next-line(?:\s*:[\s\S]*?)?\s*-->$/u;
|
|
7
|
+
/** `/* doc-references-ignore-next-line *\/`, optionally with `: reason`, as an MDX `{expression}`'s value. */
|
|
8
|
+
const MDX_COMMENT_RE = /^\/\*\s*doc-references-ignore-next-line(?:\s*:[\s\S]*?)?\s*\*\/$/u;
|
|
9
|
+
/**
|
|
10
|
+
* The 1-based line whose references an ignore-next-line comment suppresses, the line after the
|
|
11
|
+
* comment ends, or `undefined` when `node` isn't one. In `.md` the comment is an HTML comment that
|
|
12
|
+
* is the whole `html` node; in `.mdx`, which has no HTML comments (one fails to parse), it is a
|
|
13
|
+
* comment-only `{/* ... *\/}` expression. Code blocks are leaves, so a comment inside one is text.
|
|
14
|
+
*/
|
|
15
|
+
const ignoredNextLine = (node) => {
|
|
16
|
+
const end = node.position?.end.line;
|
|
17
|
+
const value = node.value?.trim();
|
|
18
|
+
if (end === undefined || value === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const isMarker = node.type === "html" ? HTML_COMMENT_RE.test(value) : (0, parse_1.isCommentExpression)(node) && MDX_COMMENT_RE.test(value);
|
|
21
|
+
return isMarker ? end + 1 : undefined;
|
|
22
|
+
};
|
|
23
|
+
exports.ignoredNextLine = ignoredNextLine;
|
|
24
|
+
//# sourceMappingURL=suppressions.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Finding } from "./doc-references/finding";
|
|
2
|
+
export declare const OUTPUT_FORMATS: readonly ["text", "json"];
|
|
3
|
+
/** How findings are printed: `text` lines for people, a `json` array for tools. */
|
|
4
|
+
export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
|
5
|
+
/** An expected error as its message; anything else is a bug, so its stack is kept for the report. */
|
|
6
|
+
export declare const formatError: (error: unknown) => string;
|
|
7
|
+
/** One finding as `file:line:col target reason[ did you mean <suggestion>]`. */
|
|
8
|
+
export declare const formatFinding: (finding: Finding) => string;
|
|
9
|
+
/**
|
|
10
|
+
* Findings as `text` (one {@link formatFinding} line each, nothing when there are none) or as a
|
|
11
|
+
* `json` array of every Finding field (`[]` when there are none). Non-empty output ends in a newline.
|
|
12
|
+
*/
|
|
13
|
+
export declare const formatFindings: (findings: ReadonlyArray<Finding>, format: OutputFormat) => string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatFindings = exports.formatFinding = exports.formatError = exports.OUTPUT_FORMATS = void 0;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const fs_errors_1 = require("./doc-references/fs-errors");
|
|
6
|
+
const parse_1 = require("./doc-references/parse");
|
|
7
|
+
exports.OUTPUT_FORMATS = ["text", "json"];
|
|
8
|
+
const isExpectedError = (error) => error instanceof config_1.ConfigError ||
|
|
9
|
+
error instanceof parse_1.DocumentParseError ||
|
|
10
|
+
error instanceof fs_errors_1.ReadError ||
|
|
11
|
+
(0, fs_errors_1.isSystemError)(error);
|
|
12
|
+
/** An expected error as its message; anything else is a bug, so its stack is kept for the report. */
|
|
13
|
+
const formatError = (error) => !isExpectedError(error) && error instanceof Error && error.stack !== undefined
|
|
14
|
+
? `${error.stack}\n`
|
|
15
|
+
: `Error: ${(0, fs_errors_1.errorMessage)(error)}\n`;
|
|
16
|
+
exports.formatError = formatError;
|
|
17
|
+
/** One finding as `file:line:col target reason[ did you mean <suggestion>]`. */
|
|
18
|
+
const formatFinding = (finding) => [
|
|
19
|
+
`${finding.file}:${finding.line}:${finding.column}`,
|
|
20
|
+
finding.target,
|
|
21
|
+
finding.reason,
|
|
22
|
+
...(finding.suggestion === undefined ? [] : [`did you mean ${finding.suggestion}`]),
|
|
23
|
+
].join(" ");
|
|
24
|
+
exports.formatFinding = formatFinding;
|
|
25
|
+
/**
|
|
26
|
+
* Findings as `text` (one {@link formatFinding} line each, nothing when there are none) or as a
|
|
27
|
+
* `json` array of every Finding field (`[]` when there are none). Non-empty output ends in a newline.
|
|
28
|
+
*/
|
|
29
|
+
const formatFindings = (findings, format) => format === "json"
|
|
30
|
+
? `${JSON.stringify(findings, null, 2)}\n`
|
|
31
|
+
: findings.map(finding => `${(0, exports.formatFinding)(finding)}\n`).join("");
|
|
32
|
+
exports.formatFindings = formatFindings;
|
|
33
|
+
//# sourceMappingURL=format.js.map
|