@starci/eslint-grammar-core 0.15.34 → 0.16.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/index.js +4 -0
  4. package/dist/lib/why.js +15 -0
  5. package/dist/mascot.d.ts +28 -0
  6. package/dist/mascot.js +166 -0
  7. package/dist/surface-depth.d.ts +7 -0
  8. package/dist/surface-depth.js +92 -0
  9. package/docs/mascot.md +60 -0
  10. package/docs/surface-depth.md +40 -0
  11. package/package.json +2 -2
  12. package/runtime/bundle.json +74 -0
  13. package/runtime/engine/by-code-unit.mjs +6 -6
  14. package/runtime/engine/model-config.mjs +98 -76
  15. package/runtime/engine/orca-config.mjs +22 -17
  16. package/runtime/engine/release-config.mjs +22 -18
  17. package/runtime/engine/removed-vocabulary.mjs +21 -20
  18. package/runtime/engine/resources-config.mjs +42 -35
  19. package/runtime/engine/runtime-root.mjs +11 -13
  20. package/runtime/engine/secrets.mjs +170 -102
  21. package/runtime/engine/sonar-config.mjs +16 -13
  22. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  23. package/runtime/knowledge/hfs/rules.yaml +95 -6
  24. package/runtime/knowledge/hfs/slots.yaml +2 -2
  25. package/runtime/knowledge/patterns/fe/ui.yaml +13 -1
  26. package/runtime/scripts/hfs/allows.mjs +37 -38
  27. package/runtime/scripts/hfs/architecture/config.mjs +302 -268
  28. package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
  29. package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
  30. package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
  31. package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
  32. package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
  33. package/runtime/scripts/hfs/edition-slots.mjs +43 -40
  34. package/runtime/scripts/hfs/param-names.mjs +6 -11
  35. package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
  36. package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
  37. package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
  38. package/runtime/scripts/hfs/slot-classify.mjs +42 -38
  39. package/runtime/scripts/hfs/slot-errors.mjs +8 -7
  40. package/runtime/scripts/hfs/slot-imports.mjs +70 -63
  41. package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
  42. package/runtime/scripts/hfs/slot-match.mjs +122 -111
  43. package/runtime/scripts/hfs/slot-path.mjs +0 -2
  44. package/runtime/scripts/hfs/slot-required.mjs +65 -59
  45. package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
  46. package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
  47. package/runtime/scripts/hfs/slots.mjs +175 -152
  48. package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
  49. package/runtime/scripts/hfs/view.mjs +38 -40
  50. package/runtime/scripts/lib/glob.mjs +23 -14
  51. package/runtime/scripts/lib/grammar-lint.mjs +33 -37
  52. package/runtime/scripts/lib/i18n.mjs +32 -35
  53. package/runtime/scripts/lib/json-clone.mjs +45 -36
  54. package/runtime/scripts/lib/json.mjs +26 -14
  55. package/runtime/scripts/lib/list.mjs +9 -14
  56. package/runtime/scripts/lib/path-key.mjs +53 -38
  57. package/runtime/scripts/lib/read-text.mjs +30 -17
  58. package/runtime/scripts/lib/regex.mjs +1 -2
  59. package/runtime/scripts/lib/source-phrases.mjs +13 -13
  60. package/runtime/scripts/lib/ts-analysis.mjs +28 -26
  61. package/runtime/scripts/lib/ts-ast.mjs +36 -42
  62. package/runtime/scripts/lib/walk.mjs +33 -27
  63. package/runtime/scripts/lib/yaml-cached.mjs +4 -4
@@ -10,103 +10,106 @@ import { readJsonFile } from '../../lib/json.mjs';
10
10
  import { sourceLocation } from '../../lib/ts-ast.mjs';
11
11
  import { locateDeclaration } from '../slots.mjs';
12
12
  export { unwrapExpression } from '../../lib/ts-analysis.mjs';
13
-
14
13
  const CODE_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/i;
15
14
  const SCRIPT_TAIL = '[cm]?[jt]sx?';
16
- const TEST_FILE = new RegExp(String.raw`(?:^|[.-])(?:spec|test)\.${SCRIPT_TAIL}$`, 'i');
15
+ const TEST_FILE = new RegExp(String.raw `(?:^|[.-])(?:spec|test)\.${SCRIPT_TAIL}$`, 'i');
17
16
  const ASSET_EXTENSION_NAMES = 'css|scss|sass|less|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|eot|ya?ml|json';
18
- const ASSET_EXTENSION = new RegExp(String.raw`\.(?:${ASSET_EXTENSION_NAMES})$`, 'i');
17
+ const ASSET_EXTENSION = new RegExp(String.raw `\.(?:${ASSET_EXTENSION_NAMES})$`, 'i');
19
18
  // Framework build output a tsconfig may include (Next writes `.next/types/**/*.ts` back into tsconfig.json on
20
19
  // every build) is compiled for type resolution but is never source: it is gitignored, regenerated, and no
21
20
  // canon or architecture rule applies to it (inc-ffe60c49f502).
22
21
  const GENERATED_SEGMENTS = new Set(['.next', '.turbo', '.vercel', '.output', '.nuxt', '.svelte-kit', '.expo', '.docusaurus', '.swc', '.cache']);
23
22
  function isGeneratedPath(root, fileName) {
24
- return slash(path.relative(root, fileName)).split('/').slice(0, -1).some(segment => GENERATED_SEGMENTS.has(segment));
23
+ return slash(path.relative(root, fileName)).split('/').slice(0, -1).some((segment) => GENERATED_SEGMENTS.has(segment));
25
24
  }
26
25
  function diagnosticMessage(ts, diagnostic) {
27
- return ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
26
+ return ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
28
27
  }
