@titannio/webtoolkit-cli 1.3.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +639 -0
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +268 -0
  5. package/dist/bundle-audit.d.ts +7 -0
  6. package/dist/bundle-audit.js +111 -0
  7. package/dist/cleaner.d.ts +15 -0
  8. package/dist/cleaner.js +359 -0
  9. package/dist/config-reference.d.ts +8 -0
  10. package/dist/config-reference.js +805 -0
  11. package/dist/config.d.ts +306 -0
  12. package/dist/config.js +173 -0
  13. package/dist/dev-grid.d.ts +7 -0
  14. package/dist/dev-grid.js +181 -0
  15. package/dist/dev-watch.d.ts +13 -0
  16. package/dist/dev-watch.js +184 -0
  17. package/dist/environment.d.ts +10 -0
  18. package/dist/environment.js +172 -0
  19. package/dist/guard-registry.d.ts +1 -0
  20. package/dist/guard-registry.js +17 -0
  21. package/dist/guard-runner.d.ts +4 -0
  22. package/dist/guard-runner.js +36 -0
  23. package/dist/guards/any-guard.d.ts +16 -0
  24. package/dist/guards/any-guard.js +121 -0
  25. package/dist/guards/assert-no-tests-in-dist.d.ts +1 -0
  26. package/dist/guards/assert-no-tests-in-dist.js +56 -0
  27. package/dist/guards/check-mojibake.d.ts +52 -0
  28. package/dist/guards/check-mojibake.js +378 -0
  29. package/dist/guards/code-pattern-guard.d.ts +71 -0
  30. package/dist/guards/code-pattern-guard.js +654 -0
  31. package/dist/guards/dal-service-repository-check.d.ts +13 -0
  32. package/dist/guards/dal-service-repository-check.js +264 -0
  33. package/dist/guards/dependency-cruiser-guard.d.ts +14 -0
  34. package/dist/guards/dependency-cruiser-guard.js +69 -0
  35. package/dist/guards/documentation-guard.d.ts +3 -0
  36. package/dist/guards/documentation-guard.js +370 -0
  37. package/dist/guards/guard-config.d.ts +19 -0
  38. package/dist/guards/guard-config.js +87 -0
  39. package/dist/guards/internal-link-guard.d.ts +12 -0
  40. package/dist/guards/internal-link-guard.js +272 -0
  41. package/dist/guards/package-surface-guard.d.ts +37 -0
  42. package/dist/guards/package-surface-guard.js +234 -0
  43. package/dist/guards/pnpm-workspace-config.d.ts +2 -0
  44. package/dist/guards/pnpm-workspace-config.js +40 -0
  45. package/dist/guards/rebuild-preflight.d.ts +29 -0
  46. package/dist/guards/rebuild-preflight.js +137 -0
  47. package/dist/guards/repository-hygiene-guard.d.ts +12 -0
  48. package/dist/guards/repository-hygiene-guard.js +70 -0
  49. package/dist/guards/schema-guard.d.ts +10 -0
  50. package/dist/guards/schema-guard.js +160 -0
  51. package/dist/guards/singleton-deps-guard.d.ts +21 -0
  52. package/dist/guards/singleton-deps-guard.js +183 -0
  53. package/dist/guards/tsconfig-guard.d.ts +5 -0
  54. package/dist/guards/tsconfig-guard.js +105 -0
  55. package/dist/guards/workspace-manifest-guard.d.ts +21 -0
  56. package/dist/guards/workspace-manifest-guard.js +210 -0
  57. package/dist/jsdoc-report.d.ts +7 -0
  58. package/dist/jsdoc-report.js +456 -0
  59. package/dist/process.d.ts +20 -0
  60. package/dist/process.js +86 -0
  61. package/dist/ready-service.d.ts +7 -0
  62. package/dist/ready-service.js +135 -0
  63. package/dist/release-gate.d.ts +7 -0
  64. package/dist/release-gate.js +35 -0
  65. package/dist/repo-check.d.ts +7 -0
  66. package/dist/repo-check.js +121 -0
  67. package/dist/tasks.d.ts +17 -0
  68. package/dist/tasks.js +195 -0
  69. package/dist/upgrade.d.ts +10 -0
  70. package/dist/upgrade.js +674 -0
  71. package/dist/validate.d.ts +7 -0
  72. package/dist/validate.js +51 -0
  73. package/dist/workspace-tests.d.ts +33 -0
  74. package/dist/workspace-tests.js +529 -0
  75. package/package.json +57 -0
