@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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/why.js +15 -0
- package/dist/mascot.d.ts +28 -0
- package/dist/mascot.js +166 -0
- package/dist/surface-depth.d.ts +7 -0
- package/dist/surface-depth.js +92 -0
- package/docs/mascot.md +60 -0
- package/docs/surface-depth.md +40 -0
- package/package.json +2 -2
- package/runtime/bundle.json +74 -0
- package/runtime/engine/by-code-unit.mjs +6 -6
- package/runtime/engine/model-config.mjs +98 -76
- package/runtime/engine/orca-config.mjs +22 -17
- package/runtime/engine/release-config.mjs +22 -18
- package/runtime/engine/removed-vocabulary.mjs +21 -20
- package/runtime/engine/resources-config.mjs +42 -35
- package/runtime/engine/runtime-root.mjs +11 -13
- package/runtime/engine/secrets.mjs +170 -102
- package/runtime/engine/sonar-config.mjs +16 -13
- package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
- package/runtime/knowledge/hfs/rules.yaml +95 -6
- package/runtime/knowledge/hfs/slots.yaml +2 -2
- package/runtime/knowledge/patterns/fe/ui.yaml +13 -1
- package/runtime/scripts/hfs/allows.mjs +37 -38
- package/runtime/scripts/hfs/architecture/config.mjs +302 -268
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
- package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
- package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
- package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
- package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
- package/runtime/scripts/hfs/edition-slots.mjs +43 -40
- package/runtime/scripts/hfs/param-names.mjs +6 -11
- package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
- package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
- package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
- package/runtime/scripts/hfs/slot-classify.mjs +42 -38
- package/runtime/scripts/hfs/slot-errors.mjs +8 -7
- package/runtime/scripts/hfs/slot-imports.mjs +70 -63
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
- package/runtime/scripts/hfs/slot-match.mjs +122 -111
- package/runtime/scripts/hfs/slot-path.mjs +0 -2
- package/runtime/scripts/hfs/slot-required.mjs +65 -59
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
- package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
- package/runtime/scripts/hfs/slots.mjs +175 -152
- package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
- package/runtime/scripts/hfs/view.mjs +38 -40
- package/runtime/scripts/lib/glob.mjs +23 -14
- package/runtime/scripts/lib/grammar-lint.mjs +33 -37
- package/runtime/scripts/lib/i18n.mjs +32 -35
- package/runtime/scripts/lib/json-clone.mjs +45 -36
- package/runtime/scripts/lib/json.mjs +26 -14
- package/runtime/scripts/lib/list.mjs +9 -14
- package/runtime/scripts/lib/path-key.mjs +53 -38
- package/runtime/scripts/lib/read-text.mjs +30 -17
- package/runtime/scripts/lib/regex.mjs +1 -2
- package/runtime/scripts/lib/source-phrases.mjs +13 -13
- package/runtime/scripts/lib/ts-analysis.mjs +28 -26
- package/runtime/scripts/lib/ts-ast.mjs +36 -42
- package/runtime/scripts/lib/walk.mjs +33 -27
- 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
|
-
|
|
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
|
-
|
|
26
|
+
return ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
28
27
|
}
|
|
29
|
-
|
|
30
28
|
function compilerError(ts, root, diagnostic, project, ruleId = 'ARCH_TSCONFIG_INVALID') {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
251
|
+
return workspaces.find(workspace => isInside(workspace.root, file)) ?? null;
|
|
231
252
|
}
|
|
232
|
-
|
|
233
253
|
function exportPatternCapture(pattern, request) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
249
|
-
|
|
270
|
+
if (request !== '.')
|
|
271
|
+
return [];
|
|
272
|
+
return exportTargetStrings(declaration);
|
|
250
273
|
}
|
|
251
|
-
|
|
252
274
|
function exportMapCandidates(declaration, request) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
420
|
+
return slash(path.relative(root, fileName));
|
|
385
421
|
}
|
|
386
|
-
|
|
387
422
|
export function reachableViolation(edges, firstEdge, forbidden, { follow = () => true } = {}) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
}
|