29
-
30
28
  function compilerError(ts, root, diagnostic, project, ruleId = 'ARCH_TSCONFIG_INVALID') {
31
- const fileName = diagnostic.file?.fileName;
32
- let line;
33
- let column;
34
- if (diagnostic.file && Number.isInteger(diagnostic.start)) {
35
- const point = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
36
- line = point.line + 1;
37
- column = point.character + 1;
38
- }
39
- return {
40
- ruleId,
41
- project,
42
- ...(fileName && isInside(root, fileName) ? { path: slash(path.relative(root, fileName)) } : {}),
43
- ...(line ? { line, column } : {}),
44
- message: diagnosticMessage(ts, diagnostic),
45
- };
46
- }
47
-
29
+ const fileName = diagnostic.file?.fileName;
30
+ let line;
31
+ let column;
32
+ if (diagnostic.file && diagnostic.start !== undefined && Number.isInteger(diagnostic.start)) {
33
+ const point = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
34
+ line = point.line + 1;
35
+ column = point.character + 1;
36
+ }
37
+ return {
38
+ ruleId,
39
+ project,
40
+ ...(fileName && isInside(root, fileName) ? { path: slash(path.relative(root, fileName)) } : {}),
41
+ ...(line ? { line, column } : {}),
42
+ message: diagnosticMessage(ts, diagnostic),
43
+ };
44
+ }
48
45
  /**
49
46
  * Load TypeScript through the target package boundary, never through StarCi's own dependency graph. A side folder of an app has no
50
47
  * package.json of its own: the app root's one manifest is the boundary its TypeScript is installed under.
51
48
  */
52
49
  function loadTargetTypeScript(repositoryRoot) {
53
- const packageFile = path.join(locateDeclaration(repositoryRoot).appRoot, 'package.json');
54
- if (!fs.existsSync(packageFile)) throw new Error('ARCH_TYPESCRIPT_MISSING: target package.json is required to resolve target-installed TypeScript.');
55
- const targetRequire = createRequire(packageFile);
56
- let resolved;
57
- let ts;
58
- try {
59
- resolved = targetRequire.resolve('typescript');
60
- ts = targetRequire('typescript');
61
- } catch {
62
- throw new Error('ARCH_TYPESCRIPT_MISSING: install TypeScript in the checked repository; StarCi does not substitute its own parser.');
63
- }
64
- if (!ts?.createProgram || !ts?.resolveModuleName || !ts?.readConfigFile) {
65
- throw new Error('ARCH_TYPESCRIPT_INVALID: the target TypeScript package does not expose the compiler API.');
66
- }
67
- return { ts, resolved, version: String(ts.version ?? 'unknown') };
68
- }
69
-
50
+ const packageFile = path.join(locateDeclaration(repositoryRoot).appRoot, 'package.json');
51
+ if (!fs.existsSync(packageFile))
52
+ throw new Error('ARCH_TYPESCRIPT_MISSING: target package.json is required to resolve target-installed TypeScript.');
53
+ const targetRequire = createRequire(packageFile);
54
+ let resolved;
55
+ let ts;
56
+ try {
57
+ resolved = targetRequire.resolve('typescript');
58
+ ts = targetRequire('typescript');
59
+ }
60
+ catch {
61
+ throw new Error('ARCH_TYPESCRIPT_MISSING: install TypeScript in the checked repository; StarCi does not substitute its own parser.');
62
+ }
63
+ if (!ts?.createProgram || !ts?.resolveModuleName || !ts?.readConfigFile) {
64
+ throw new Error('ARCH_TYPESCRIPT_INVALID: the target TypeScript package does not expose the compiler API.');
65
+ }
66
+ return { ts, resolved, version: String(ts.version ?? 'unknown') };
67
+ }
70
68
  const readJson = (file) => readJsonFile(file, {});
71
-
72
69
  function pathAliasMatches(specifier, paths = {}) {
73
- return Object.keys(paths).some(pattern => {
74
- if (!pattern.includes('*')) return pattern === specifier;
75
- const [before, after] = pattern.split('*');
76
- return specifier.startsWith(before) && specifier.endsWith(after ?? '');
77
- });
70
+ return Object.keys(paths).some(pattern => {
71
+ if (!pattern.includes('*'))
72
+ return pattern === specifier;
73
+ const [before = '', after] = pattern.split('*');
74
+ return specifier.startsWith(before) && specifier.endsWith(after ?? '');
75
+ });
78
76
  }
79
-
80
77
  function importDeclarationRuntime(ts, node) {
81
- const clause = node.importClause;
82
- if (!clause) return true;
83
- if (clause.isTypeOnly) return false;
84
- if (clause.name) return true;
85
- const named = clause.namedBindings;
86
- if (named && ts.isNamedImports(named)) return named.elements.some(element => !element.isTypeOnly);
87
- return Boolean(named);
88
- }
89
-
78
+ const clause = node.importClause;
79
+ if (!clause)
80
+ return true;
81
+ if (clause.isTypeOnly)
82
+ return false;
83
+ if (clause.name)
84
+ return true;
85
+ const named = clause.namedBindings;
86
+ if (named && ts.isNamedImports(named))
87
+ return named.elements.some(element => !element.isTypeOnly);
88
+ return Boolean(named);
89
+ }
90
90
  function exportDeclarationRuntime(ts, node) {
91
- if (node.isTypeOnly) return false;
92
- if (node.exportClause && ts.isNamedExports(node.exportClause)) return node.exportClause.elements.some(element => !element.isTypeOnly);
93
- return true;
91
+ if (node.isTypeOnly)
92
+ return false;
93
+ if (node.exportClause && ts.isNamedExports(node.exportClause))
94
+ return node.exportClause.elements.some(element => !element.isTypeOnly);
95
+ return true;
94
96
  }