@@ -0,0 +1,370 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { basename, dirname, extname, isAbsolute, join, matchesGlob, relative, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { loadConfig } from '../config.js';
5
+ const defaultExcludedDirectories = new Set(['.corepack', '.git', 'build', 'coverage', 'dist', 'node_modules']);
6
+ function normalizePath(filePath) {
7
+ return filePath.replace(/\\/gu, '/');
8
+ }
9
+ function relativePath(root, filePath) {
10
+ return normalizePath(relative(root, filePath));
11
+ }
12
+ function isInsideRoot(root, filePath) {
13
+ const fromRoot = relative(root, filePath);
14
+ return fromRoot !== '..' && !fromRoot.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) && !isAbsolute(fromRoot);
15
+ }
16
+ function assertStringArray(value, label, required = false) {
17
+ if ((!required && value === undefined) || (Array.isArray(value) && value.every((item) => typeof item === 'string') && (!required || value.length > 0)))
18
+ return;
19
+ throw new Error(`${label} must be ${required ? 'a non-empty ' : 'an '}array of strings.`);
20
+ }
21
+ function assertSafePattern(pattern, label) {
22
+ const comparable = pattern.replaceAll('{basename}', 'file.md').replaceAll('{stem}', 'file');
23
+ if (isAbsolute(comparable) || normalizePath(comparable).split('/').includes('..')) {
24
+ throw new Error(`${label} must stay inside the repository: ${pattern}`);
25
+ }
26
+ }
27
+ export function validateDocumentationConfig(config) {
28
+ assertStringArray(config.files, 'documentation.files', true);
29
+ assertStringArray(config.excludeDirectories, 'documentation.excludeDirectories');
30
+ assertStringArray(config.requiredFiles, 'documentation.requiredFiles');
31
+ const patterns = [...config.files, ...(config.requiredFiles ?? [])];
32
+ const reachability = config.checks?.reachability;
33
+ if (reachability) {
34
+ assertStringArray(reachability.entrypoints, 'documentation.checks.reachability.entrypoints', true);
35
+ assertStringArray(reachability.files, 'documentation.checks.reachability.files', true);
36
+ patterns.push(...reachability.entrypoints, ...reachability.files);
37
+ }
38
+ if (config.collections !== undefined && !Array.isArray(config.collections))
39
+ throw new Error('documentation.collections must be an array.');
40
+ for (const [index, collection] of (config.collections ?? []).entries()) {
41
+ assertStringArray(collection.files, `documentation.collections[${index}].files`, true);
42
+ assertStringArray(collection.exclude, `documentation.collections[${index}].exclude`);
43
+ patterns.push(...collection.files, ...(collection.exclude ?? []));
44
+ if (collection.index)
45
+ patterns.push(collection.index);
46
+ if (collection.metadata && (typeof collection.metadata !== 'object' || Array.isArray(collection.metadata))) {
47
+ throw new Error(`documentation.collections[${index}].metadata must be an object.`);
48
+ }
49
+ const paired = collection.pairedDocuments;
50
+ if (paired) {
51
+ if (typeof paired.target !== 'string' || paired.target.length === 0)
52
+ throw new Error(`documentation.collections[${index}].pairedDocuments.target must be a string.`);
53
+ patterns.push(paired.target);
54
+ if (paired.index)
55
+ patterns.push(paired.index);
56
+ if (paired.table) {
57
+ assertStringArray(paired.table.header, `documentation.collections[${index}].pairedDocuments.table.header`, true);
58
+ if (!paired.table.header.includes(paired.table.fileColumn))
59
+ throw new Error(`documentation.collections[${index}].pairedDocuments.table.fileColumn must name a header column.`);
60
+ }
61
+ }
62
+ }
63
+ if (config.inventories !== undefined && !Array.isArray(config.inventories))
64
+ throw new Error('documentation.inventories must be an array.');
65
+ for (const [index, inventory] of (config.inventories ?? []).entries()) {
66
+ if (typeof inventory.document !== 'string' || inventory.document.length === 0)
67
+ throw new Error(`documentation.inventories[${index}].document must be a string.`);
68
+ assertStringArray(inventory.sources, `documentation.inventories[${index}].sources`, true);
69
+ patterns.push(inventory.document, ...inventory.sources);
70
+ }
71
+ for (const pattern of patterns)
72
+ assertSafePattern(pattern, 'Documentation path');
73
+ }
74
+ function walkFiles(directory, excludedDirectories) {
75
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
76
+ const filePath = join(directory, entry.name);
77
+ if (entry.isDirectory())
78
+ return excludedDirectories.has(entry.name) ? [] : walkFiles(filePath, excludedDirectories);
79
+ /* v8 ignore next -- non-file directory entries are intentionally ignored */
80
+ if (!entry.isFile())
81
+ return [];
82
+ return [filePath];
83
+ });
84
+ }
85
+ function matches(filePath, patterns) {
86
+ return patterns.some((pattern) => matchesGlob(normalizePath(filePath), normalizePath(pattern)));
87
+ }
88
+ function inspectMarkdown(filePath) {
89
+ const content = readFileSync(filePath, 'utf8');
90
+ const headings = [];
91
+ const links = [];
92
+ const metadata = new Map();
93
+ let fence;
94
+ for (const [index, line] of content.split(/\r?\n/u).entries()) {
95
+ const fenceMatch = line.match(/^\s*(```+|~~~+)/u);
96
+ if (fenceMatch) {
97
+ fence = fence ? undefined : fenceMatch[1][0];
98
+ continue;
99
+ }
100
+ if (fence)
101
+ continue;
102
+ const heading = line.match(/^(#{1,6})\s+(.+?)\s*$/u);
103
+ if (heading)
104
+ headings.push({ level: heading[1].length, text: heading[2], line: index + 1 });
105
+ const metadataMatch = line.match(/^>\s+\*\*([^*]+):\*\*\s+(.+?)\s{0,2}$/u);
106
+ if (metadataMatch)
107
+ metadata.set(metadataMatch[1], metadataMatch[2]);
108
+ for (const linkMatch of line.matchAll(/!?\[[^\]]*\]\((<[^>]+>|[^\s)]+)(?:\s+["'][^"']*["'])?\)/gu)) {
109
+ links.push({ target: linkMatch[1].replace(/^<|>$/gu, ''), line: index + 1 });
110
+ }
111
+ }
112
+ return { headings, links, metadata, content };
113
+ }
114
+ function resolveLocalTarget(root, source, target) {
115
+ if (!target || target.startsWith('#') || target.startsWith('//') || /^[a-z][a-z\d+.-]*:/iu.test(target))
116
+ return undefined;
117
+ const pathPart = target.split(/[?#]/u, 1)[0];
118
+ if (!pathPart)
119
+ return undefined;
120
+ let decoded = pathPart;
121
+ try {
122
+ decoded = decodeURIComponent(pathPart);
123
+ }
124
+ catch {
125
+ // Keep the original path so malformed links are reported as missing.
126
+ }
127
+ return resolve(pathPart.startsWith('/') ? root : dirname(source), pathPart.startsWith('/') ? `.${decoded}` : decoded);
128
+ }
129
+ function resolvedMarkdownTarget(root, source, target) {
130
+ const resolved = resolveLocalTarget(root, source, target);
131
+ if (!resolved || !existsSync(resolved))
132
+ return resolved;
133
+ return statSync(resolved).isDirectory() ? join(resolved, 'README.md') : resolved;
134
+ }
135
+ function linkCount(root, source, parsed, target) {
136
+ return parsed.links.filter((link) => resolvedMarkdownTarget(root, source, link.target) === target).length;
137
+ }
138
+ function interpolate(template, source) {
139
+ const fileName = basename(source);
140
+ return template.replaceAll('{basename}', fileName).replaceAll('{stem}', basename(fileName, extname(fileName)));
141
+ }
142
+ function markdownCells(line) {
143
+ const trimmed = line.trim();
144
+ if (!trimmed.startsWith('|') || !trimmed.endsWith('|'))
145
+ return [];
146
+ return trimmed.slice(1, -1).split('|').map((cell) => cell.trim());
147
+ }
148
+ function validatePairContent(root, target, parsed, pair) {
149
+ const errors = [];
150
+ const display = relativePath(root, target);
151
+ if (pair.table) {
152
+ const lines = parsed.content.split(/\r?\n/u);
153
+ const headerIndex = lines.findIndex((line) => JSON.stringify(markdownCells(line)) === JSON.stringify(pair.table?.header));
154
+ if (headerIndex < 0) {
155
+ errors.push(`${display}: missing table header | ${pair.table.header.join(' | ')} |`);
156
+ }
157
+ else {
158
+ const columnIndex = pair.table.header.indexOf(pair.table.fileColumn);
159
+ const rows = [];
160
+ for (let index = headerIndex + 2; index < lines.length; index += 1) {
161
+ const cells = markdownCells(lines[index]);
162
+ if (cells.length === 0)
163
+ break;
164
+ rows.push(cells);
165
+ }
166
+ const minRows = pair.table.minRows ?? 1;
167
+ if (rows.length < minRows)
168
+ errors.push(`${display}: table requires at least ${minRows} data row(s)`);
169
+ for (const row of rows) {
170
+ const references = [...(row[columnIndex] ?? '').matchAll(/`([^`]+)`/gu)].map((match) => match[1]);
171
+ const invalidReference = references.find((reference) => {
172
+ const filePath = resolve(root, reference);
173
+ return !isInsideRoot(root, filePath) || !existsSync(filePath) || !statSync(filePath).isFile();
174
+ });
175
+ if (references.length === 0 || invalidReference) {
176
+ errors.push(`${display}: invalid or missing file reference in ${pair.table.fileColumn}: ${row[columnIndex] ?? ''}`);
177
+ }
178
+ }
179
+ }
180
+ }
181
+ if (pair.finalSection) {
182
+ const headings = parsed.headings.filter((heading) => heading.level === 2);
183
+ const section = headings.find((heading) => heading.text === pair.finalSection?.heading);
184
+ if (!section) {
185
+ errors.push(`${display}: missing final section ## ${pair.finalSection.heading}`);
186
+ }
187
+ else {
188
+ if (headings.at(-1) !== section)
189
+ errors.push(`${display}: ## ${pair.finalSection.heading} must be the final H2 section`);
190
+ const lines = parsed.content.split(/\r?\n/u).slice(section.line);
191
+ const items = lines.filter((line) => /^\s*-\s+\S/u.test(line)).length;
192
+ if (items < (pair.finalSection.minItems ?? 1))
193
+ errors.push(`${display}: final section requires at least ${pair.finalSection.minItems ?? 1} list item(s)`);
194
+ }
195
+ }
196
+ return errors;
197
+ }
198
+ export function checkDocumentation(root, config) {
199
+ validateDocumentationConfig(config);
200
+ const excluded = config.excludeDirectories ? new Set(config.excludeDirectories) : defaultExcludedDirectories;
201
+ const allFiles = walkFiles(root, excluded);
202
+ const relativeFiles = new Map(allFiles.map((file) => [file, relativePath(root, file)]));
203
+ const documentFiles = allFiles.filter((file) => matches(relativeFiles.get(file), config.files));
204
+ const parsed = new Map(documentFiles.map((file) => [file, inspectMarkdown(file)]));
205
+ const errors = [];
206
+ for (const [file, markdown] of parsed) {
207
+ const display = relativePath(root, file);
208
+ if (config.checks?.singleH1 !== false) {
209
+ const h1Count = markdown.headings.filter((heading) => heading.level === 1).length;
210
+ if (h1Count !== 1)
211
+ errors.push(`${display}: expected exactly one H1; found ${h1Count}`);
212
+ }
213
+ if (config.checks?.headingOrder !== false) {
214
+ for (let index = 1; index < markdown.headings.length; index += 1) {
215
+ const previous = markdown.headings[index - 1];
216
+ const current = markdown.headings[index];
217
+ if (current.level > previous.level + 1)
218
+ errors.push(`${display}:${current.line}: heading jumps from H${previous.level} to H${current.level}`);
219
+ }
220
+ }
221
+ if (config.checks?.localLinks !== false) {
222
+ for (const link of markdown.links) {
223
+ const target = resolveLocalTarget(root, file, link.target);
224
+ if (target && (!isInsideRoot(root, target) || !existsSync(target)))
225
+ errors.push(`${display}:${link.line}: broken or unsafe local link: ${link.target}`);
226
+ }
227
+ }
228
+ }
229
+ const reachability = config.checks?.reachability;
230
+ if (reachability) {
231
+ const roots = documentFiles.filter((file) => matches(relativeFiles.get(file), reachability.entrypoints));
232
+ const reachable = new Set(roots);
233
+ const queue = [...roots];
234
+ while (queue.length > 0) {
235
+ const source = queue.shift();
236
+ for (const link of parsed.get(source).links) {
237
+ const target = resolvedMarkdownTarget(root, source, link.target);
238
+ if (!target || reachable.has(target) || !parsed.has(target))
239
+ continue;
240
+ reachable.add(target);
241
+ queue.push(target);
242
+ }
243
+ }
244
+ for (const file of documentFiles.filter((candidate) => matches(relativeFiles.get(candidate), reachability.files))) {
245
+ if (!reachable.has(file))
246
+ errors.push(`${relativePath(root, file)}: document is unreachable from configured entrypoints`);
247
+ }
248
+ }
249
+ for (const requiredFile of config.requiredFiles ?? []) {
250
+ if (!existsSync(resolve(root, requiredFile)))
251
+ errors.push(`${normalizePath(requiredFile)}: required documentation file is missing`);
252
+ }
253
+ for (const collection of config.collections ?? []) {
254
+ const sources = documentFiles.filter((file) => {
255
+ const display = relativeFiles.get(file);
256
+ return matches(display, collection.files) && !matches(display, collection.exclude ?? []);
257
+ });
258
+ const indexPath = collection.index ? resolve(root, collection.index) : undefined;
259
+ const indexMarkdown = indexPath && existsSync(indexPath) ? inspectMarkdown(indexPath) : undefined;
260
+ if (indexPath && !indexMarkdown)
261
+ errors.push(`${relativePath(root, indexPath)}: collection index is missing`);
262
+ const uniqueValues = new Map();
263
+ const generatedPairs = new Set();
264
+ for (const source of sources) {
265
+ const sourceMarkdown = parsed.get(source);
266
+ const display = relativePath(root, source);
267
+ if (indexPath && indexMarkdown && linkCount(root, indexPath, indexMarkdown, source) !== 1) {
268
+ errors.push(`${display}: expected exactly one link in ${relativePath(root, indexPath)}`);
269
+ }
270
+ for (const [field, rule] of Object.entries(collection.metadata ?? {})) {
271
+ const rawValue = sourceMarkdown.metadata.get(field);
272
+ if (!rawValue) {
273
+ errors.push(`${display}: missing required metadata: ${field}`);
274
+ continue;
275
+ }
276
+ const value = rawValue.replaceAll('`', '').trim();
277
+ if (rule.equals !== undefined && value !== interpolate(rule.equals, source))
278
+ errors.push(`${display}: ${field} must equal ${interpolate(rule.equals, source)}`);
279
+ if (rule.unique) {
280
+ const values = uniqueValues.get(field) ?? new Map();
281
+ const previous = values.get(value);
282
+ if (previous)
283
+ errors.push(`${display}: duplicate ${field} with ${previous}: ${value}`);
284
+ else
285
+ values.set(value, display);
286
+ uniqueValues.set(field, values);
287
+ }
288
+ if (rule.repositoryPaths) {
289
+ const references = [...rawValue.matchAll(/`([^`]+)`/gu)].map((match) => match[1]);
290
+ if (references.length < (rule.minItems ?? 1))
291
+ errors.push(`${display}: ${field} requires at least ${rule.minItems ?? 1} repository path(s)`);
292
+ for (const reference of references) {
293
+ const target = resolve(root, reference);
294
+ if (!isInsideRoot(root, target) || !existsSync(target))
295
+ errors.push(`${display}: missing repository path in ${field}: ${reference}`);
296
+ }
297
+ }
298
+ }
299
+ const pair = collection.pairedDocuments;
300
+ if (!pair)
301
+ continue;
302
+ const target = resolve(root, interpolate(pair.target, source));
303
+ generatedPairs.add(target);
304
+ if (linkCount(root, source, sourceMarkdown, target) !== 1)
305
+ errors.push(`${display}: expected exactly one link to ${relativePath(root, target)}`);
306
+ if (!existsSync(target)) {
307
+ errors.push(`${relativePath(root, target)}: paired document is missing`);
308
+ continue;
309
+ }
310
+ const targetMarkdown = parsed.get(target) ?? inspectMarkdown(target);
311
+ if (pair.index) {
312
+ const pairIndex = resolve(root, pair.index);
313
+ if (!existsSync(pairIndex) || linkCount(root, pairIndex, inspectMarkdown(pairIndex), target) !== 1) {
314
+ errors.push(`${relativePath(root, target)}: expected exactly one link in ${relativePath(root, pairIndex)}`);
315
+ }
316
+ }
317
+ errors.push(...validatePairContent(root, target, targetMarkdown, pair));
318
+ }
319
+ const pair = collection.pairedDocuments;
320
+ if (pair) {
321
+ const pairDirectory = resolve(root, dirname(pair.target));
322
+ const pairIndex = pair.index ? resolve(root, pair.index) : undefined;
323
+ for (const file of documentFiles.filter((candidate) => dirname(candidate) === pairDirectory && candidate !== pairIndex)) {
324
+ if (!generatedPairs.has(file))
325
+ errors.push(`${relativePath(root, file)}: paired document has no source document`);
326
+ }
327
+ }
328
+ }
329
+ for (const inventory of config.inventories ?? []) {
330
+ const inventoryPath = resolve(root, inventory.document);
331
+ if (!existsSync(inventoryPath)) {
332
+ errors.push(`${normalizePath(inventory.document)}: inventory document is missing`);
333
+ continue;
334
+ }
335
+ const sources = allFiles.filter((file) => matches(relativeFiles.get(file), inventory.sources));
336
+ const minMatches = inventory.minMatches ?? 0;
337
+ if (sources.length < minMatches)
338
+ errors.push(`${normalizePath(inventory.document)}: inventory requires at least ${minMatches} source match(es)`);
339
+ const content = readFileSync(inventoryPath, 'utf8');
340
+ for (const source of sources) {
341
+ const display = relativeFiles.get(source);
342
+ if (!content.includes(`\`${display}\``))
343
+ errors.push(`${normalizePath(inventory.document)}: source is not inventoried: ${display}`);
344
+ }
345
+ }
346
+ return errors.sort();
347
+ }
348
+ /* v8 ignore start -- executable adapter */
349
+ async function main() {
350
+ const { config, configPath } = await loadConfig(process.cwd());
351
+ if (!configPath || !config.documentation)
352
+ throw new Error('documentation is not configured in .webtoolkit-cli/config.json.');
353
+ const root = dirname(dirname(configPath));
354
+ const errors = checkDocumentation(root, config.documentation);
355
+ if (errors.length > 0) {
356
+ console.error(errors.join('\n'));
357
+ process.exitCode = 1;
358
+ return;
359
+ }
360
+ console.info('Documentation is valid.');
361
+ }
362
+ /* v8 ignore stop */
363
+ /* v8 ignore start -- executable adapter */
364
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
365
+ main().catch((error) => {
366
+ console.error(error.message);
367
+ process.exitCode = 1;
368
+ });
369
+ }
370
+ /* v8 ignore stop */
@@ -0,0 +1,19 @@
1
+ import type { GuardsConfig } from '../config.js';
2
+ export declare const BASE_ARTIFACT_EXCLUDE_PATTERNS: string[];
3
+ export declare const BASE_SOURCE_EXTENSIONS: Set<string>;
4
+ export declare const BASE_TYPESCRIPT_EXTENSIONS: Set<string>;
5
+ export declare const BASE_JSX_EXTENSIONS: Set<string>;
6
+ export declare const BASE_SOURCE_EXCLUDE_PATTERNS: string[];
7
+ export declare const BASE_LAYER_EXCLUDE_PATTERNS: string[];
8
+ export declare function loadGuardConfig<K extends keyof GuardsConfig>(name: K, cwd?: string): Promise<NonNullable<GuardsConfig[K]>>;
9
+ export declare function compilePatterns(patterns?: string[], basePatterns?: string[]): RegExp[];
10
+ export declare function hasExtension(filePath: string, extensions: Set<string>): boolean;
11
+ export declare function resolveProjectPath(root: string, configuredPath: string): string;
12
+ export declare function assertConfiguredScanScope(options: {
13
+ root: string;
14
+ guardName: string;
15
+ configPath: string;
16
+ configuredPaths: string[];
17
+ eligibleFiles: string[];
18
+ }): void;
19
+ export declare function isMainModule(moduleUrl: string, argv?: string[]): boolean;
@@ -0,0 +1,87 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { loadConfig } from '../config.js';
5
+ export const BASE_ARTIFACT_EXCLUDE_PATTERNS = [
6
+ '(^|[/\\\\])node_modules([/\\\\]|$)',
7
+ '(^|[/\\\\])dist([/\\\\]|$)',
8
+ '(^|[/\\\\])build([/\\\\]|$)',
9
+ '(^|[/\\\\])coverage([/\\\\]|$)',
10
+ ];
11
+ export const BASE_SOURCE_EXTENSIONS = new Set([
12
+ '.cjs',
13
+ '.cts',
14
+ '.js',
15
+ '.jsx',
16
+ '.mjs',
17
+ '.mts',
18
+ '.ts',
19
+ '.tsx',
20
+ ]);
21
+ export const BASE_TYPESCRIPT_EXTENSIONS = new Set(['.cts', '.mts', '.ts', '.tsx']);
22
+ export const BASE_JSX_EXTENSIONS = new Set(['.jsx', '.tsx']);
23
+ export const BASE_SOURCE_EXCLUDE_PATTERNS = [
24
+ ...BASE_ARTIFACT_EXCLUDE_PATTERNS,
25
+ '\\.(test|spec)([.-]|$)',
26
+ '(^|[/\\\\])tests?([/\\\\]|$)',
27
+ '(^|[/\\\\])__tests__([/\\\\]|$)',
28
+ '\\.stories\\.',
29
+ ];
30
+ export const BASE_LAYER_EXCLUDE_PATTERNS = [
31
+ ...BASE_SOURCE_EXCLUDE_PATTERNS,
32
+ '\\.(config|setup)\\.',
33
+ ];
34
+ export async function loadGuardConfig(name, cwd = process.cwd()) {
35
+ const { config, configPath } = await loadConfig(cwd);
36
+ const guardConfig = config.guards?.[name];
37
+ if (!configPath || !guardConfig) {
38
+ throw new Error(`guards.${name} is not configured in .webtoolkit-cli/config.json.`);
39
+ }
40
+ return guardConfig;
41
+ }
42
+ export function compilePatterns(patterns = [], basePatterns = []) {
43
+ return [...basePatterns, ...patterns].map((pattern) => {
44
+ try {
45
+ return new RegExp(pattern);
46
+ }
47
+ catch {
48
+ throw new Error(`Invalid regular expression in guard configuration: ${pattern}`);
49
+ }
50
+ });
51
+ }
52
+ export function hasExtension(filePath, extensions) {
53
+ const extension = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
54
+ return extensions.has(extension);
55
+ }
56
+ export function resolveProjectPath(root, configuredPath) {
57
+ const absolutePath = path.resolve(root, configuredPath);
58
+ const relativePath = path.relative(root, absolutePath);
59
+ if (path.isAbsolute(configuredPath) || relativePath === '..' || relativePath.startsWith(`..${path.sep}`)) {
60
+ throw new Error(`Guard path must stay inside the project: ${configuredPath}`);
61
+ }
62
+ return absolutePath;
63
+ }
64
+ export function assertConfiguredScanScope(options) {
65
+ for (const configuredPath of options.configuredPaths) {
66
+ const absolutePath = resolveProjectPath(options.root, configuredPath);
67
+ let stats;
68
+ try {
69
+ stats = fs.statSync(absolutePath);
70
+ }
71
+ catch (error) {
72
+ if (error.code === 'ENOENT') {
73
+ throw new Error(`${options.guardName}: ${options.configPath} contains a missing directory: ${configuredPath}`);
74
+ }
75
+ throw error;
76
+ }
77
+ if (!stats.isDirectory()) {
78
+ throw new Error(`${options.guardName}: ${options.configPath} must contain directories; received: ${configuredPath}`);
79
+ }
80
+ }
81
+ if (options.eligibleFiles.length === 0) {
82
+ throw new Error(`${options.guardName}: ${options.configPath} matched zero eligible files in: ${options.configuredPaths.join(', ')}`);
83
+ }
84
+ }
85
+ export function isMainModule(moduleUrl, argv = process.argv) {
86
+ return Boolean(argv[1]) && path.resolve(argv[1]) === fileURLToPath(moduleUrl);
87
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Internal Link Guard
4
+ *
5
+ * Prevents internal SPA navigation through raw <a href="...">.
6
+ * For React routes, use <Link to="..."> from react-router-dom.
7
+ */
8
+ import type { PathScanGuardConfig } from '../config.js';
9
+ export declare function runInternalLinkGuard(options?: {
10
+ rootDir?: string;
11
+ config?: PathScanGuardConfig;
12
+ }): Promise<number>;