@starci/eslint-canon-fe 8.2.18 → 8.4.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.
- package/CHANGELOG.md +15 -0
- package/README.md +19 -1
- package/callback-shape.mjs +344 -0
- package/client-boundary.mjs +1 -1
- package/docs/hooks-folder.md +1 -1
- package/docs/size-and-state-budget.md +6 -6
- package/drawing.mjs +65 -28
- package/env-owner.mjs +120 -9
- package/function-budget.mjs +49 -0
- package/hooks-folder.mjs +149 -18
- package/icon.mjs +67 -0
- package/index.mjs +47 -0
- package/lib/own-hook-origin.mjs +53 -0
- package/lib/program-for.mjs +22 -0
- package/lib/pure-owner-helper.mjs +148 -0
- package/lib/redux-hook-origin.mjs +44 -0
- package/next-conventions.mjs +38 -35
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/slots.yaml +430 -92
- package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/models/runtimes.yaml +1 -1
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +1 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/api/git/lib.mjs +3 -0
- package/runtime/scripts/api/git/merge-base.mjs +9 -0
- package/runtime/scripts/api/git/recorded-lines.mjs +54 -8
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/project-rule.mjs +3 -1
- package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
- package/runtime/scripts/hfs/slot-imports.mjs +18 -3
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/lib/env.mjs +20 -0
- package/runtime/scripts/lib/event-contract.mjs +107 -0
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/ts-analysis.mjs +23 -0
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/served-locale.mjs +80 -9
- package/size-and-state-budget.mjs +11 -8
- package/size-growth.mjs +10 -6
- package/swr-family.mjs +197 -0
- package/translation.mjs +135 -7
- package/transport.mjs +126 -59
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Import and namespace consumption analysis shared by dead-export edges.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** Disk/staged test consumers have no production checker; bind their local names without resolving dependencies. */
|
|
5
|
+
const namespaceChecker = (context, sourceFile, namespace) => {
|
|
6
|
+
if (context.namespaceCheckers.has(sourceFile)) return context.namespaceCheckers.get(sourceFile);
|
|
7
|
+
const { ts } = context;
|
|
8
|
+
let checker = context.checkerFor?.(sourceFile.fileName);
|
|
9
|
+
if (!checker?.getSymbolAtLocation(namespace.name)) {
|
|
10
|
+
const options = { noResolve: true, noLib: true, types: [], allowJs: true, target: ts.ScriptTarget.Latest };
|
|
11
|
+
const host = ts.createCompilerHost(options);
|
|
12
|
+
const filename = path.resolve(sourceFile.fileName);
|
|
13
|
+
host.getSourceFile = file => path.resolve(file) === filename ? sourceFile : undefined;
|
|
14
|
+
host.fileExists = file => path.resolve(file) === filename;
|
|
15
|
+
host.readFile = file => path.resolve(file) === filename ? sourceFile.text : undefined;
|
|
16
|
+
checker = ts.createProgram([filename], options, host).getTypeChecker();
|
|
17
|
+
}
|
|
18
|
+
context.namespaceCheckers.set(sourceFile, checker);
|
|
19
|
+
return checker;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const transparentExpression = (ts, node) => node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node)
|
|
23
|
+
|| ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node));
|
|
24
|
+
|
|
25
|
+
const literalMember = (ts, node) => {
|
|
26
|
+
while (transparentExpression(ts, node)) node = node.expression;
|
|
27
|
+
return node && (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) ? node.text : null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const destructuredMember = (ts, name) => {
|
|
31
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
32
|
+
if (ts.isComputedPropertyName(name)) return literalMember(ts, name.expression);
|
|
33
|
+
return literalMember(ts, name);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** A destructuring alias consumes the property's name, while a rest/computed binding may consume every export. */
|
|
37
|
+
const destructuredBindings = (ts, pattern) => {
|
|
38
|
+
const names = [];
|
|
39
|
+
const elements = ts.isObjectBindingPattern(pattern) ? pattern.elements : pattern.properties;
|
|
40
|
+
for (const element of elements) {
|
|
41
|
+
if (element.dotDotDotToken || ts.isSpreadAssignment(element)) return { all: true, pairs: [] };
|
|
42
|
+
const name = destructuredMember(ts, element.propertyName ?? element.name);
|
|
43
|
+
if (name === null) return { all: true, pairs: [] };
|
|
44
|
+
names.push(name);
|
|
45
|
+
}
|
|
46
|
+
return { all: false, pairs: names.map(imported => ({ imported, exported: null })) };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const namespaceReference = (ts, checker, symbol, node) => {
|
|
50
|
+
if (checker.getSymbolAtLocation(node) === symbol) return true;
|
|
51
|
+
const parent = node.parent;
|
|
52
|
+
if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) return checker.getShorthandAssignmentValueSymbol(parent) === symbol;
|
|
53
|
+
return ts.isExportSpecifier(parent) && node === (parent.propertyName ?? parent.name)
|
|
54
|
+
&& checker.getExportSpecifierLocalTargetSymbol(parent) === symbol;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const unwrappedNamespace = (ts, node) => {
|
|
58
|
+
while (transparentExpression(ts, node.parent)) node = node.parent;
|
|
59
|
+
return node;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const indexedNamespaceMember = (ts, node) => {
|
|
63
|
+
if (!ts.isTypeQueryNode(node.parent) || node.parent.exprName !== node) return null;
|
|
64
|
+
let query = node.parent;
|
|
65
|
+
while (ts.isParenthesizedTypeNode(query.parent)) query = query.parent;
|
|
66
|
+
const indexed = query.parent;
|
|
67
|
+
return ts.isIndexedAccessTypeNode(indexed) && indexed.objectType === query && ts.isLiteralTypeNode(indexed.indexType)
|
|
68
|
+
? literalMember(ts, indexed.indexType.literal) : null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** The first member is the entry export even in a chain such as namespace.Enum.Member or namespace.Type.Inner. */
|
|
72
|
+
const namespaceUse = (ts, reference) => {
|
|
73
|
+
const node = unwrappedNamespace(ts, reference);
|
|
74
|
+
const parent = node.parent;
|
|
75
|
+
let name = null;
|
|
76
|
+
if (ts.isPropertyAccessExpression(parent) && parent.expression === node) name = parent.name.text;
|
|
77
|
+
else if (ts.isQualifiedName(parent) && parent.left === node) name = parent.right.text;
|
|
78
|
+
else if (ts.isElementAccessExpression(parent) && parent.expression === node) name = literalMember(ts, parent.argumentExpression);
|
|
79
|
+
else if (ts.isVariableDeclaration(parent) && parent.initializer === node && ts.isObjectBindingPattern(parent.name)) return destructuredBindings(ts, parent.name);
|
|
80
|
+
else if (ts.isBinaryExpression(parent) && parent.right === node && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
81
|
+
&& ts.isObjectLiteralExpression(parent.left)) return destructuredBindings(ts, parent.left);
|
|
82
|
+
else name = indexedNamespaceMember(ts, node);
|
|
83
|
+
return name === null ? { all: true, pairs: [] } : { all: false, pairs: [{ imported: name, exported: null }] };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const namespaceBindings = (context, declaration, namespace) => {
|
|
87
|
+
if (context.namespaceUses.has(namespace)) return context.namespaceUses.get(namespace);
|
|
88
|
+
const { ts } = context;
|
|
89
|
+
const sourceFile = declaration.getSourceFile();
|
|
90
|
+
const checker = namespaceChecker(context, sourceFile, namespace);
|
|
91
|
+
const symbol = checker.getSymbolAtLocation(namespace.name);
|
|
92
|
+
const result = { all: false, pairs: [] };
|
|
93
|
+
const visit = node => {
|
|
94
|
+
if (ts.isJSDocSeeTag(node) || ts.isJSDocLink(node) || ts.isJSDocLinkCode(node) || ts.isJSDocLinkPlain(node)) return;
|
|
95
|
+
if (ts.isIdentifier(node) && node !== namespace.name && namespaceReference(ts, checker, symbol, node)) {
|
|
96
|
+
const used = namespaceUse(ts, node);
|
|
97
|
+
result.all ||= used.all;
|
|
98
|
+
result.pairs.push(...used.pairs);
|
|
99
|
+
}
|
|
100
|
+
if (!result.all) {
|
|
101
|
+
ts.forEachChild(node, visit);
|
|
102
|
+
for (const doc of node.jsDoc ?? []) visit(doc);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
// An unavailable binding must never turn an unknown consumer into a false unused-export finding.
|
|
106
|
+
if (symbol) visit(sourceFile);
|
|
107
|
+
else result.all = true;
|
|
108
|
+
context.namespaceUses.set(namespace, result);
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const importBindings = (context, declaration) => {
|
|
113
|
+
const { ts } = context;
|
|
114
|
+
const clause = declaration.importClause;
|
|
115
|
+
if (!clause) return { all: false, pairs: [] }; // side-effect import: uses nothing
|
|
116
|
+
const pairs = [];
|
|
117
|
+
if (clause.name) pairs.push({ imported: 'default', exported: null });
|
|
118
|
+
const named = clause.namedBindings;
|
|
119
|
+
if (named && named.kind === ts.SyntaxKind.NamespaceImport) {
|
|
120
|
+
const namespace = namespaceBindings(context, declaration, named);
|
|
121
|
+
return { all: namespace.all, pairs: [...pairs, ...namespace.pairs] };
|
|
122
|
+
}
|
|
123
|
+
if (named) for (const element of named.elements) pairs.push({ imported: (element.propertyName ?? element.name).text, exported: null });
|
|
124
|
+
return { all: false, pairs };
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const exportBindings = (ts, declaration) => {
|
|
128
|
+
const clause = declaration.exportClause;
|
|
129
|
+
if (!clause || clause.kind === ts.SyntaxKind.NamespaceExport) return { all: true, pairs: [] };
|
|
130
|
+
return { all: false, pairs: clause.elements.map(element => ({ imported: (element.propertyName ?? element.name).text, exported: element.name.text })) };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const importTypeBindings = (ts, declaration) => {
|
|
134
|
+
let qualifier = declaration.qualifier;
|
|
135
|
+
if (!qualifier) return { all: true, pairs: [] };
|
|
136
|
+
while (qualifier.kind === ts.SyntaxKind.QualifiedName) qualifier = qualifier.left;
|
|
137
|
+
return { all: false, pairs: [{ imported: qualifier.text, exported: null }] };
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** Names an import/export declaration takes from its target: {all} or {pairs: [{imported, exported}]}. */
|
|
141
|
+
export function bindingsOf(context, declaration) {
|
|
142
|
+
const { ts } = context;
|
|
143
|
+
if (!declaration) return { all: true, pairs: [] };
|
|
144
|
+
if (declaration.kind === ts.SyntaxKind.ImportDeclaration) return importBindings(context, declaration);
|
|
145
|
+
if (declaration.kind === ts.SyntaxKind.ExportDeclaration) return exportBindings(ts, declaration);
|
|
146
|
+
if (declaration.kind === ts.SyntaxKind.ImportTypeNode) return importTypeBindings(ts, declaration);
|
|
147
|
+
// ImportEqualsDeclaration, dynamic import(), require(): the whole module is taken.
|
|
148
|
+
return { all: true, pairs: [] };
|
|
149
|
+
}
|
|
@@ -17,15 +17,16 @@
|
|
|
17
17
|
* and contributes no name here.
|
|
18
18
|
*
|
|
19
19
|
* A name is used when a production file outside the owner imports it from the entry (type-only imports count). A
|
|
20
|
-
* namespace import
|
|
21
|
-
*
|
|
20
|
+
* namespace import uses its statically accessed members; a computed read or an escape of the namespace uses every name.
|
|
21
|
+
* `import x = require`, a dynamic import()/require(), an `export *` or `export * as` of the entry make every name used.
|
|
22
|
+
* A file outside the owner that re-exports the name (`export { n as m } from`) uses it when `m` is itself
|
|
22
23
|
* used by the importers of that file; a re-exporting file nobody imports is a framework entry (a route file) and uses it.
|
|
23
24
|
* Specs and tests are not in the graph, so an export used only by a spec is dead, on purpose, with these exceptions (unit test
|
|
24
25
|
* standard): the unit spec of every unit role (ruleParams.be.unitRoles: `<name>.service.spec.ts`, `<name>.cli.spec.ts`) and of every logic role
|
|
25
26
|
* (ruleParams.be.logicRoles: `<name>.policy.spec.ts`, `<name>.client.spec.ts`, ...), a
|
|
26
27
|
* unit spec of a webhook, gateway or subscription door (`.webhook.spec.ts`, `.gateway.spec.ts`, `.subscription.spec.ts`: a door has no service of its own), a
|
|
27
28
|
* `*.builder.ts` under src/tests/fixtures/builders, and a file of the test world (slots be.tests.world and be.tests.world.kit, which
|
|
28
|
-
* open and migrate the real databases of the e2e run) read from disk count as consumers, because a spec can only provide an
|
|
29
|
+
* open and migrate the real databases of the e2e run) read from disk or staged in the git index count as consumers, because a spec can only provide an
|
|
29
30
|
* Inject*() token or a param type the entry exports, and the world wires the same capabilities the apps do. An integration
|
|
30
31
|
* spec (slot be.tests.integration, `src/tests/integration/<capability>/*.integration-spec.ts`) counts as a consumer of exactly one
|
|
31
32
|
* owner: the integration (slot be.integrations, `src/modules/integrations/<provider>/`) whose provider folder is its capability
|
|
@@ -38,6 +39,8 @@ import { canonical } from './config.mjs';
|
|
|
38
39
|
import path from 'node:path';
|
|
39
40
|
import { treeOf } from './required-files.mjs';
|
|
40
41
|
import { globExpression } from '../../lib/glob.mjs';
|
|
42
|
+
import { stagedConsumerSources } from './staged-consumers.mjs';
|
|
43
|
+
import { bindingsOf } from './dead-exports-bindings.mjs';
|
|
41
44
|
|
|
42
45
|
export const DEAD_EXPORT_RULE_IDS = ['HFS_UNUSED_EXPORT', 'HFS_UNUSED_FILE'];
|
|
43
46
|
|
|
@@ -69,40 +72,6 @@ function extraEntries(graph, entry) {
|
|
|
69
72
|
return names.flatMap(name => (name.includes('*') ? matches(name) : [`${found.root}/${name}`].filter(rel => graph.files.has(rel))));
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
const importBindings = (ts, declaration) => {
|
|
73
|
-
const clause = declaration.importClause;
|
|
74
|
-
if (!clause) return { all: false, pairs: [] }; // side-effect import: uses nothing
|
|
75
|
-
const pairs = [];
|
|
76
|
-
if (clause.name) pairs.push({ imported: 'default', exported: null });
|
|
77
|
-
const named = clause.namedBindings;
|
|
78
|
-
if (named && named.kind === ts.SyntaxKind.NamespaceImport) return { all: true, pairs: [] };
|
|
79
|
-
if (named) for (const element of named.elements) pairs.push({ imported: (element.propertyName ?? element.name).text, exported: null });
|
|
80
|
-
return { all: false, pairs };
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const exportBindings = (ts, declaration) => {
|
|
84
|
-
const clause = declaration.exportClause;
|
|
85
|
-
if (!clause || clause.kind === ts.SyntaxKind.NamespaceExport) return { all: true, pairs: [] };
|
|
86
|
-
return { all: false, pairs: clause.elements.map(element => ({ imported: (element.propertyName ?? element.name).text, exported: element.name.text })) };
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const importTypeBindings = (ts, declaration) => {
|
|
90
|
-
let qualifier = declaration.qualifier;
|
|
91
|
-
if (!qualifier) return { all: true, pairs: [] };
|
|
92
|
-
while (qualifier.kind === ts.SyntaxKind.QualifiedName) qualifier = qualifier.left;
|
|
93
|
-
return { all: false, pairs: [{ imported: qualifier.text, exported: null }] };
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/** Names an import/export declaration takes from its target: {all} or {pairs: [{imported, exported}]}. */
|
|
97
|
-
function bindingsOf(ts, declaration) {
|
|
98
|
-
if (!declaration) return { all: true, pairs: [] };
|
|
99
|
-
if (declaration.kind === ts.SyntaxKind.ImportDeclaration) return importBindings(ts, declaration);
|
|
100
|
-
if (declaration.kind === ts.SyntaxKind.ExportDeclaration) return exportBindings(ts, declaration);
|
|
101
|
-
if (declaration.kind === ts.SyntaxKind.ImportTypeNode) return importTypeBindings(ts, declaration);
|
|
102
|
-
// ImportEqualsDeclaration, dynamic import(), require(): the whole module is taken.
|
|
103
|
-
return { all: true, pairs: [] };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
75
|
const addExportName = (found, sourceFile, name, node) => {
|
|
107
76
|
if (found.has(name)) return;
|
|
108
77
|
found.set(name, sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1);
|
|
@@ -241,10 +210,12 @@ function inIntegrationOf(graph, to, provider) {
|
|
|
241
210
|
}
|
|
242
211
|
|
|
243
212
|
/** The pseudo edges of one test consumer file: its import/export declarations that resolve to graph files it may reach. */
|
|
244
|
-
function consumerFileEdges({ ts, options, graph, config }, rel, provider) {
|
|
213
|
+
function consumerFileEdges({ ts, options, graph, config }, rel, provider, indexedText) {
|
|
245
214
|
const abs = path.join(config.root, ...rel.split('/'));
|
|
246
|
-
let text;
|
|
247
|
-
|
|
215
|
+
let text = indexedText;
|
|
216
|
+
if (text === undefined) {
|
|
217
|
+
try { text = fs.readFileSync(abs, 'utf8'); } catch { return []; }
|
|
218
|
+
}
|
|
248
219
|
const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true);
|
|
249
220
|
const edges = [];
|
|
250
221
|
for (const statement of sourceFile.statements) {
|
|
@@ -257,17 +228,19 @@ function consumerFileEdges({ ts, options, graph, config }, rel, provider) {
|
|
|
257
228
|
}
|
|
258
229
|
|
|
259
230
|
/**
|
|
260
|
-
* Pseudo edges (read from disk, outside the production program) to graph files: from the unit role specs, the fixture builders and
|
|
231
|
+
* Pseudo edges (read from disk and staged index sources, outside the production program) to graph files: from the unit role specs, the fixture builders and
|
|
261
232
|
* the test world files to anything, and from an integration spec only to the integration of its own provider folder.
|
|
262
233
|
*/
|
|
263
234
|
function testConsumerEdges({ context, graph, config }) {
|
|
264
235
|
const resolution = { ts: context.ts, options: context.projects?.[0]?.options ?? {}, graph, config };
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
236
|
+
// Live lint keeps tracked disk consumers; staged snapshots add the consumers present in the same commit.
|
|
237
|
+
const staged = stagedConsumerSources(config.root, file => testConsumer(graph, file) || integrationSpecProvider(graph, file) !== null);
|
|
238
|
+
const consumers = [...[...treeOf(config.root).files].map(file => ({ file })), ...staged]
|
|
239
|
+
.map(source => ({ ...source, any: testConsumer(graph, source.file) }))
|
|
240
|
+
.map(({ file, text, any }) => ({ file, text, provider: any ? null : integrationSpecProvider(graph, file), any }))
|
|
268
241
|
.filter(({ any, provider }) => any || provider !== null)
|
|
269
242
|
.sort((a, b) => a.file.localeCompare(b.file));
|
|
270
|
-
return consumers.flatMap(({ file: rel, provider }) => consumerFileEdges(resolution, rel, provider));
|
|
243
|
+
return consumers.flatMap(({ file: rel, text, provider }) => consumerFileEdges(resolution, rel, provider, text));
|
|
271
244
|
}
|
|
272
245
|
|
|
273
246
|
const edgeIndex = edges => {
|
|
@@ -307,7 +280,7 @@ const consumedNames = (context, file, ownerKey, depth, visiting) => {
|
|
|
307
280
|
const result = { all: false, names: new Set() };
|
|
308
281
|
for (const edge of context.edgesTo.get(file) ?? []) {
|
|
309
282
|
if (ownerKey && unitOfFile(context.graph, edge.from) === ownerKey) continue;
|
|
310
|
-
const bindings = bindingsOf(context
|
|
283
|
+
const bindings = bindingsOf(context, edge.edge.declaration);
|
|
311
284
|
if (bindings.all) { result.all = true; continue; }
|
|
312
285
|
for (const pair of bindings.pairs) consumeReexportedName(context, result, edge, pair, depth, visiting);
|
|
313
286
|
}
|
|
@@ -316,10 +289,10 @@ const consumedNames = (context, file, ownerKey, depth, visiting) => {
|
|
|
316
289
|
|
|
317
290
|
/** Adds to `used` the entry exports reached by a consumer outside the owner that imports a file the entry re-exports by name. */
|
|
318
291
|
const addReexportedUses = (context, key, publicEntry, used) => {
|
|
319
|
-
const {
|
|
292
|
+
const { graph } = context;
|
|
320
293
|
for (const edge of graph.edges) {
|
|
321
294
|
if (edge.from !== publicEntry || !edge.reexport || graph.unit(edge.to) !== key) continue;
|
|
322
|
-
const reexport = bindingsOf(
|
|
295
|
+
const reexport = bindingsOf(context, edge.edge.declaration);
|
|
323
296
|
const inner = consumedNames(context, edge.to, key, 0, new Set([publicEntry, edge.to]));
|
|
324
297
|
if (reexport.all) continue;
|
|
325
298
|
for (const pair of reexport.pairs) if (inner.all || inner.names.has(pair.imported)) used.names.add(pair.exported);
|
|
@@ -337,7 +310,7 @@ const unusedExportViolation = (publicEntry, name, line, owner) => ({
|
|
|
337
310
|
const addSiblingEntryUses = (context, publicEntry, declared, used) => {
|
|
338
311
|
for (const edge of context.edgesTo.get(publicEntry) ?? []) {
|
|
339
312
|
if (edge.reexport || edge.from === publicEntry || !declared.has(edge.from)) continue;
|
|
340
|
-
const bindings = bindingsOf(context
|
|
313
|
+
const bindings = bindingsOf(context, edge.edge.declaration);
|
|
341
314
|
if (bindings.all) used.all = true;
|
|
342
315
|
for (const pair of bindings.pairs) used.names.add(pair.imported);
|
|
343
316
|
}
|
|
@@ -377,7 +350,7 @@ const ownerExportFindings = (context, config) => {
|
|
|
377
350
|
|
|
378
351
|
export function checkDeadExports({ context, graph, config }) {
|
|
379
352
|
const edgesTo = edgeIndex([...graph.edges, ...testConsumerEdges({ context, graph, config })]);
|
|
380
|
-
const analysis = { ...context, graph, edgesTo };
|
|
353
|
+
const analysis = { ...context, graph, edgesTo, namespaceCheckers: new WeakMap(), namespaceUses: new WeakMap() };
|
|
381
354
|
const exports = ownerExportFindings(analysis, config);
|
|
382
355
|
const files = deadFiles(graph, config);
|
|
383
356
|
const dead = exports.violations.length;
|
|
@@ -2,22 +2,23 @@ import { machineKit } from './machine-ast.mjs';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* R41 `default-deny-app-guard` (BE_DEFAULT_DENY). Every api app root provides its `APP_GUARD` entries in the order
|
|
5
|
-
* throttler -> CSRF origin guard -> AuthGuard, each exactly once, so no request reaches a handler before it was
|
|
6
|
-
* rate-limited, origin-checked and
|
|
5
|
+
* throttler -> CSRF origin guard -> CredentialGuard -> AuthGuard, each exactly once, so no request reaches a handler before it was
|
|
6
|
+
* rate-limited, origin-checked, credential-authenticated and admitted. A guard is classified by structure, never by name or folder alone
|
|
7
7
|
* (BE-CONVENTION 1.7 names the roles, not a library):
|
|
8
8
|
* - throttler: a class of @nestjs/throttler or one that extends it, or a guard that reads through a Reflector the
|
|
9
9
|
* per-door metadata (the `@RateLimit(tier)` decorator) that a SetMetadata call of its own capability writes;
|
|
10
10
|
* - CSRF origin guard: a guard declared in platform/http-security that reads the request origin or referer header;
|
|
11
|
+
* - CredentialGuard: the `CredentialGuard` class declared in domain/identity;
|
|
11
12
|
* - AuthGuard: the `AuthGuard` class declared in domain/identity.
|
|
12
13
|
* Other guards are permitted between them; a missing, repeated or reordered one of the three is a finding.
|
|
13
14
|
*/
|
|
14
15
|
export const DEFAULT_DENY_RULE_IDS = ['BE_DEFAULT_DENY'];
|
|
15
16
|
|
|
16
17
|
const RULE = 'BE_DEFAULT_DENY';
|
|
17
|
-
const ORDER = ['throttler', 'csrf', 'auth'];
|
|
18
|
+
const ORDER = ['throttler', 'csrf', 'credential', 'auth'];
|
|
18
19
|
const ORIGIN_HEADERS = new Set(['origin', 'referer', 'referrer']);
|
|
19
20
|
const METADATA_READERS = new Set(['get', 'getAll', 'getAllAndMerge', 'getAllAndOverride']);
|
|
20
|
-
const LABEL = { throttler: 'the throttler guard', csrf: 'the CSRF origin guard of platform/http-security', auth: 'AuthGuard of domain/identity' };
|
|
21
|
+
const LABEL = { throttler: 'the throttler guard', csrf: 'the CSRF origin guard of platform/http-security', credential: 'CredentialGuard of domain/identity', auth: 'AuthGuard of domain/identity' };
|
|
21
22
|
|
|
22
23
|
const keySymbol = (kit, checker, node) => (node ? kit.aliased(checker, kit.symbolAt(checker, node)) ?? null : null);
|
|
23
24
|
|
|
@@ -98,6 +99,7 @@ const classifyDeclaration = (kit, graph, keyCache, declaration) => {
|
|
|
98
99
|
if (readsDoorMetadata(kit, graph, keyCache, declaration, owner)) return 'throttler';
|
|
99
100
|
}
|
|
100
101
|
if (owner?.tier === 'domain' && owner.name === 'identity' && ts.isClassDeclaration(declaration) && declaration.name?.text === 'AuthGuard') return 'auth';
|
|
102
|
+
if (owner?.tier === 'domain' && owner.name === 'identity' && ts.isClassDeclaration(declaration) && declaration.name?.text === 'CredentialGuard') return 'credential';
|
|
101
103
|
return null;
|
|
102
104
|
};
|
|
103
105
|
|
|
@@ -117,7 +119,7 @@ const roleMessages = (app, roles, anchor) => {
|
|
|
117
119
|
const out = [];
|
|
118
120
|
for (const role of ORDER) {
|
|
119
121
|
const entries = roles.filter(item => item.role === role);
|
|
120
|
-
if (entries.length === 0) out.push({ node: anchor, message: `App ${app.name} does not provide ${LABEL[role]} as an APP_GUARD; an api app is denied by default: throttler, then CSRF origin guard, then AuthGuard.` });
|
|
122
|
+
if (entries.length === 0) out.push({ node: anchor, message: `App ${app.name} does not provide ${LABEL[role]} as an APP_GUARD; an api app is denied by default: throttler, then CSRF origin guard, then CredentialGuard, then AuthGuard.` });
|
|
121
123
|
else if (entries.length > 1) out.push({ node: entries[1].guard.node, message: `App ${app.name} provides ${LABEL[role]} more than once as an APP_GUARD.` });
|
|
122
124
|
}
|
|
123
125
|
return out;
|
|
@@ -128,7 +130,7 @@ const orderMessages = (app, roles) => {
|
|
|
128
130
|
if (!indexes.every(index => index >= 0)) return [];
|
|
129
131
|
const out = [];
|
|
130
132
|
for (let i = 1; i < indexes.length; i += 1) {
|
|
131
|
-
if (indexes[i] < indexes[i - 1]) out.push({ node: roles[indexes[i]].guard.node, message: `App ${app.name} provides ${LABEL[ORDER[i]]} before ${LABEL[ORDER[i - 1]]}; the APP_GUARD order is throttler, CSRF origin guard, AuthGuard.` });
|
|
133
|
+
if (indexes[i] < indexes[i - 1]) out.push({ node: roles[indexes[i]].guard.node, message: `App ${app.name} provides ${LABEL[ORDER[i]]} before ${LABEL[ORDER[i - 1]]}; the APP_GUARD order is throttler, CSRF origin guard, CredentialGuard, AuthGuard.` });
|
|
132
134
|
}
|
|
133
135
|
return out;
|
|
134
136
|
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { normalizedSymbolValue } from './ast-walks.mjs';
|
|
3
|
+
import { unwrapExpression } from './typescript.mjs';
|
|
4
|
+
|
|
5
|
+
/** Feature composition may inject block renders into a pure view without making the view a world owner. */
|
|
6
|
+
export class BlockRenderAnalysis {
|
|
7
|
+
constructor(renderer) {
|
|
8
|
+
this.renderer = renderer;
|
|
9
|
+
this.world = renderer.world;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
featureComposition(expression, expectedFile) {
|
|
13
|
+
const file = expression?.getSourceFile().fileName;
|
|
14
|
+
return Boolean(file && !expectedFile && this.world.insideAny(this.world.roots.features, file)
|
|
15
|
+
&& path.basename(file) === 'index.tsx');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
renderProp(expression, checker, expectedFile) {
|
|
19
|
+
return this.featureComposition(expression, expectedFile) && this.renderValue(expression, checker);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
blockTarget(expression, checker) {
|
|
23
|
+
const functions = this.world.expressionFunctions(expression, checker);
|
|
24
|
+
return functions.length > 0 && functions.every(fn => {
|
|
25
|
+
const file = fn.getSourceFile().fileName;
|
|
26
|
+
return this.world.sourceSet.has(path.resolve(file)) && path.basename(file) === 'index.tsx'
|
|
27
|
+
&& this.world.tierOf(this.world.roots.components, file)?.tier === 'blocks'
|
|
28
|
+
&& this.renderer.functionHasRender(fn, checker);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
callbacksRender(functions, checker, seen) {
|
|
33
|
+
return functions.length > 0 && functions.every(fn => {
|
|
34
|
+
const values = this.renderer.helpers.returnedExpressions(this.world.ts, fn);
|
|
35
|
+
return !this.world.functionUsesWorld(fn, checker) && values.length > 0
|
|
36
|
+
&& values.every(value => this.renderValue(value, checker, seen));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
renderValue(input, checker, seen = new Set()) {
|
|
41
|
+
const ts = this.world.ts;
|
|
42
|
+
const value = unwrapExpression(ts, input);
|
|
43
|
+
if (!value) return false;
|
|
44
|
+
if (value.kind === ts.SyntaxKind.NullKeyword || value.kind === ts.SyntaxKind.FalseKeyword) return true;
|
|
45
|
+
if (ts.isConditionalExpression(value)) return this.renderValue(value.whenTrue, checker, seen)
|
|
46
|
+
&& this.renderValue(value.whenFalse, checker, seen);
|
|
47
|
+
if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
48
|
+
return this.renderValue(value.right, checker, seen);
|
|
49
|
+
if (ts.isArrayLiteralExpression(value)) return value.elements.every(item => this.renderValue(item, checker, seen));
|
|
50
|
+
if (ts.isJsxFragment(value)) return this.fragmentBlocks(value, checker, seen);
|
|
51
|
+
if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value)) return this.jsxBlock(value, checker, seen);
|
|
52
|
+
if (ts.isCallExpression(value)) return this.createdBlock(value, checker, seen) || this.mappedBlocks(value, checker, seen);
|
|
53
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) return this.callbacksRender([value], checker, seen);
|
|
54
|
+
return this.referencedBlocks(value, checker, seen);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fragmentBlocks(value, checker, seen) {
|
|
58
|
+
const ts = this.world.ts;
|
|
59
|
+
return value.children.every(child => {
|
|
60
|
+
if (ts.isJsxText(child)) return !child.text.trim();
|
|
61
|
+
return this.renderValue(ts.isJsxExpression(child) ? child.expression : child, checker, seen);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
jsxBlock(value, checker, seen) {
|
|
66
|
+
const ts = this.world.ts;
|
|
67
|
+
const opening = ts.isJsxElement(value) ? value.openingElement : value;
|
|
68
|
+
if (!this.blockTarget(opening.tagName, checker)) return false;
|
|
69
|
+
const boundary = { hasBoundary: true };
|
|
70
|
+
return this.renderer.jsxChildrenBoundary(value, opening, checker, seen, null, false, boundary)
|
|
71
|
+
&& this.renderer.jsxAttributesBoundary(opening, checker, seen, null, boundary);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
mappedBlocks(value, checker, seen) {
|
|
75
|
+
const ts = this.world.ts;
|
|
76
|
+
const callee = unwrapExpression(ts, value.expression);
|
|
77
|
+
if (!ts.isPropertyAccessExpression(callee) && !ts.isElementAccessExpression(callee)) return false;
|
|
78
|
+
let name = ts.isPropertyAccessExpression(callee) ? callee.name.text : null;
|
|
79
|
+
if (name === null && ts.isStringLiteralLike(callee.argumentExpression)) name = callee.argumentExpression.text;
|
|
80
|
+
return name === 'map' && value.arguments.length > 0
|
|
81
|
+
&& this.callbacksRender(this.world.expressionFunctions(value.arguments[0], checker), checker, seen);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
createdBlock(value, checker, seen) {
|
|
85
|
+
const ts = this.world.ts;
|
|
86
|
+
if (!this.renderer.isCreateElement(value, checker) || !value.arguments[0]
|
|
87
|
+
|| !this.blockTarget(value.arguments[0], checker)) return false;
|
|
88
|
+
const boundary = { hasBoundary: true };
|
|
89
|
+
const props = value.arguments[1] ? unwrapExpression(ts, value.arguments[1]) : null;
|
|
90
|
+
return this.renderer.createElementPropsBoundary(props, checker, seen, null, boundary, false)
|
|
91
|
+
&& value.arguments.slice(2).every(child => this.renderer.renderBoundary(child, checker, seen));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
referencedBlocks(value, checker, seen) {
|
|
95
|
+
const ts = this.world.ts;
|
|
96
|
+
const symbol = normalizedSymbolValue(ts, checker, this.renderer.helpers.selectedSymbol(ts, checker, value));
|
|
97
|
+
if (!symbol || seen.has(symbol)) return false;
|
|
98
|
+
const nextSeen = new Set(seen).add(symbol);
|
|
99
|
+
const values = (symbol.getDeclarations?.() ?? []).flatMap(declaration => ts.isVariableDeclaration(declaration)
|
|
100
|
+
&& declaration.initializer ? [declaration.initializer] : []);
|
|
101
|
+
if (values.length) return values.every(item => this.renderValue(item, checker, nextSeen));
|
|
102
|
+
return this.callbacksRender(this.world.expressionFunctions(value, checker), checker, nextSeen);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { normalizedSymbolValue } from './ast-walks.mjs';
|
|
3
3
|
import { relativePath, unwrapExpression } from './typescript.mjs';
|
|
4
|
+
import { BlockRenderAnalysis } from './frontend-block-render.mjs';
|
|
4
5
|
function importDeclarationFor(ts, node) {
|
|
5
6
|
for (let current = node; current; current = current.parent) if (ts.isImportDeclaration(current)) return current;
|
|
6
7
|
return null;
|
|
@@ -41,12 +42,14 @@ export class WorldRenderAnalysis {
|
|
|
41
42
|
constructor(world, helpers) {
|
|
42
43
|
this.world = world;
|
|
43
44
|
this.helpers = helpers;
|
|
45
|
+
this.blocks = new BlockRenderAnalysis(this);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
expressionHasRender(expression, checker, seen = new Set()) {
|
|
47
49
|
expression = unwrapExpression(this.world.ts, expression);
|
|
48
50
|
if (!expression) return false;
|
|
49
51
|
if (this.world.ts.isJsxElement(expression) || this.world.ts.isJsxSelfClosingElement(expression) || this.world.ts.isJsxFragment(expression)) return true;
|
|
52
|
+
if (this.world.ts.isArrayLiteralExpression(expression)) return expression.elements.some(item => this.expressionHasRender(item, checker, seen));
|
|
50
53
|
if (this.world.ts.isCallExpression(expression) && isReactCreateElement(this.world.ts, checker, expression)) return true;
|
|
51
54
|
if (this.world.ts.isConditionalExpression(expression)) return this.conditionalExpressionHasRender(expression, checker, seen);
|
|
52
55
|
const logicalRender = this.logicalExpressionHasRender(expression, checker, seen);
|
|
@@ -148,6 +151,22 @@ export class WorldRenderAnalysis {
|
|
|
148
151
|
|| this.world.expressionFunctions(expression, checker).some(fn => this.functionHasRender(fn, checker));
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
isCreateElement(expression, checker) {
|
|
155
|
+
return isReactCreateElement(this.world.ts, checker, expression);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
propValueBoundary(input, checker, seen, expectedFile, pureTarget) {
|
|
159
|
+
const ts = this.world.ts;
|
|
160
|
+
const value = unwrapExpression(ts, input);
|
|
161
|
+
if (pureTarget && this.blocks.renderProp(value, checker, expectedFile)) return true;
|
|
162
|
+
if (pureTarget && this.blocks.featureComposition(value, expectedFile) && ts.isObjectLiteralExpression(value)) {
|
|
163
|
+
return value.properties.every(property => !ts.isPropertyAssignment(property)
|
|
164
|
+
|| this.propValueBoundary(property.initializer, checker, seen, expectedFile, pureTarget));
|
|
165
|
+
}
|
|
166
|
+
return !this.expressionSuppliesRender(value, checker) || this.pureRenderTarget(value, checker, expectedFile)
|
|
167
|
+
|| this.renderBoundary(value, checker, seen, true, expectedFile);
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
routedLayoutChild(opening, child, checker) {
|
|
152
171
|
const ts = this.world.ts;
|
|
153
172
|
if (!ts.isJsxExpression(child) || !child.expression) return false;
|
|
@@ -216,7 +235,7 @@ export class WorldRenderAnalysis {
|
|
|
216
235
|
if (!pureTarget && !wrapper) return false;
|
|
217
236
|
const boundary = { hasBoundary: pureTarget };
|
|
218
237
|
const props = expression.arguments[1] ? unwrapExpression(ts, expression.arguments[1]) : null;
|
|
219
|
-
if (!this.createElementPropsBoundary(props, checker, seen, expectedFile, boundary)) return false;
|
|
238
|
+
if (!this.createElementPropsBoundary(props, checker, seen, expectedFile, boundary, pureTarget)) return false;
|
|
220
239
|
const children = expression.arguments.slice(2);
|
|
221
240
|
if (children.length) {
|
|
222
241
|
if (!children.every(child => this.renderBoundary(child, checker, seen, false, expectedFile))) return false;
|
|
@@ -225,19 +244,19 @@ export class WorldRenderAnalysis {
|
|
|
225
244
|
return boundary.hasBoundary;
|
|
226
245
|
}
|
|
227
246
|
|
|
228
|
-
createElementPropsBoundary(props, checker, seen, expectedFile, boundary) {
|
|
247
|
+
createElementPropsBoundary(props, checker, seen, expectedFile, boundary, pureTarget) {
|
|
229
248
|
const ts = this.world.ts;
|
|
230
249
|
if (!props || props.kind === ts.SyntaxKind.NullKeyword) return true;
|
|
231
250
|
if (!ts.isObjectLiteralExpression(props) || props.properties.some(property => ts.isSpreadAssignment(property))) return false;
|
|
232
|
-
return props.properties.every(property => this.propsPropertyBoundary(property, checker, seen, expectedFile, boundary));
|
|
251
|
+
return props.properties.every(property => this.propsPropertyBoundary(property, checker, seen, expectedFile, boundary, pureTarget));
|
|
233
252
|
}
|
|
234
253
|
|
|
235
|
-
propsPropertyBoundary(property, checker, seen, expectedFile, boundary) {
|
|
254
|
+
propsPropertyBoundary(property, checker, seen, expectedFile, boundary, pureTarget) {
|
|
236
255
|
const ts = this.world.ts;
|
|
237
256
|
if (!ts.isPropertyAssignment(property)) return true;
|
|
238
257
|
const value = unwrapExpression(ts, property.initializer);
|
|
258
|
+
if (!this.propValueBoundary(value, checker, seen, expectedFile, pureTarget)) return false;
|
|
239
259
|
if (!this.expressionSuppliesRender(value, checker)) return true;
|
|
240
|
-
if (!this.pureRenderTarget(value, checker, expectedFile) && !this.renderBoundary(value, checker, seen, true, expectedFile)) return false;
|
|
241
260
|
const name = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) ? property.name.text : null;
|
|
242
261
|
if (['children', 'component', 'content', 'render', 'view'].includes(name)) boundary.hasBoundary = true;
|
|
243
262
|
return true;
|
|
@@ -270,7 +289,7 @@ export class WorldRenderAnalysis {
|
|
|
270
289
|
if (!pureTarget && !wrapper) return false;
|
|
271
290
|
const boundary = { hasBoundary: pureTarget };
|
|
272
291
|
if (!this.jsxChildrenBoundary(expression, opening, checker, seen, expectedFile, pureTarget, boundary)) return false;
|
|
273
|
-
if (!this.jsxAttributesBoundary(opening, checker, seen, expectedFile, boundary)) return false;
|
|
292
|
+
if (!this.jsxAttributesBoundary(opening, checker, seen, expectedFile, boundary, pureTarget)) return false;
|
|
274
293
|
return boundary.hasBoundary;
|
|
275
294
|
}
|
|
276
295
|
|
|
@@ -286,13 +305,13 @@ export class WorldRenderAnalysis {
|
|
|
286
305
|
return true;
|
|
287
306
|
}
|
|
288
307
|
|
|
289
|
-
jsxAttributesBoundary(opening, checker, seen, expectedFile, boundary) {
|
|
308
|
+
jsxAttributesBoundary(opening, checker, seen, expectedFile, boundary, pureTarget = false) {
|
|
290
309
|
const ts = this.world.ts;
|
|
291
310
|
return opening.attributes.properties.every(attribute => !ts.isJsxAttribute(attribute) || !attribute.initializer
|
|
292
|
-
|| this.jsxAttributeBoundary(attribute, checker, seen, expectedFile, boundary));
|
|
311
|
+
|| this.jsxAttributeBoundary(attribute, checker, seen, expectedFile, boundary, pureTarget));
|
|
293
312
|
}
|
|
294
313
|
|
|
295
|
-
jsxAttributeBoundary(attribute, checker, seen, expectedFile, boundary) {
|
|
314
|
+
jsxAttributeBoundary(attribute, checker, seen, expectedFile, boundary, pureTarget) {
|
|
296
315
|
const ts = this.world.ts;
|
|
297
316
|
const name = attribute.name.text;
|
|
298
317
|
if (['fallback', 'errorElement'].includes(name)) {
|
|
@@ -300,10 +319,10 @@ export class WorldRenderAnalysis {
|
|
|
300
319
|
if (ts.isJsxExpression(attribute.initializer)
|
|
301
320
|
&& !this.renderBoundary(attribute.initializer.expression, checker, seen, true, expectedFile)) return false;
|
|
302
321
|
}
|
|
322
|
+
if (ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression
|
|
323
|
+
&& !this.propValueBoundary(attribute.initializer.expression, checker, seen, expectedFile, pureTarget)) return false;
|
|
303
324
|
if (ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression
|
|
304
325
|
&& this.expressionSuppliesRender(attribute.initializer.expression, checker)) {
|
|
305
|
-
if (!this.pureRenderTarget(attribute.initializer.expression, checker, expectedFile)
|
|
306
|
-
&& !this.renderBoundary(attribute.initializer.expression, checker, seen, true, expectedFile)) return false;
|
|
307
326
|
if (['component', 'content', 'render', 'view'].includes(name)) boundary.hasBoundary = true;
|
|
308
327
|
}
|
|
309
328
|
return true;
|
|
@@ -74,7 +74,7 @@ class WorldAnalysis {
|
|
|
74
74
|
let found = false;
|
|
75
75
|
const visit = node => {
|
|
76
76
|
if (!found && this.ts.isCallExpression(node) && this.ts.isIdentifier(node.expression) && node.expression.text === 'fetch'
|
|
77
|
-
&&
|
|
77
|
+
&& (checker.getSymbolAtLocation(node.expression)?.getDeclarations() ?? []).every(declaration => declaration.getSourceFile().isDeclarationFile)) found = true;
|
|
78
78
|
if (!found) this.ts.forEachChild(node, visit);
|
|
79
79
|
};
|
|
80
80
|
visit(sourceFile);
|