95
-
96
97
  function runtimeImport(ts, node) {
97
- if (ts.isImportDeclaration(node)) return importDeclarationRuntime(ts, node);
98
- if (ts.isExportDeclaration(node)) return exportDeclarationRuntime(ts, node);
99
- return true;
98
+ if (ts.isImportDeclaration(node))
99
+ return importDeclarationRuntime(ts, node);
100
+ if (ts.isExportDeclaration(node))
101
+ return exportDeclarationRuntime(ts, node);
102
+ return true;
100
103
  }
101
-
102
104
  export function isUnshadowedCommonJsRequire(ts, checker, expression) {
103
- if (!ts.isIdentifier(expression) || expression.text !== 'require') return false;
104
- const symbol = checker?.getSymbolAtLocation(expression);
105
- if (!symbol) return true;
106
- const declarations = symbol.getDeclarations?.() ?? [];
107
- return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile);
105
+ if (!ts.isIdentifier(expression) || expression.text !== 'require')
106
+ return false;
107
+ const symbol = checker?.getSymbolAtLocation(expression);
108
+ if (!symbol)
109
+ return true;
110
+ const declarations = symbol.getDeclarations?.() ?? [];
111
+ return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile);
108
112
  }
109
-
110
113
  /**
111
114
  * The files a template-literal import of data can load. import(`../messages/${locale}.json`) - the
112
115
  * next-intl request config - is bundled as a context of every file under ../messages whose name ends in
@@ -116,302 +119,339 @@ export function isUnshadowedCommonJsRequire(ts, checker, expression) {
116
119
  */
117
120
  /** The data-asset extension tail and relative head of an import template, or null when the template cannot name a data context. */
118
121
  function assetContextParts(ts, argument) {
119
- if (!argument || !ts.isTemplateExpression(argument)) return null;
120
- const head = argument.head.text;
121
- const spans = argument.templateSpans;
122
- const tail = spans[spans.length - 1].literal.text;
123
- if (!/^\.\.?\//.test(head) || !head.endsWith('/') || !/^\.[a-z0-9]+$/i.test(tail) || !ASSET_EXTENSION.test(tail)) return null;
124
- if (spans.slice(0, -1).some(span => span.literal.text.split('/').includes('..'))) return null;
125
- return { head, tail };
126
- }
127
-
122
+ if (!argument || !ts.isTemplateExpression(argument))
123
+ return null;
124
+ const head = argument.head.text;
125
+ const spans = argument.templateSpans;
126
+ const tail = spans[spans.length - 1]?.literal.text ?? '';
127
+ if (!/^\.\.?\//.test(head) || !head.endsWith('/') || !/^\.[a-z0-9]+$/i.test(tail) || !ASSET_EXTENSION.test(tail))
128
+ return null;
129
+ if (spans.slice(0, -1).some(span => span.literal.text.split('/').includes('..')))
130
+ return null;
131
+ return { head, tail };
132
+ }
128
133
  /** The specifiers of every file below `directory` (node_modules skipped) whose name ends in `tail`, sorted by name at each level. */
129
134
  function assetFileSpecifiers(directory, head, tail) {
130
- const specifiers = [];
131
- const visit = (dir) => {
132
- for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
133
- if (entry.name === 'node_modules') continue;
134
- const absolute = path.join(dir, entry.name);
135
- if (entry.isDirectory()) visit(absolute);
136
- else if (entry.isFile() && entry.name.toLowerCase().endsWith(tail.toLowerCase())) specifiers.push(`${head}${slash(path.relative(directory, absolute))}`);
137
- }
138
- };
139
- visit(directory);
140
- return specifiers;
135
+ const specifiers = [];
136
+ const visit = (dir) => {
137
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
138
+ if (entry.name === 'node_modules')
139
+ continue;
140
+ const absolute = path.join(dir, entry.name);
141
+ if (entry.isDirectory())
142
+ visit(absolute);
143
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith(tail.toLowerCase()))
144
+ specifiers.push(`${head}${slash(path.relative(directory, absolute))}`);
145
+ }
146
+ };
147
+ visit(directory);
148
+ return specifiers;
141
149
  }
142
-
143
150
  function assetContextSpecifiers(ts, sourceFile, argument) {
144
- const parts = assetContextParts(ts, argument);
145
- if (!parts) return null;
146
- const directory = path.resolve(path.dirname(sourceFile.fileName), parts.head);
147
- let stat;
148
- try { stat = fs.statSync(directory); } catch { return null; }
149
- if (!stat.isDirectory()) return null;
150
- const specifiers = assetFileSpecifiers(directory, parts.head, parts.tail);
151
- return specifiers.length ? specifiers : null;
152
- }
153
-
151
+ const parts = assetContextParts(ts, argument);
152
+ if (!parts)
153
+ return null;
154
+ const directory = path.resolve(path.dirname(sourceFile.fileName), parts.head);
155
+ let stat;
156
+ try {
157
+ stat = fs.statSync(directory);
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ if (!stat.isDirectory())
163
+ return null;
164
+ const specifiers = assetFileSpecifiers(directory, parts.head, parts.tail);
165
+ return specifiers.length ? specifiers : null;
166
+ }
154
167
  function staticModuleReference(ts, node, found, unproven) {
155
- if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
156
- found.push({ node: node.moduleSpecifier, specifier: node.moduleSpecifier.text, runtime: runtimeImport(ts, node), declaration: node });
157
- return true;
158
- } else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)
159
- && node.moduleReference.expression && ts.isStringLiteralLike(node.moduleReference.expression)) {
160
- found.push({ node: node.moduleReference.expression, specifier: node.moduleReference.expression.text, runtime: !node.isTypeOnly, declaration: node });
161
- return true;
162
- } else if (ts.isImportTypeNode(node)) {
163
- const literal = ts.isLiteralTypeNode(node.argument) && ts.isStringLiteralLike(node.argument.literal) ? node.argument.literal : null;
164
- if (literal) found.push({ node: literal, specifier: literal.text, runtime: false, declaration: node });
165
- else unproven.push({ node, kind: 'TypeScript import type' });
166
- return true;
167
- }
168
- return false;
168
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
169
+ found.push({ node: node.moduleSpecifier, specifier: node.moduleSpecifier.text, runtime: runtimeImport(ts, node), declaration: node });
170
+ return true;
171
+ }
172
+ else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)
173
+ && node.moduleReference.expression && ts.isStringLiteralLike(node.moduleReference.expression)) {
174
+ found.push({ node: node.moduleReference.expression, specifier: node.moduleReference.expression.text, runtime: !node.isTypeOnly, declaration: node });
175
+ return true;
176
+ }
177
+ else if (ts.isImportTypeNode(node)) {
178
+ const literal = ts.isLiteralTypeNode(node.argument) && ts.isStringLiteralLike(node.argument.literal) ? node.argument.literal : null;
179
+ if (literal)
180
+ found.push({ node: literal, specifier: literal.text, runtime: false, declaration: node });
181
+ else
182
+ unproven.push({ node, kind: 'TypeScript import type' });
183
+ return true;
184
+ }
185
+ return false;
169
186
  }
