@veda-hq/pariksha 0.5.13 → 0.5.17
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/NOTICE +6 -0
- package/README.md +280 -5
- package/dist/absolute-paths.js +59 -0
- package/dist/aggregate-gate-needs.js +153 -0
- package/dist/cli.js +130 -0
- package/dist/doc-command-rules.js +179 -0
- package/dist/doc-command-targets.js +122 -0
- package/dist/doc-commands.js +187 -0
- package/dist/doc-files.js +74 -0
- package/dist/doc-flag-spec.js +87 -0
- package/dist/doc-identifiers.js +348 -0
- package/dist/doc-links.js +183 -0
- package/dist/doc-paths.js +267 -0
- package/dist/doc-references-config.js +172 -0
- package/dist/doc-references-gate.js +338 -0
- package/dist/doc-references.js +535 -0
- package/dist/doc-scripts.js +224 -0
- package/dist/non-synced-resources.js +164 -0
- package/dist/pnpm-settings.js +350 -0
- package/dist/semver-range.js +320 -0
- package/dist/shell-text.js +193 -0
- package/package.json +5 -3
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// DOC-DRIFT-02, documented scripts: a `pnpm <script>` a doc shows exists, in
|
|
2
|
+
// the scope the doc runs it from, and a verification step does not run a
|
|
3
|
+
// script that changes the tree it is meant to check (`lint` auto-fixes:
|
|
4
|
+
// `lint:ci` is the check). A step that names a missing script looks like a
|
|
5
|
+
// check and runs nothing.
|
|
6
|
+
import { readdirSync } from 'node:fs';
|
|
7
|
+
import { join, posix } from 'node:path';
|
|
8
|
+
import { codeSpans as markdownCodeSpans } from "./doc-references.js";
|
|
9
|
+
import { isRecord } from "./registry-ratchet.js";
|
|
10
|
+
import { maskQuoted, stripComment } from "./shell-text.js";
|
|
11
|
+
/**
|
|
12
|
+
* Executables in `node_modules/.bin`. THROWS rather than returning a
|
|
13
|
+
* wildcard: a missing install that let every name pass would print a pass
|
|
14
|
+
* while scanning nothing.
|
|
15
|
+
*/
|
|
16
|
+
export const binNames = (root) => {
|
|
17
|
+
const dir = join(root, 'node_modules', '.bin');
|
|
18
|
+
try {
|
|
19
|
+
return readdirSync(dir);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error(`cannot read ${dir}; install the dependencies before this check`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Every manifest the config lists that exists. One that does not parse would
|
|
27
|
+
* silently shrink the known-script set, so it fails loud instead.
|
|
28
|
+
*/
|
|
29
|
+
const readManifests = (manifests, read) => manifests.flatMap((path) => {
|
|
30
|
+
const text = read(path);
|
|
31
|
+
if (text === undefined)
|
|
32
|
+
return [];
|
|
33
|
+
let pkg;
|
|
34
|
+
try {
|
|
35
|
+
pkg = JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`${path} is not valid JSON — cannot determine which scripts exist`);
|
|
39
|
+
}
|
|
40
|
+
const name = isRecord(pkg) && typeof pkg['name'] === 'string' ? pkg['name'] : undefined;
|
|
41
|
+
const raw = isRecord(pkg) ? pkg['scripts'] : undefined;
|
|
42
|
+
const scripts = {};
|
|
43
|
+
if (isRecord(raw))
|
|
44
|
+
for (const [key, body] of Object.entries(raw))
|
|
45
|
+
scripts[key] = String(body);
|
|
46
|
+
return [{ scope: posix.dirname(path), name, scripts }];
|
|
47
|
+
});
|
|
48
|
+
/** A manifest's scope keys: its directory, and its package name if any. */
|
|
49
|
+
const scopeKeys = (manifest) => (manifest.name === undefined ? [manifest.scope] : [manifest.scope, manifest.name]);
|
|
50
|
+
/** Every name `pnpm <name>` can resolve to: a script in any listed manifest, or an executable in `node_modules/.bin`. */
|
|
51
|
+
export const definedScripts = (root, manifests, read) => new Set([...binNames(root), ...readManifests(manifests, read).flatMap((manifest) => Object.keys(manifest.scripts))]);
|
|
52
|
+
/**
|
|
53
|
+
* Scripts per scope, because `pnpm foo` does not resolve `foo` from a
|
|
54
|
+
* workspace. Keyed by both the workspace directory and the package name, as
|
|
55
|
+
* `--filter` takes either. Bins resolve in every scope: pnpm puts the same
|
|
56
|
+
* hoisted `.bin` on PATH for a filtered command.
|
|
57
|
+
*/
|
|
58
|
+
export const scriptScopes = (root, manifests, read) => {
|
|
59
|
+
const scopes = new Map();
|
|
60
|
+
const bins = binNames(root);
|
|
61
|
+
for (const manifest of readManifests(manifests, read)) {
|
|
62
|
+
const names = new Set([...Object.keys(manifest.scripts), ...bins]);
|
|
63
|
+
for (const key of scopeKeys(manifest))
|
|
64
|
+
scopes.set(key, names);
|
|
65
|
+
}
|
|
66
|
+
return scopes;
|
|
67
|
+
};
|
|
68
|
+
export const scriptBodies = (manifests, read) => {
|
|
69
|
+
const scripts = new Map();
|
|
70
|
+
const scopeDirs = new Map();
|
|
71
|
+
for (const manifest of readManifests(manifests, read)) {
|
|
72
|
+
const bodies = new Map(Object.entries(manifest.scripts));
|
|
73
|
+
for (const key of scopeKeys(manifest)) {
|
|
74
|
+
scripts.set(key, bodies);
|
|
75
|
+
scopeDirs.set(key, manifest.scope);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { scripts, scopeDirs };
|
|
79
|
+
};
|
|
80
|
+
/** Half-open [start, end) ranges of every inline code span on a line, between its delimiters. */
|
|
81
|
+
export const spanRanges = (line) => markdownCodeSpans(line).map((span) => [span.start, span.end]);
|
|
82
|
+
const FENCE_OPEN = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
83
|
+
/**
|
|
84
|
+
* Each line's place relative to fenced code blocks, by CommonMark's rule: an
|
|
85
|
+
* opening fence is three or more backticks or tildes indented at most three
|
|
86
|
+
* spaces (a backtick fence's info string has no backtick); it closes at a
|
|
87
|
+
* fence of the same character, at least as long, with nothing after it but
|
|
88
|
+
* spaces; an unclosed fence runs to the end.
|
|
89
|
+
*/
|
|
90
|
+
export const fenceRoles = (lines) => {
|
|
91
|
+
const roles = [];
|
|
92
|
+
let fence = null;
|
|
93
|
+
for (const raw of lines) {
|
|
94
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
95
|
+
if (fence === null) {
|
|
96
|
+
const open = FENCE_OPEN.exec(line);
|
|
97
|
+
const marker = open?.[2] ?? '';
|
|
98
|
+
if (open !== null && !(marker.startsWith('`') && String(open[3]).includes('`'))) {
|
|
99
|
+
fence = { char: marker.charAt(0), length: marker.length };
|
|
100
|
+
roles.push('fence');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
roles.push('prose');
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const close = /^ {0,3}(`+|~+)[ \t]*$/.exec(line)?.[1];
|
|
108
|
+
if (close !== undefined && close.charAt(0) === fence.char && close.length >= fence.length) {
|
|
109
|
+
fence = null;
|
|
110
|
+
roles.push('fence');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
roles.push('code');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return roles;
|
|
117
|
+
};
|
|
118
|
+
/** pnpm's own subcommands, which are not package scripts. */
|
|
119
|
+
const BUILTIN = new Set([
|
|
120
|
+
'install',
|
|
121
|
+
'add',
|
|
122
|
+
'remove',
|
|
123
|
+
'exec',
|
|
124
|
+
'dlx',
|
|
125
|
+
'why',
|
|
126
|
+
'audit',
|
|
127
|
+
'list',
|
|
128
|
+
'outdated',
|
|
129
|
+
'update',
|
|
130
|
+
'publish',
|
|
131
|
+
'pack',
|
|
132
|
+
'link',
|
|
133
|
+
'patch',
|
|
134
|
+
'store',
|
|
135
|
+
'config',
|
|
136
|
+
'approve-builds',
|
|
137
|
+
'rebuild',
|
|
138
|
+
'prune',
|
|
139
|
+
'setup',
|
|
140
|
+
'licenses',
|
|
141
|
+
'deploy',
|
|
142
|
+
'env',
|
|
143
|
+
'init',
|
|
144
|
+
'create',
|
|
145
|
+
'run',
|
|
146
|
+
'help',
|
|
147
|
+
'self-update',
|
|
148
|
+
'doctor',
|
|
149
|
+
'root',
|
|
150
|
+
'bin',
|
|
151
|
+
'fetch',
|
|
152
|
+
'import',
|
|
153
|
+
'unlink',
|
|
154
|
+
'dedupe',
|
|
155
|
+
'cache',
|
|
156
|
+
]);
|
|
157
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
158
|
+
/** What may precede `pnpm` for it to start a command: nothing, a separator, a prompt, env assignments, or a wrapper. */
|
|
159
|
+
export const commandStart = (wrappers) => new RegExp(`(?:^|&&|\\|\\||;|\\||\\$|\\(|\\b(?:${['exec', ...wrappers.map(escapeRegExp), 'time', 'env', 'then', 'do'].join('|')})|\\s--)\\s*(?:[A-Z_][A-Z0-9_]*=\\S*\\s+)*$`);
|
|
160
|
+
/** The documented-script rules over `files`; `scopes` judges where each resolves. */
|
|
161
|
+
export const findScriptProblems = (config, files, defined, scopes, read) => {
|
|
162
|
+
const problems = [];
|
|
163
|
+
const start = commandStart(config.scripts.wrappers);
|
|
164
|
+
// `pnpm run foo`, `pnpm foo`, `pnpm --filter x foo`; not `npx`, which
|
|
165
|
+
// resolves from the registry. The `--filter` target is captured: a filtered
|
|
166
|
+
// command resolves in another scope.
|
|
167
|
+
const invocation = /\bpnpm\s+(?:run\s+)?(?:--filter[= ](\S+)\s+)?([a-z][a-z0-9:-]*)/g;
|
|
168
|
+
for (const relative of files) {
|
|
169
|
+
const content = read(relative);
|
|
170
|
+
if (content === undefined)
|
|
171
|
+
continue;
|
|
172
|
+
const lines = content.split('\n');
|
|
173
|
+
const roles = fenceRoles(lines);
|
|
174
|
+
lines.forEach((line, index) => {
|
|
175
|
+
const role = roles[index];
|
|
176
|
+
// A fence line itself carries no command; its info string is a label.
|
|
177
|
+
if (role === 'fence' || config.exemptMarker.test(line))
|
|
178
|
+
return;
|
|
179
|
+
const inFence = role === 'code';
|
|
180
|
+
const spans = inFence ? null : spanRanges(line);
|
|
181
|
+
// A shell comment in a fence is prose; a `#` inside quotes or escaped starts none.
|
|
182
|
+
const text = inFence ? stripComment(line) : line;
|
|
183
|
+
// A fence line is shell: a quoted separator starts no command, so the
|
|
184
|
+
// start is judged in the quote-masked line. Prose is not shell.
|
|
185
|
+
const code = inFence ? maskQuoted(text) : text;
|
|
186
|
+
for (const match of text.matchAll(invocation)) {
|
|
187
|
+
const filter = match[1];
|
|
188
|
+
const script = String(match[2]);
|
|
189
|
+
if (BUILTIN.has(script))
|
|
190
|
+
continue;
|
|
191
|
+
// Only text the author marked as code counts, and all of the invocation must be in it.
|
|
192
|
+
const span = spans === null ? [0, text.length] : spans.find(([a, b]) => match.index >= a && match.index + match[0].length <= b);
|
|
193
|
+
if (span === undefined)
|
|
194
|
+
continue;
|
|
195
|
+
// `pnpm dev:*` and `pnpm <script>` name a family, not a script.
|
|
196
|
+
if (/^[*<]/.test(text.slice(match.index + match[0].length)))
|
|
197
|
+
continue;
|
|
198
|
+
// Only a command's start runs anything: `using pnpm v11` in a quoted log line is output.
|
|
199
|
+
if (!start.test(code.slice(span[0], match.index)))
|
|
200
|
+
continue;
|
|
201
|
+
const report = (reason) => {
|
|
202
|
+
problems.push({ file: relative, line: index + 1, script, reason });
|
|
203
|
+
};
|
|
204
|
+
if (!defined.has(script)) {
|
|
205
|
+
report(`no workspace defines a \`${script}\` script — this step runs nothing`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Existing somewhere is not resolving where the doc runs it: an unknown
|
|
209
|
+
// --filter target is not this check's to adjudicate.
|
|
210
|
+
const scope = scopes.get(filter ?? '.');
|
|
211
|
+
if (scope !== undefined && !scope.has(script)) {
|
|
212
|
+
report(filter === undefined
|
|
213
|
+
? `\`${script}\` is not a root script — \`pnpm ${script}\` resolves the root manifest, so this needs \`--filter\``
|
|
214
|
+
: `\`${script}\` is not a script of \`${filter}\` — the filter names a workspace that does not define it`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const safer = config.scripts.mutating.get(script);
|
|
218
|
+
if (safer !== undefined && config.scripts.verification.test(line))
|
|
219
|
+
report(`\`${script}\` MUTATES the tree (it auto-fixes) — a verification step must use \`${safer}\``);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return problems;
|
|
224
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// non-synced-resources: every relative Markdown link in a document names a
|
|
2
|
+
// file or folder the repository holds, under the same case.
|
|
3
|
+
//
|
|
4
|
+
// A link to a file that was never committed, or to one outside the checkout,
|
|
5
|
+
// works for its writer and for nobody else. Each `[text](dest)` and
|
|
6
|
+
// `` on a line (in code blocks too) is read; `http(s)://`,
|
|
7
|
+
// `mailto:` and `#anchor` links are not. Its `#fragment` and `?query` go, then
|
|
8
|
+
// its destination is the text inside `<…>`, or the text before a space (a
|
|
9
|
+
// title). A destination starting with `/` is read from the repository's top.
|
|
10
|
+
// It is refused when its folder does not exist, when it lands outside the
|
|
11
|
+
// repository, when nothing is there, or when the name on disk differs in case
|
|
12
|
+
// (macOS opens `./README.MD` as `./readme.md`; Linux and CI do not). An image
|
|
13
|
+
// over 1 MiB is warned about, never refused.
|
|
14
|
+
//
|
|
15
|
+
// Ported from a shell check unchanged: paths are joined and split as the shell
|
|
16
|
+
// joined them (dirname, basename, `cd && pwd`), so a link's text decides what
|
|
17
|
+
// counts as inside the repository exactly as before.
|
|
18
|
+
import { lstatSync, readdirSync, statSync } from 'node:fs';
|
|
19
|
+
import { posix } from 'node:path';
|
|
20
|
+
import { linesOf } from "./doc-files.js";
|
|
21
|
+
/** A Markdown link or image, as the shell check's `grep -oE` matched it. */
|
|
22
|
+
const LINK = /!?\[[^\]]*\]\([^)]+\)/g;
|
|
23
|
+
const SKIPPED = /^(https?:\/\/|mailto:|#)/;
|
|
24
|
+
const IMAGE = /\.(png|jpg|jpeg|gif|webp)$/;
|
|
25
|
+
const LARGE_IMAGE_BYTES = 1_048_576;
|
|
26
|
+
export const DEFAULT_GUIDANCE = ['Please ensure all referenced files exist in the repository.'];
|
|
27
|
+
/** The destination of each link on each line of `text`, as written, http(s), mailto and anchor links dropped. */
|
|
28
|
+
export const linkTargets = (text) => linesOf(text)
|
|
29
|
+
.flatMap((line) => [...line.matchAll(LINK)].map((m) => m[0].slice(m[0].indexOf('](') + 2, -1)))
|
|
30
|
+
.filter((dest) => !SKIPPED.test(dest));
|
|
31
|
+
/** A destination without its fragment, query or title, and out of its angle brackets; '' when nothing is left. */
|
|
32
|
+
export const linkPath = (dest) => {
|
|
33
|
+
let link = dest;
|
|
34
|
+
const hash = link.indexOf('#');
|
|
35
|
+
if (hash !== -1)
|
|
36
|
+
link = link.slice(0, hash);
|
|
37
|
+
const query = link.indexOf('?');
|
|
38
|
+
if (query !== -1)
|
|
39
|
+
link = link.slice(0, query);
|
|
40
|
+
if (link.startsWith('<') && link.indexOf('>', 1) !== -1)
|
|
41
|
+
return link.slice(1, link.indexOf('>'));
|
|
42
|
+
const space = link.indexOf(' ');
|
|
43
|
+
return space === -1 ? link : link.slice(0, space);
|
|
44
|
+
};
|
|
45
|
+
const stripTrailingSlashes = (path) => {
|
|
46
|
+
const stripped = path.replace(/\/+$/, '');
|
|
47
|
+
return stripped === '' && path !== '' ? '/' : stripped;
|
|
48
|
+
};
|
|
49
|
+
/** POSIX `dirname`. */
|
|
50
|
+
export const dirnameOf = (path) => {
|
|
51
|
+
const trimmed = stripTrailingSlashes(path);
|
|
52
|
+
const slash = trimmed.lastIndexOf('/');
|
|
53
|
+
if (slash === -1)
|
|
54
|
+
return '.';
|
|
55
|
+
const head = trimmed.slice(0, slash).replace(/\/+$/, '');
|
|
56
|
+
return head === '' ? '/' : head;
|
|
57
|
+
};
|
|
58
|
+
/** POSIX `basename`. */
|
|
59
|
+
export const basenameOf = (path) => {
|
|
60
|
+
const trimmed = stripTrailingSlashes(path);
|
|
61
|
+
return trimmed === '/' ? '/' : trimmed.slice(trimmed.lastIndexOf('/') + 1);
|
|
62
|
+
};
|
|
63
|
+
const isDir = (path) => {
|
|
64
|
+
try {
|
|
65
|
+
return statSync(path).isDirectory();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const exists = (path) => {
|
|
72
|
+
try {
|
|
73
|
+
const s = statSync(path);
|
|
74
|
+
return s.isFile() || s.isDirectory();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
/** Whether folder `dir` holds an entry named exactly `name`; `.` and `..` always. */
|
|
81
|
+
export const nameOnDisk = (dir, name) => {
|
|
82
|
+
if (name === '.' || name === '..')
|
|
83
|
+
return true;
|
|
84
|
+
try {
|
|
85
|
+
return readdirSync(dir).includes(name);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/** A regular file's size in bytes, the link itself for a symlink (as `stat` without `-L` reads it). */
|
|
92
|
+
const sizeOf = (path) => {
|
|
93
|
+
try {
|
|
94
|
+
const s = lstatSync(path);
|
|
95
|
+
return statSync(path).isFile() ? s.size : undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The lines for one document's links, and whether any was refused. `file` is
|
|
103
|
+
* as listed; a relative one is read from `top`, the repository's top folder.
|
|
104
|
+
*/
|
|
105
|
+
export const checkDoc = (top, file, text) => {
|
|
106
|
+
const out = [];
|
|
107
|
+
let refused = false;
|
|
108
|
+
const fileDir = dirnameOf(file);
|
|
109
|
+
const under = (path) => path.startsWith(`${top}/`);
|
|
110
|
+
const relativeOf = (path) => (under(path) ? path.slice(top.length + 1) : path);
|
|
111
|
+
for (const dest of linkTargets(text)) {
|
|
112
|
+
const link = linkPath(dest);
|
|
113
|
+
if (link === '')
|
|
114
|
+
continue;
|
|
115
|
+
let target = link.startsWith('/') ? `${top}${link}` : fileDir.startsWith('/') ? `${fileDir}/${link}` : `${top}/${fileDir}/${link}`;
|
|
116
|
+
const parent = dirnameOf(target);
|
|
117
|
+
if (!isDir(parent) && under(target)) {
|
|
118
|
+
out.push(`❌ Error: Reference to file in non-existent directory in ${file}:`, ` Link: ${link}`, ` Expected path: ${relativeOf(target)}`, ' Parent directory does not exist', '');
|
|
119
|
+
refused = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// `cd "$parent" && pwd`: the folder with `.` and `..` resolved by name.
|
|
123
|
+
target = `${isDir(parent) ? posix.resolve(parent) : parent}/${basenameOf(target)}`;
|
|
124
|
+
if (!under(target)) {
|
|
125
|
+
out.push(`❌ Error: Reference to file outside repository in ${file}:`, ` Link: ${link}`, ` Resolved path: ${target}`, ' All references must be within the repository', '');
|
|
126
|
+
refused = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const relative = relativeOf(target);
|
|
130
|
+
if (!exists(target)) {
|
|
131
|
+
out.push(`❌ Error: Reference to non-existent file in ${file}:`, ` Link: ${link}`, ` Expected path: ${relative}`, '');
|
|
132
|
+
refused = true;
|
|
133
|
+
}
|
|
134
|
+
else if (!nameOnDisk(dirnameOf(target), basenameOf(target))) {
|
|
135
|
+
out.push(`❌ Error: Reference whose case differs from the file on disk in ${file}:`, ` Link: ${link}`, ` Expected path: ${relative}`, '');
|
|
136
|
+
refused = true;
|
|
137
|
+
}
|
|
138
|
+
const size = IMAGE.test(target) ? sizeOf(target) : undefined;
|
|
139
|
+
if (size !== undefined && size > LARGE_IMAGE_BYTES) {
|
|
140
|
+
out.push(`⚠️ Warning: Large image referenced in ${file}:`, ` Image: ${relative}`, ` Size: ~${Math.floor(size / 1024 / 1024)} MB (${Math.floor(size / 1024)} KB)`, ' Consider compressing images to keep repository size small.', '');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { refused, out };
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* The report for `docs` (path and text, in order), printed as the shell check
|
|
147
|
+
* printed it. `top` is asked for only when there is a document to read.
|
|
148
|
+
*/
|
|
149
|
+
export const checkNonSyncedResources = (docs, top, config) => {
|
|
150
|
+
const out = ['→ Checking for non-synced resource references...'];
|
|
151
|
+
if (docs.length === 0)
|
|
152
|
+
return { ok: true, out: [...out, '✅ No documentation files to check.'] };
|
|
153
|
+
out.push(` Checking ${docs.length} documentation file(s)...`);
|
|
154
|
+
const root = top();
|
|
155
|
+
let refused = false;
|
|
156
|
+
for (const doc of docs) {
|
|
157
|
+
const result = checkDoc(root, doc.file, doc.text());
|
|
158
|
+
out.push(...result.out);
|
|
159
|
+
refused ||= result.refused;
|
|
160
|
+
}
|
|
161
|
+
if (refused)
|
|
162
|
+
return { ok: false, out: [...out, '', ...config.guidance] };
|
|
163
|
+
return { ok: true, out: [...out, '✅ All resource references are valid'] };
|
|
164
|
+
};
|