170
-
171
187
  function dynamicImportReference(ts, sourceFile, node, found, unproven) {
172
- const context = [1, 2].includes(node.arguments.length) ? assetContextSpecifiers(ts, sourceFile, node.arguments[0]) : null;
173
- if ([1, 2].includes(node.arguments.length) && ts.isStringLiteralLike(node.arguments[0])) {
174
- found.push({ node: node.arguments[0], specifier: node.arguments[0].text, runtime: true, declaration: node });
175
- } else if (context) {
176
- for (const specifier of context) found.push({ node: node.arguments[0], specifier, runtime: true, declaration: node });
177
- } else unproven.push({ node, kind: 'dynamic import()' });
178
- }
179
-
180
- function requireReference(ts, checker, node, found, unproven) {
181
- if (node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0])) {
182
- found.push({ node: node.arguments[0], specifier: node.arguments[0].text, runtime: true, declaration: node });
183
- } else unproven.push({ node, kind: 'dynamic require()' });
184
- }
185
-
188
+ const first = node.arguments[0];
189
+ const context = [1, 2].includes(node.arguments.length) ? assetContextSpecifiers(ts, sourceFile, first) : null;
190
+ if ([1, 2].includes(node.arguments.length) && first && ts.isStringLiteralLike(first)) {
191
+ found.push({ node: first, specifier: first.text, runtime: true, declaration: node });
192
+ }
193
+ else if (context && first) {
194
+ for (const specifier of context)
195
+ found.push({ node: first, specifier, runtime: true, declaration: node });
196
+ }
197
+ else
198
+ unproven.push({ node, kind: 'dynamic import()' });
199
+ }
200
+ function requireReference(ts, _checker, node, found, unproven) {
201
+ const first = node.arguments[0];
202
+ if (node.arguments.length === 1 && first && ts.isStringLiteralLike(first)) {
203
+ found.push({ node: first, specifier: first.text, runtime: true, declaration: node });
204
+ }
205
+ else
206
+ unproven.push({ node, kind: 'dynamic require()' });
207
+ }
186
208
  function callModuleReference(ts, sourceFile, checker, node, found, unproven) {
187
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) dynamicImportReference(ts, sourceFile, node, found, unproven);
188
- else if (ts.isCallExpression(node) && isUnshadowedCommonJsRequire(ts, checker, node.expression)) requireReference(ts, checker, node, found, unproven);
209
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword)
210
+ dynamicImportReference(ts, sourceFile, node, found, unproven);
211
+ else if (ts.isCallExpression(node) && isUnshadowedCommonJsRequire(ts, checker, node.expression))
212
+ requireReference(ts, checker, node, found, unproven);
189
213
  }
190
-
191
214
  function moduleReferences(ts, sourceFile, checker) {
192
- const found = [];
193
- const unproven = [];
194
- const visit = node => {
195
- if (!staticModuleReference(ts, node, found, unproven)) callModuleReference(ts, sourceFile, checker, node, found, unproven);
196
- ts.forEachChild(node, visit);
197
- };
198
- visit(sourceFile);
199
- return { found, unproven };
200
- }
201
-
215
+ const found = [];
216
+ const unproven = [];
217
+ const visit = (node) => {
218
+ if (!staticModuleReference(ts, node, found, unproven))
219
+ callModuleReference(ts, sourceFile, checker, node, found, unproven);
220
+ ts.forEachChild(node, visit);
221
+ };
222
+ visit(sourceFile);
223
+ return { found, unproven };
224
+ }
202
225
  function isProductionSource(root, sourceFile) {
203
- return isInside(root, sourceFile.fileName)
204
- && CODE_EXTENSIONS.test(sourceFile.fileName)
205
- && !sourceFile.isDeclarationFile
206
- && !TEST_FILE.test(sourceFile.fileName)
207
- && !slash(sourceFile.fileName).includes('/node_modules/')
208
- && !isGeneratedPath(root, sourceFile.fileName);
209
- }
210
-
226
+ return isInside(root, sourceFile.fileName)
227
+ && CODE_EXTENSIONS.test(sourceFile.fileName)
228
+ && !sourceFile.isDeclarationFile
229
+ && !TEST_FILE.test(sourceFile.fileName)
230
+ && !slash(sourceFile.fileName).includes('/node_modules/')
231
+ && !isGeneratedPath(root, sourceFile.fileName);
232
+ }
211
233
  function workspaceMetadata(config) {
212
- const backendAppRoots = config.backend.apps.map(item => path.join(config.root, ...item.split('/')));
213
- const create = relative => {
214
- const root = path.join(config.root, ...relative.split('/'));
215
- const pkg = readJson(path.join(root, 'package.json'));
216
- const routeRoots = config.frontend.routes.map(item => path.join(config.root, ...item.split('/')));
217
- return { root: canonical(root), relative, name: typeof pkg.name === 'string' ? pkg.name : null, exports: pkg.exports, manifest: pkg, workspace: true,
218
- app: routeRoots.some(route => isInside(root, route))
219
- || backendAppRoots.some(appRoot => isInside(appRoot, root) || isInside(root, appRoot)) };
220
- };
221
- const roots = config.workspaces.map(create);
222
- // A side of an app has no package.json: the app root's one manifest names the package the side's root files belong to.
223
- const rootPackage = readJson(path.join(config.packageRoot ?? config.root, 'package.json')) ?? {};
224
- roots.push({ root: canonical(config.root), relative: '.', name: typeof rootPackage.name === 'string' ? rootPackage.name : null, exports: rootPackage.exports,
225
- app: config.frontend.routes.some(item => isInside(config.root, path.join(config.root, ...item.split('/')))) || config.kinds.includes('backend') });
226
- return roots.sort((a, b) => b.root.length - a.root.length);
227
- }
228
-
234
+ const backendAppRoots = config.backend.apps.map(item => path.join(config.root, ...item.split('/')));
235
+ const create = (relative) => {
236
+ const root = path.join(config.root, ...relative.split('/'));
237
+ const pkg = readJson(path.join(root, 'package.json'));
238
+ const routeRoots = config.frontend.routes.map(item => path.join(config.root, ...item.split('/')));
239
+ return { root: canonical(root), relative, name: typeof pkg.name === 'string' ? pkg.name : null, exports: pkg.exports, manifest: pkg, workspace: true,
240
+ app: routeRoots.some(route => isInside(root, route))
241
+ || backendAppRoots.some(appRoot => isInside(appRoot, root) || isInside(root, appRoot)) };
242
+ };
243
+ const roots = config.workspaces.map(create);
244
+ // A side of an app has no package.json: the app root's one manifest names the package the side's root files belong to.
245
+ const rootPackage = readJson(path.join(config.packageRoot ?? config.root, 'package.json')) ?? {};
246
+ roots.push({ root: canonical(config.root), relative: '.', name: typeof rootPackage.name === 'string' ? rootPackage.name : null, exports: rootPackage.exports,
247
+ app: config.frontend.routes.some(item => isInside(config.root, path.join(config.root, ...item.split('/')))) || config.kinds.includes('backend') });
248
+ return roots.sort((a, b) => b.root.length - a.root.length);
249
+ }
229
250
  function workspaceOf(workspaces, file) {
230
- return workspaces.find(workspace => isInside(workspace.root, file)) ?? null;
251
+ return workspaces.find(workspace => isInside(workspace.root, file)) ?? null;
231
252
  }
232
-
233
253
  function exportPatternCapture(pattern, request) {
234
- if (!pattern.includes('*')) return pattern === request ? '' : null;
235
- const [before, after = ''] = pattern.split('*');
236
- return request.startsWith(before) && request.endsWith(after) ? request.slice(before.length, request.length - after.length) : null;
254
+ if (!pattern.includes('*'))
255
+ return pattern === request ? '' : null;
256
+ const [before = '', after = ''] = pattern.split('*');
257
+ return request.startsWith(before) && request.endsWith(after) ? request.slice(before.length, request.length - after.length) : null;
237
258
  }
238
-
239
259
  export function exportTargetStrings(value) {
240
- if (typeof value === 'string') return [value];
241
- if (Array.isArray(value)) return value.flatMap(exportTargetStrings);
242
- if (value && typeof value === 'object' && !Object.keys(value).some(key => key.startsWith('.'))) return Object.values(value).flatMap(exportTargetStrings);
243
- return [];
260
+ if (typeof value === 'string')
261
+ return [value];
262
+ if (Array.isArray(value))
263
+ return value.flatMap(exportTargetStrings);
264
+ if (value && typeof value === 'object' && !Object.keys(value).some(key => key.startsWith('.')))
265
+ return Object.values(value).flatMap(exportTargetStrings);
266
+ return [];
244
267
  }
245
-
246
268
  /** The package-relative export targets a request (`.` or `./sub`) of a workspace package declares, from exports, else types/main. */
247
269
  function rootExportCandidates(declaration, request) {
248
- if (request !== '.') return [];
249
- return exportTargetStrings(declaration);
270
+ if (request !== '.')
271
+ return [];
272
+ return exportTargetStrings(declaration);
250
273
  }
251
-
252
274
  function exportMapCandidates(declaration, request) {
253
- const keys = Object.keys(declaration);
254
- if (!keys.some(key => key.startsWith('.'))) return rootExportCandidates(declaration, request);
255
- const candidates = [];
256
- for (const key of keys) {
257
- const capture = exportPatternCapture(key, request);
258
- if (capture !== null) candidates.push(...exportTargetStrings(declaration[key]).map(target => target.replaceAll('*', capture)));
259
- }
260
- return candidates;
261
- }
262
-
275
+ const keys = Object.keys(declaration);
276
+ if (!keys.some(key => key.startsWith('.')))
277
+ return rootExportCandidates(declaration, request);
278
+ const candidates = [];
279
+ for (const key of keys) {
280
+ const capture = exportPatternCapture(key, request);
281
+ if (capture !== null)
282
+ candidates.push(...exportTargetStrings(declaration[key]).map(target => target.replaceAll('*', capture)));
283
+ }
284
+ return candidates;
285
+ }
263
286
  function exportCandidates(workspace, specifier) {
264
- if (!workspace.name || !sameOrUnder(specifier, workspace.name)) return [];
265
- const request = specifier === workspace.name ? '.' : `.${specifier.slice(workspace.name.length)}`;
266
- const declaration = workspace.exports;
267
- if (typeof declaration === 'string' || Array.isArray(declaration)) return rootExportCandidates(declaration, request);
268
- if (declaration && typeof declaration === 'object') return exportMapCandidates(declaration, request);
269
- return [];
270
- }
271
-
287
+ if (!workspace.name || !sameOrUnder(specifier, workspace.name))
288
+ return [];
289
+ const request = specifier === workspace.name ? '.' : `.${specifier.slice(workspace.name.length)}`;
290
+ const declaration = workspace.exports;
291
+ if (typeof declaration === 'string' || Array.isArray(declaration))
292
+ return rootExportCandidates(declaration, request);
293
+ if (declaration && typeof declaration === 'object')
294
+ return exportMapCandidates(declaration, request);
295
+ return [];
296
+ }
272
297
  const SOURCE_EXTENSIONS = { '.d.ts': ['.ts', '.tsx'], '.d.mts': ['.mts'], '.d.cts': ['.cts'], '.js': ['.ts', '.tsx'], '.jsx': ['.tsx'], '.mjs': ['.mts'], '.cjs': ['.cts'] };
273
-
274
298
  /** rootDir and outDir of a workspace package's build tsconfig (tsconfig.build.json, else tsconfig.json), extends followed; null when it declares none. */
275
299
  function workspaceBuildLayout(ts, workspace) {
276
- if (workspace.layout !== undefined) return workspace.layout;
277
- workspace.layout = null;
278
- for (const name of ['tsconfig.build.json', 'tsconfig.json']) {
279
- const file = path.join(workspace.root, name);
280
- if (!fs.existsSync(file)) continue;
281
- const read = ts.readConfigFile(file, ts.sys.readFile);
282
- if (read.error) continue;
283
- const { options } = ts.parseJsonConfigFileContent(read.config, ts.sys, workspace.root, undefined, file);
284
- if (options.outDir) { workspace.layout = { rootDir: options.rootDir ? path.resolve(options.rootDir) : path.join(workspace.root, 'src'), outDir: path.resolve(options.outDir) }; break; }
285
- }
286
- return workspace.layout;
287
- }
288
-
300
+ if (workspace.layout !== undefined)
301
+ return workspace.layout;
302
+ workspace.layout = null;
303
+ for (const name of ['tsconfig.build.json', 'tsconfig.json']) {
304
+ const file = path.join(workspace.root, name);
305
+ if (!fs.existsSync(file))
306
+ continue;
307
+ const read = ts.readConfigFile(file, ts.sys.readFile);
308
+ if (read.error)
309
+ continue;
310
+ const { options } = ts.parseJsonConfigFileContent(read.config, ts.sys, workspace.root, undefined, file);
311
+ if (options.outDir) {
312
+ workspace.layout = { rootDir: options.rootDir ? path.resolve(options.rootDir) : path.join(workspace.root, 'src'), outDir: path.resolve(options.outDir) };
313
+ break;
314
+ }
315
+ }
316
+ return workspace.layout ?? null;
317
+ }
289
318
  /** The source file a built export target (`./dist/sub/index.js`, `.d.ts`) stands for: outDir back to rootDir, else the dist-to-src layout; null when none exists. */
290
319
  function sourceOfTarget(ts, workspace, target) {
291
- const absolute = path.resolve(workspace.root, target);
292
- const layout = workspaceBuildLayout(ts, workspace);
293
- const extension = Object.keys(SOURCE_EXTENSIONS).filter(item => absolute.endsWith(item)).sort((a, b) => b.length - a.length)[0];
294
- const sourceRoots = [];
295
- if (layout && isInside(layout.outDir, absolute)) sourceRoots.push([layout.outDir, layout.rootDir]);
296
- const first = slash(path.relative(workspace.root, absolute)).split('/')[0];
297
- if (first) sourceRoots.push([path.join(workspace.root, first), path.join(workspace.root, 'src')]);
298
- if (!extension) return null;
299
- for (const [from, to] of sourceRoots) {
300
- const stem = path.join(to, path.relative(from, absolute)).slice(0, -extension.length);
301
- for (const candidate of SOURCE_EXTENSIONS[extension]) if (fs.existsSync(stem + candidate)) return canonical(stem + candidate);
302
- }
303
- return null;
304
- }
305
-
306
- const manifestEntryTargets = manifest => [manifest?.types, manifest?.typings, manifest?.module, manifest?.main].filter(item => typeof item === 'string');
307
-
308
- const packageIndexSource = workspace => {
309
- for (const name of ['index.ts', 'index.tsx']) if (fs.existsSync(path.join(workspace.root, 'src', name))) return canonical(path.join(workspace.root, 'src', name));
310
- return null;
320
+ const absolute = path.resolve(workspace.root, target);
321
+ const layout = workspaceBuildLayout(ts, workspace);
322
+ const extension = Object.keys(SOURCE_EXTENSIONS).filter(item => absolute.endsWith(item)).sort((a, b) => b.length - a.length)[0];
323
+ const sourceRoots = [];
324
+ if (layout && isInside(layout.outDir, absolute))
325
+ sourceRoots.push([layout.outDir, layout.rootDir]);
326
+ const first = slash(path.relative(workspace.root, absolute)).split('/')[0];
327
+ if (first)
328
+ sourceRoots.push([path.join(workspace.root, first), path.join(workspace.root, 'src')]);
329
+ if (!extension)
330
+ return null;
331
+ for (const [from, to] of sourceRoots) {
332
+ const stem = path.join(to, path.relative(from, absolute)).slice(0, -extension.length);
333
+ for (const candidate of SOURCE_EXTENSIONS[extension] ?? [])
334
+ if (fs.existsSync(stem + candidate))
335
+ return canonical(stem + candidate);
336
+ }
337
+ return null;
338
+ }
339
+ const manifestEntryTargets = (manifest) => [manifest?.types, manifest?.typings, manifest?.module, manifest?.main].filter((item) => typeof item === 'string');
340
+ const packageIndexSource = (workspace) => {
341
+ for (const name of ['index.ts', 'index.tsx'])
342
+ if (fs.existsSync(path.join(workspace.root, 'src', name)))
343
+ return canonical(path.join(workspace.root, 'src', name));
344
+ return null;
311
345
  };
312
-
313
346
  /** The source entry a workspace import resolves to without a build: its export targets mapped back to source, `src/index.ts` for the package root. */
314
347
  export function workspaceSourceEntry(ts, workspace, specifier) {
315
- if (!workspace.workspace || !workspace.name || !sameOrUnder(specifier, workspace.name)) return null;
316
- const isRoot = specifier === workspace.name;
317
- const candidates = isRoot && !workspace.exports ? manifestEntryTargets(workspace.manifest) : exportCandidates(workspace, specifier);
318
- for (const target of candidates) {
319
- if (!target.startsWith('./') || target.includes('..')) continue;
320
- const source = sourceOfTarget(ts, workspace, target);
321
- if (source) return source;
322
- }
323
- return isRoot ? packageIndexSource(workspace) : null;
324
- }
325
-
348
+ if (!workspace.workspace || !workspace.name || !sameOrUnder(specifier, workspace.name))
349
+ return null;
350
+ const isRoot = specifier === workspace.name;
351
+ const candidates = isRoot && !workspace.exports ? manifestEntryTargets(workspace.manifest) : exportCandidates(workspace, specifier);
352
+ for (const target of candidates) {
353
+ if (!target.startsWith('./') || target.includes('..'))
354
+ continue;
355
+ const source = sourceOfTarget(ts, workspace, target);
356
+ if (source)
357
+ return source;
358
+ }
359
+ return isRoot ? packageIndexSource(workspace) : null;
360
+ }
326
361
  /** Every source file the package.json `exports` of a workspace package maps (each non-wildcard subpath, dist back to source): its public entries. */
327
362
  export function workspaceExportSources(ts, workspace) {
328
- const declaration = workspace.exports;
329
- if (!declaration || typeof declaration !== 'object' || Array.isArray(declaration)) return [];
330
- const sources = new Set();
331
- for (const [key, value] of Object.entries(declaration)) {
332
- if (!key.startsWith('.') || key.includes('*')) continue;
333
- for (const target of exportTargetStrings(value)) {
334
- if (!target.startsWith('./') || target.includes('..')) continue;
335
- const source = sourceOfTarget(ts, workspace, target);
336
- if (source) sources.add(source);
363
+ const declaration = workspace.exports;
364
+ if (!declaration || typeof declaration !== 'object' || Array.isArray(declaration))
365
+ return [];
366
+ const sources = new Set();
367
+ for (const [key, value] of Object.entries(declaration)) {
368
+ if (!key.startsWith('.') || key.includes('*'))
369
+ continue;
370
+ for (const target of exportTargetStrings(value)) {
371
+ if (!target.startsWith('./') || target.includes('..'))
372
+ continue;
373
+ const source = sourceOfTarget(ts, workspace, target);
374
+ if (source)
375
+ sources.add(source);
376
+ }
337
377
  }
338
- }
339
- return [...sources];
378
+ return [...sources];
340
379
  }
341
-
342
380
  function packageExported(ts, workspace, specifier, actualTarget) {
343
- return exportCandidates(workspace, specifier).some(target => {
344
- if (!target.startsWith('./') || target.includes('..')) return false;
345
- const expected = canonical(path.resolve(workspace.root, target));
346
- return expected === canonical(actualTarget) || sourceOfTarget(ts, workspace, target) === canonical(actualTarget);
347
- });
381
+ return exportCandidates(workspace, specifier).some(target => {
382
+ if (!target.startsWith('./') || target.includes('..'))
383
+ return false;
384
+ const expected = canonical(path.resolve(workspace.root, target));
385
+ return expected === canonical(actualTarget) || sourceOfTarget(ts, workspace, target) === canonical(actualTarget);
386
+ });
348
387
  }
349
-
350
388
  function boundaryViolation(root, edge, fromWorkspace, toWorkspace, ruleId, message) {
351
- return {
352
- ruleId,
353
- path: relativePath(root, edge.from),
354
- line: edge.line,
355
- column: edge.column,
356
- specifier: edge.specifier,
357
- resolvedPath: relativePath(root, edge.to),
358
- message,
359
- fromPackage: fromWorkspace?.name ?? fromWorkspace?.relative,
360
- toPackage: toWorkspace?.name ?? toWorkspace?.relative,
361
- };
362
- }
363
-
389
+ return {
390
+ ruleId,
391
+ path: relativePath(root, edge.from),
392
+ line: edge.line,
393
+ column: edge.column,
394
+ specifier: edge.specifier,
395
+ resolvedPath: relativePath(root, edge.to),
396
+ message,
397
+ fromPackage: fromWorkspace?.name ?? fromWorkspace?.relative,
398
+ toPackage: toWorkspace?.name ?? toWorkspace?.relative,
399
+ };
400
+ }
364
401
  /** Parse configured tsconfigs, then build only selected programs and their project-reference closure. */
365
402
  export function buildTypeScriptContext(config, injectedTypeScript, paths = []) {
366
- const loaded = injectedTypeScript ? { ts: injectedTypeScript, resolved: '(injected test compiler)', version: String(injectedTypeScript.version) }
367
- : loadTargetTypeScript(config.root);
368
- // config.hfs is the opened slot resolver (functions), so it never keys a shared value: the repository declaration it was opened
369
- // from (plain data) identifies it, the manifest being the runtime's one.
370
- const context = sharedInProgramRun('architecture-context', loaded.ts, { config: { ...config, hfs: config.hfs?.repo ?? null }, paths }, () => typeScriptContext(config, loaded, paths));
371
- // Callers add to and sort the errors: each gets its own list.
372
- return { ...context, errors: [...context.errors] };
373
- }
374
-
403
+ const loaded = injectedTypeScript ? { ts: injectedTypeScript, resolved: '(injected test compiler)', version: String(injectedTypeScript.version) }
404
+ : loadTargetTypeScript(config.root);
405
+ // config.hfs is the opened slot resolver (functions), so it never keys a shared value: the repository declaration it was opened
406
+ // from (plain data) identifies it, the manifest being the runtime's one.
407
+ const context = sharedInProgramRun('architecture-context', loaded.ts, { config: { ...config, hfs: config.hfs?.repo ?? null }, paths }, () => typeScriptContext(config, loaded, paths));
408
+ // Callers add to and sort the errors: each gets its own list.
409
+ return { ...context, errors: [...context.errors] };
410
+ }
375
411
  function typeScriptContext(config, loaded, paths) {
376
- return contextFromProjects(config, loaded, paths, {
377
- canonical, compilerError, isInside, slash, readTypeScriptProject, typeScriptProjectReferencePath,
378
- sameOrUnder, createTypeScriptProgram, isProductionSource, sourceLocation, workspaceMetadata, workspaceOf,
379
- pathAliasMatches, moduleReferences, workspaceSourceEntry, resolveTypeScriptModule, crossesSide, assetExtension: ASSET_EXTENSION,
380
- relativePath, packageExported, boundaryViolation,
381
- });
412
+ return contextFromProjects(config, loaded, paths, {
413
+ canonical, compilerError, isInside, slash, readTypeScriptProject, typeScriptProjectReferencePath,
414
+ sameOrUnder, createTypeScriptProgram, isProductionSource, sourceLocation, workspaceMetadata, workspaceOf,
415
+ pathAliasMatches, moduleReferences, workspaceSourceEntry, resolveTypeScriptModule, crossesSide, assetExtension: ASSET_EXTENSION,
416
+ relativePath, packageExported, boundaryViolation,
417
+ });
382
418
  }
383
419
  export function relativePath(root, fileName) {
384
- return slash(path.relative(root, fileName));
420
+ return slash(path.relative(root, fileName));
385
421
  }
386
-
387
422
  export function reachableViolation(edges, firstEdge, forbidden, { follow = () => true } = {}) {
388
- const queue = [{ file: firstEdge.to, chain: [firstEdge.from, firstEdge.to] }];
389
- const visited = new Set();
390
- while (queue.length) {
391
- const current = queue.shift();
392
- if (visited.has(current.file)) continue;
393
- visited.add(current.file);
394
- if (forbidden(current.file)) return current.chain;
395
- for (const edge of edges.get(current.file) ?? []) if (follow(edge)) queue.push({ file: edge.to, chain: [...current.chain, edge.to] });
396
- }
397
- return null;
398
- }
399
-
423
+ const queue = [{ file: firstEdge.to, chain: [firstEdge.from, firstEdge.to] }];
424
+ const visited = new Set();
425
+ while (queue.length) {
426
+ const current = queue.shift();
427
+ if (!current)
428
+ break;
429
+ if (visited.has(current.file))
430
+ continue;
431
+ visited.add(current.file);
432
+ if (forbidden(current.file))
433
+ return current.chain;
434
+ for (const edge of edges.get(current.file) ?? [])
435
+ if (follow(edge))
436
+ queue.push({ file: edge.to, chain: [...current.chain, edge.to] });
437
+ }
438
+ return null;
439
+ }
400
440
  /** A framework identity the checkers cannot prove statically. */
401
441
  export const UNPROVEN_FRAMEWORK = '(unproven framework identity)';
402
-
403
442
  /* ---- the AST walks every architecture checker shares ---- */
404
-
405
443
  /** The names of `expected` an import or re-export statement binds (all of them for a namespace or star form). */
406
444
  export function referencedExports(ts, statement, expected) {
407
- if (ts.isImportDeclaration(statement)) {
408
- const bindings = statement.importClause?.namedBindings;
409
- if (!bindings || ts.isNamespaceImport(bindings)) return bindings ? [...expected] : [];
410
- return bindings.elements.map(element => element.propertyName?.text ?? element.name.text).filter(name => expected.has(name));
411
- }
412
- if (ts.isExportDeclaration(statement)) {
413
- if (!statement.exportClause || ts.isNamespaceExport(statement.exportClause)) return [...expected];
414
- return statement.exportClause.elements.map(element => element.propertyName?.text ?? element.name.text).filter(name => expected.has(name));
415
- }
416
- return [...expected];
445
+ if (ts.isImportDeclaration(statement)) {
446
+ const bindings = statement.importClause?.namedBindings;
447
+ if (!bindings || ts.isNamespaceImport(bindings))
448
+ return bindings ? [...expected] : [];
449
+ return bindings.elements.map(element => element.propertyName?.text ?? element.name.text).filter(name => expected.has(name));
450
+ }
451
+ if (ts.isExportDeclaration(statement)) {
452
+ if (!statement.exportClause || ts.isNamespaceExport(statement.exportClause))
453
+ return [...expected];
454
+ return statement.exportClause.elements.map(element => element.propertyName?.text ?? element.name.text).filter(name => expected.has(name));
455
+ }
456
+ return [...expected];
417
457
  }