@starci/eslint-canon-be 3.2.18 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +46 -4
- package/app-composition.mjs +176 -0
- package/config-owner.mjs +34 -19
- package/feature-thin.mjs +59 -17
- package/index.mjs +2 -0
- package/lib/app-roles.mjs +48 -0
- package/lib/webhook-ack.mjs +87 -0
- package/lib/webhook-handoff.mjs +138 -0
- package/lib/webhook-verifiers.mjs +147 -0
- package/module-layering.mjs +2 -2
- package/module-shape.mjs +6 -5
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/knowledge/hfs/rules.yaml +41 -13
- package/runtime/knowledge/hfs/slots.yaml +99 -43
- package/runtime/knowledge/patterns/fe/folder.yaml +3 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +110 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +20 -3
- 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/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.mjs +14 -9
- 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/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +21 -12
- package/runtime/scripts/hfs/architecture/register-once.mjs +13 -5
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +3 -6
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/tiers.mjs +8 -1
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +1 -11
- package/runtime/scripts/hfs/manifest-shape.mjs +27 -2
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/slot-imports.mjs +3 -1
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/lib/ts-analysis.mjs +20 -0
- package/webhooks.mjs +141 -69
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** Imported JSX components receive repeated data contracts; executable prop values remain compared. */
|
|
2
|
+
import { unwrapExpression as unwrap } from '../../lib/ts-analysis.mjs';
|
|
3
|
+
|
|
4
|
+
const TRANSLATION_MODULES = new Set(['next-intl', 'next-intl/server']);
|
|
5
|
+
const TRANSLATION_FACTORIES = new Set(['useTranslations', 'getTranslations']);
|
|
6
|
+
const uppercase = (name) => /^[A-Z]/.test(name);
|
|
7
|
+
|
|
8
|
+
function bindName(ts, scope, name, binding) {
|
|
9
|
+
if (ts.isIdentifier(name)) scope.bindings.set(name.text, binding);
|
|
10
|
+
else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
|
|
11
|
+
for (const element of name.elements) if (ts.isBindingElement(element)) bindName(ts, scope, element.name, binding);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function importBindings(ts, scope, statement) {
|
|
16
|
+
const clause = statement.importClause;
|
|
17
|
+
if (!clause || clause.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier)) return;
|
|
18
|
+
const module = statement.moduleSpecifier.text;
|
|
19
|
+
if (clause.name) scope.bindings.set(clause.name.text, { kind: 'component' });
|
|
20
|
+
const named = clause.namedBindings;
|
|
21
|
+
if (!named) return;
|
|
22
|
+
if (ts.isNamespaceImport(named)) scope.bindings.set(named.name.text, { kind: 'namespace', module });
|
|
23
|
+
else for (const element of named.elements) {
|
|
24
|
+
if (element.isTypeOnly) continue;
|
|
25
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
26
|
+
const kind = TRANSLATION_MODULES.has(module) && TRANSLATION_FACTORIES.has(imported) ? 'translation-factory' : 'component';
|
|
27
|
+
scope.bindings.set(element.name.text, { kind });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function lexicalBindings(ts, sourceFile) {
|
|
32
|
+
const scopes = new WeakMap();
|
|
33
|
+
const root = { parent: null, bindings: new Map() };
|
|
34
|
+
root.functionScope = root;
|
|
35
|
+
const visit = (node, outer) => {
|
|
36
|
+
if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) bindName(ts, outer, node.name, { kind: 'local' });
|
|
37
|
+
const functionScope = ts.isFunctionLike(node);
|
|
38
|
+
const nested = functionScope || ts.isBlock(node) || ts.isCatchClause(node) || ts.isClassExpression(node)
|
|
39
|
+
|| ts.isForStatement(node) || ts.isForOfStatement(node) || ts.isForInStatement(node);
|
|
40
|
+
const scope = nested ? { parent: outer, bindings: new Map(), functionScope: functionScope ? null : outer.functionScope } : outer;
|
|
41
|
+
if (functionScope) scope.functionScope = scope;
|
|
42
|
+
scopes.set(node, scope);
|
|
43
|
+
if (ts.isImportDeclaration(node)) importBindings(ts, scope, node);
|
|
44
|
+
if (ts.isParameter(node)) bindName(ts, scope, node.name, { kind: 'local' });
|
|
45
|
+
if (ts.isFunctionExpression(node) && node.name) bindName(ts, scope, node.name, { kind: 'local' });
|
|
46
|
+
if (ts.isVariableDeclaration(node)) {
|
|
47
|
+
const flags = node.parent?.flags ?? ts.NodeFlags.Const;
|
|
48
|
+
bindName(ts, flags & ts.NodeFlags.BlockScoped ? scope : scope.functionScope, node.name,
|
|
49
|
+
{ kind: flags & ts.NodeFlags.Const ? 'constant' : 'local', declaration: node });
|
|
50
|
+
}
|
|
51
|
+
ts.forEachChild(node, (child) => { visit(child, scope); });
|
|
52
|
+
};
|
|
53
|
+
visit(sourceFile, root);
|
|
54
|
+
return (identifier) => {
|
|
55
|
+
for (let scope = scopes.get(identifier); scope; scope = scope.parent) {
|
|
56
|
+
if (scope.bindings.has(identifier.text)) return scope.bindings.get(identifier.text);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function translationFactory(ts, node, bindingOf) {
|
|
63
|
+
node = unwrap(ts, node);
|
|
64
|
+
if (ts.isIdentifier(node)) return bindingOf(node)?.kind === 'translation-factory';
|
|
65
|
+
return ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)
|
|
66
|
+
&& TRANSLATION_FACTORIES.has(node.name.text) && bindingOf(node.expression)?.kind === 'namespace'
|
|
67
|
+
&& TRANSLATION_MODULES.has(bindingOf(node.expression).module);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function translator(ts, node, bindingOf) {
|
|
71
|
+
if (!ts.isIdentifier(node)) return false;
|
|
72
|
+
const binding = bindingOf(node);
|
|
73
|
+
if (binding?.kind !== 'constant') return false;
|
|
74
|
+
let initializer = unwrap(ts, binding.declaration.initializer);
|
|
75
|
+
if (initializer && ts.isAwaitExpression(initializer)) initializer = unwrap(ts, initializer.expression);
|
|
76
|
+
return Boolean(initializer && ts.isCallExpression(initializer) && translationFactory(ts, initializer.expression, bindingOf));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function importedComponent(ts, tag, bindingOf) {
|
|
80
|
+
if (ts.isIdentifier(tag)) return uppercase(tag.text) && bindingOf(tag)?.kind === 'component';
|
|
81
|
+
if (!ts.isPropertyAccessExpression(tag) || !uppercase(tag.name.text)) return false;
|
|
82
|
+
let root = tag.expression;
|
|
83
|
+
while (ts.isPropertyAccessExpression(root)) root = root.expression;
|
|
84
|
+
return ts.isIdentifier(root) && bindingOf(root)?.kind === 'namespace';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function dataProperty(ts, property, mapping) {
|
|
88
|
+
if (ts.isSpreadAssignment(property)) return mapping(property.expression);
|
|
89
|
+
if (ts.isShorthandPropertyAssignment(property)) return !property.objectAssignmentInitializer;
|
|
90
|
+
if (!ts.isPropertyAssignment(property)) return false;
|
|
91
|
+
return (!ts.isComputedPropertyName(property.name) || mapping(property.name.expression)) && mapping(property.initializer);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function dataMapping(ts, expression, bindingOf) {
|
|
95
|
+
const node = unwrap(ts, expression);
|
|
96
|
+
if (!node) return false;
|
|
97
|
+
const mapping = (value) => dataMapping(ts, value, bindingOf);
|
|
98
|
+
if (ts.isIdentifier(node) || ts.isLiteralExpression(node) || node.kind === ts.SyntaxKind.TrueKeyword
|
|
99
|
+
|| node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword || node.kind === ts.SyntaxKind.ThisKeyword) return true;
|
|
100
|
+
if (ts.isPropertyAccessExpression(node)) return mapping(node.expression);
|
|
101
|
+
if (ts.isElementAccessExpression(node)) return mapping(node.expression) && mapping(node.argumentExpression);
|
|
102
|
+
if (ts.isObjectLiteralExpression(node)) return node.properties.every((property) => dataProperty(ts, property, mapping));
|
|
103
|
+
if (ts.isArrayLiteralExpression(node)) return node.elements.every((element) => ts.isOmittedExpression(element)
|
|
104
|
+
|| mapping(ts.isSpreadElement(element) ? element.expression : element));
|
|
105
|
+
if (ts.isTemplateExpression(node)) return node.templateSpans.every((span) => mapping(span.expression));
|
|
106
|
+
if (ts.isPrefixUnaryExpression(node)) return [ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken].includes(node.operator)
|
|
107
|
+
&& ts.isNumericLiteral(node.operand);
|
|
108
|
+
return ts.isCallExpression(node) && translator(ts, node.expression, bindingOf) && node.arguments.every(mapping);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Only object/array values belong to the prop contract; do not descend into a call or function body. */
|
|
112
|
+
function nestedDataObjects(ts, expression, bindingOf, excluded) {
|
|
113
|
+
const node = unwrap(ts, expression);
|
|
114
|
+
if (!node) return;
|
|
115
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
116
|
+
if (dataMapping(ts, node, bindingOf)) { excluded.add(node); return; }
|
|
117
|
+
for (const property of node.properties) if (ts.isPropertyAssignment(property)) {
|
|
118
|
+
nestedDataObjects(ts, property.initializer, bindingOf, excluded);
|
|
119
|
+
}
|
|
120
|
+
} else if (ts.isArrayLiteralExpression(node)) {
|
|
121
|
+
for (const element of node.elements) if (!ts.isSpreadElement(element)) nestedDataObjects(ts, element, bindingOf, excluded);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Canon data prop objects qualify through an imported component binding, never its chosen name or file path. */
|
|
126
|
+
export function canonJsxCloneNodes(ts, sourceFile) {
|
|
127
|
+
const bindingOf = lexicalBindings(ts, sourceFile);
|
|
128
|
+
const excluded = new Set();
|
|
129
|
+
const visit = (node) => {
|
|
130
|
+
if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && importedComponent(ts, node.tagName, bindingOf)) {
|
|
131
|
+
for (const attribute of node.attributes.properties) {
|
|
132
|
+
if (!ts.isJsxAttribute(attribute) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer)) continue;
|
|
133
|
+
const value = unwrap(ts, attribute.initializer.expression);
|
|
134
|
+
if (!value || !ts.isObjectLiteralExpression(value)) continue;
|
|
135
|
+
if (dataMapping(ts, value, bindingOf)) excluded.add(attribute);
|
|
136
|
+
else nestedDataObjects(ts, value, bindingOf, excluded);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
ts.forEachChild(node, visit);
|
|
140
|
+
};
|
|
141
|
+
visit(sourceFile);
|
|
142
|
+
return excluded;
|
|
143
|
+
}
|
|
@@ -21,8 +21,12 @@
|
|
|
21
21
|
* helper because NestFactory is allowed only in main.ts (BE_ENTRYPOINT_ONLY_IN_APPS), so what repeats there is the
|
|
22
22
|
* canonical shape of section 5 of BE-CONVENTION. A door or main.ts that grows logic is refused by those enforcers, not
|
|
23
23
|
* excused here; every other file of the same slots (mappers, dto, app.module.ts) stays compared.
|
|
24
|
+
* Canon scaffold subtrees (job fences, read/mapping methods, queue producers/definitions, CQRS wiring and data
|
|
25
|
+
* props of imported JSX frames) are also dropped through their imported shape markers. Each exclusion ends a
|
|
26
|
+
* token segment, so a window cannot join unrelated code across omitted scaffold lines. Custom bodies stay compared.
|
|
24
27
|
*/
|
|
25
28
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
29
|
+
import { canonCloneNodes } from './canon-clones.mjs';
|
|
26
30
|
|
|
27
31
|
export const CLONE_RULE_IDS = ['HFS_DUPLICATE_CODE'];
|
|
28
32
|
|
|
@@ -61,11 +65,13 @@ function visitToken(walk, node, inType) {
|
|
|
61
65
|
const { ts, sourceFile, kinds, lines, types, lineStarts, literalKinds } = walk;
|
|
62
66
|
const { SyntaxKind } = ts;
|
|
63
67
|
if (isTriviaNode(SyntaxKind, node)) return;
|
|
68
|
+
if (walk.excluded?.has(node)) { walk.segment += 1; return; }
|
|
64
69
|
const kind = node.kind;
|
|
65
70
|
const typeScope = inType || kind === SyntaxKind.InterfaceDeclaration || kind === SyntaxKind.TypeAliasDeclaration;
|
|
66
71
|
kinds.push(normalizedKindOf(SyntaxKind, literalKinds, kind));
|
|
67
72
|
lines.push(lineOfPosition(lineStarts, node.getStart(sourceFile)));
|
|
68
73
|
types.push(typeScope ? 1 : 0);
|
|
74
|
+
walk.segments.push(walk.segment);
|
|
69
75
|
ts.forEachChild(node, (child) => { visitToken(walk, child, typeScope); });
|
|
70
76
|
}
|
|
71
77
|
|
|
@@ -73,13 +79,14 @@ function visitToken(walk, node, inType) {
|
|
|
73
79
|
const isDroppedStatement = (SyntaxKind, statement) => statement.kind === SyntaxKind.ImportDeclaration || statement.kind === SyntaxKind.ImportEqualsDeclaration
|
|
74
80
|
|| (statement.kind === SyntaxKind.ExportDeclaration && statement.moduleSpecifier);
|
|
75
81
|
|
|
76
|
-
export function tokenize(ts, sourceFile) {
|
|
82
|
+
export function tokenize(ts, sourceFile, { excluded = null } = {}) {
|
|
77
83
|
const { SyntaxKind } = ts;
|
|
78
|
-
const walk = { ts, sourceFile, kinds: [], lines: [], types: [],
|
|
84
|
+
const walk = { ts, sourceFile, kinds: [], lines: [], types: [], segments: [], segment: 0, excluded,
|
|
85
|
+
lineStarts: sourceFile.getLineStarts(), literalKinds: literalKindSet(SyntaxKind) };
|
|
79
86
|
for (const statement of sourceFile.statements) {
|
|
80
87
|
if (!isDroppedStatement(SyntaxKind, statement)) visitToken(walk, statement, false);
|
|
81
88
|
}
|
|
82
|
-
return { kinds: Int32Array.from(walk.kinds), lines: Int32Array.from(walk.lines), types: Uint8Array.from(walk.types) };
|
|
89
|
+
return { kinds: Int32Array.from(walk.kinds), lines: Int32Array.from(walk.lines), types: Uint8Array.from(walk.types), segments: Int32Array.from(walk.segments) };
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
/** The app a file belongs to, by the slot binding of the manifest; null for a file of no app. */
|
|
@@ -142,15 +149,15 @@ const addToBucket = (buckets, key, item) => {
|
|
|
142
149
|
|
|
143
150
|
const addFileWindows = (buckets, file, fileIndex, N, T, hashing) => {
|
|
144
151
|
const { pow1, pow2, B1, B2 } = hashing;
|
|
145
|
-
const { kinds, lines } = file;
|
|
152
|
+
const { kinds, lines, segments } = file;
|
|
146
153
|
const count = kinds.length;
|
|
147
154
|
const { pre1, pre2 } = prefixHashes(kinds, B1, B2);
|
|
148
155
|
let end = 0;
|
|
149
156
|
for (let start = 0; start < count; start += 1) {
|
|
150
|
-
if (start > 0 && lines[start] === lines[start - 1]) continue;
|
|
157
|
+
if (start > 0 && lines[start] === lines[start - 1] && segments[start] === segments[start - 1]) continue;
|
|
151
158
|
const limit = lines[start] + N;
|
|
152
159
|
if (end < start) end = start;
|
|
153
|
-
while (end < count && lines[end] < limit) end += 1;
|
|
160
|
+
while (end < count && lines[end] < limit && segments[end] === segments[start]) end += 1;
|
|
154
161
|
if (end === start || lines[end - 1] - lines[start] + 1 < N) continue;
|
|
155
162
|
const length = end - start;
|
|
156
163
|
if (length < T) continue;
|
|
@@ -224,7 +231,8 @@ const cloneBlocks = (entries, hits, N) => {
|
|
|
224
231
|
group.sort((x, y) => x[0] - y[0]);
|
|
225
232
|
let [start, end] = group[0];
|
|
226
233
|
for (const [nextStart, nextEnd] of group.slice(1)) {
|
|
227
|
-
if (nextStart <= end
|
|
234
|
+
if (nextStart <= end && fileA.segments[nextStart] === fileA.segments[start]
|
|
235
|
+
&& fileB.segments[nextStart + diag] === fileB.segments[start + diag]) { end = Math.max(end, nextEnd); continue; }
|
|
228
236
|
appendCloneBlock(blocks, fileA, fileB, start, end, diag, N);
|
|
229
237
|
start = nextStart; end = nextEnd;
|
|
230
238
|
}
|
|
@@ -263,7 +271,8 @@ export function checkClones({ config, context, graph } = {}) {
|
|
|
263
271
|
if (/\.d\.[cm]?tsx?$/.test(rel) || uniformByDesign(graph.resolver, rel)) continue;
|
|
264
272
|
const unit = graph.unit(rel);
|
|
265
273
|
if (!unit) continue;
|
|
266
|
-
|
|
274
|
+
const sourceFile = graph.files.get(rel).sourceFile;
|
|
275
|
+
entries.push({ rel, unit, ...tokenize(ts, sourceFile, { excluded: canonCloneNodes(ts, sourceFile, graph.profile) }) });
|
|
267
276
|
}
|
|
268
277
|
const buckets = cloneBuckets(entries, N, T);
|
|
269
278
|
const blocks = cloneBlocks(entries, cloneHits(entries, buckets), N);
|
|
@@ -226,18 +226,20 @@ const passConnections = (argument, scope) => {
|
|
|
226
226
|
const checkAppRegistrations = (config, kit, ts, byName, report) => {
|
|
227
227
|
let registrations = 0;
|
|
228
228
|
for (const app of config.apps) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
229
|
+
// The root and its composition groups (compose/<area>.ts) are one composition: a connection is passed once across them.
|
|
230
|
+
const passed = new Map();
|
|
231
|
+
for (const root of kit.appCompositionFiles(app.name)) {
|
|
232
|
+
const checker = kit.checkerOf(root.sourceFile);
|
|
233
|
+
const scope = { ts, kit, checker, byName, passed, root, app, report };
|
|
234
|
+
kit.walk(root.sourceFile, node => {
|
|
235
|
+
const receiver = registerReceiver(ts, node);
|
|
236
|
+
if (!receiver) return;
|
|
237
|
+
const owner = kit.declarationsOf(checker, receiver).map(kit.ownerOfDeclaration).find(Boolean);
|
|
238
|
+
if (owner?.tier !== 'platform' || owner.name !== 'database') return;
|
|
239
|
+
registrations += 1;
|
|
240
|
+
for (const argument of node.arguments) passConnections(argument, scope);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
241
243
|
}
|
|
242
244
|
return registrations;
|
|
243
245
|
};
|
|
@@ -74,10 +74,16 @@ const nameFromIdentifier = (ctx, checker, node, depth) => {
|
|
|
74
74
|
? nameOfExpression(ctx, kit.checkerOf(declaration.getSourceFile()), declaration.initializer, depth + 1) : null;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
/** True when `declaration` is a property of the `<app>.env.ts` reader's own interface that sits beside the options file declaring `target`: the reader feeds the options value, so its property is the same slot. */
|
|
78
|
+
const feedsOptions = (declaration, target) => {
|
|
79
|
+
const home = declaration?.getSourceFile().fileName ?? '';
|
|
80
|
+
return home.endsWith('.env.ts') && path.posix.dirname(home) === path.posix.dirname(target.getSourceFile().fileName);
|
|
81
|
+
};
|
|
82
|
+
|
|
77
83
|
const nameFromProperty = (ctx, checker, node, depth) => {
|
|
78
84
|
const target = ctx.kit.declarationsOf(checker, node.name)[0];
|
|
79
85
|
if (!target) return null;
|
|
80
|
-
const names = new Set(assignmentsOf(ctx, node.name.text).filter(item => item.declaration === target)
|
|
86
|
+
const names = new Set(assignmentsOf(ctx, node.name.text).filter(item => item.declaration === target || feedsOptions(item.declaration, target))
|
|
81
87
|
.map(item => nameOfExpression(ctx, item.checker, item.initializer, depth + 1)).filter(Boolean));
|
|
82
88
|
return names.size === 1 ? [...names][0] : null;
|
|
83
89
|
};
|
|
@@ -282,30 +288,35 @@ const composedConnection = (kit, declared, checker, inner) => {
|
|
|
282
288
|
return value !== null && declared.has(value) ? value : null;
|
|
283
289
|
};
|
|
284
290
|
|
|
285
|
-
const collectComposedConnections = (kit, declared, checker, call, items) => {
|
|
291
|
+
const collectComposedConnections = (kit, declared, checker, call, file, items) => {
|
|
286
292
|
for (const argument of call.arguments) {
|
|
287
293
|
kit.walk(argument, inner => {
|
|
288
294
|
const value = composedConnection(kit, declared, checker, inner);
|
|
289
295
|
if (value === null) return true;
|
|
290
|
-
items.push({ connection: value, node: inner });
|
|
296
|
+
items.push({ connection: value, node: inner, file });
|
|
291
297
|
return false;
|
|
292
298
|
});
|
|
293
299
|
}
|
|
294
300
|
};
|
|
295
301
|
|
|
296
|
-
/**
|
|
302
|
+
/**
|
|
303
|
+
* The declared connection names the composition of `app` (its root app.module.ts and the composition groups of compose/) passes to
|
|
304
|
+
* the database module registration, as [{connection, node, file}] in path then source order.
|
|
305
|
+
*/
|
|
297
306
|
export function connectionsComposedBy(kit, app) {
|
|
298
307
|
const { ts } = kit;
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
308
|
+
const files = kit.appCompositionFiles(app);
|
|
309
|
+
if (!files.length) return { root: null, items: [] };
|
|
301
310
|
const declared = new Set(kit.resolver.repo.connections.map(connection => connection.name));
|
|
302
|
-
const checker = kit.checkerOf(root.sourceFile);
|
|
303
311
|
const items = [];
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
312
|
+
for (const file of files) {
|
|
313
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
314
|
+
kit.walk(file.sourceFile, node => {
|
|
315
|
+
if (!(ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register')) return;
|
|
316
|
+
const owner = kit.declarationsOf(checker, node.expression.expression).map(kit.ownerOfDeclaration).find(Boolean);
|
|
317
|
+
if (owner?.tier !== 'platform' || owner.name !== 'database') return;
|
|
318
|
+
collectComposedConnections(kit, declared, checker, node, file, items);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return { root: files[0], items };
|
|
311
322
|
}
|
|
@@ -23,8 +23,7 @@ const RUNNER_KINDS = new Set(['migrate', 'cli']);
|
|
|
23
23
|
const compositionsOf = (kit, resolver, model, appOf) => {
|
|
24
24
|
const composed = new Map(resolver.repo.apps.map(app => [app.name, { items: [], unresolved: false }]));
|
|
25
25
|
for (const app of resolver.repo.apps) {
|
|
26
|
-
const
|
|
27
|
-
for (const item of items) composed.get(app.name).items.push({ ...item, file: root });
|
|
26
|
+
for (const item of connectionsComposedBy(kit, app.name).items) composed.get(app.name).items.push(item);
|
|
28
27
|
}
|
|
29
28
|
for (const site of model.registrations) {
|
|
30
29
|
const app = appOf(site.file.rel);
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* (ruleParams.be.logicRoles: `<name>.policy.spec.ts`, `<name>.client.spec.ts`, ...), a
|
|
26
26
|
* 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
27
|
* `*.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
|
|
28
|
+
* 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
29
|
* Inject*() token or a param type the entry exports, and the world wires the same capabilities the apps do. An integration
|
|
30
30
|
* spec (slot be.tests.integration, `src/tests/integration/<capability>/*.integration-spec.ts`) counts as a consumer of exactly one
|
|
31
31
|
* owner: the integration (slot be.integrations, `src/modules/integrations/<provider>/`) whose provider folder is its capability
|
|
@@ -38,6 +38,7 @@ import { canonical } from './config.mjs';
|
|
|
38
38
|
import path from 'node:path';
|
|
39
39
|
import { treeOf } from './required-files.mjs';
|
|
40
40
|
import { globExpression } from '../../lib/glob.mjs';
|
|
41
|
+
import { stagedConsumerSources } from './staged-consumers.mjs';
|
|
41
42
|
|
|
42
43
|
export const DEAD_EXPORT_RULE_IDS = ['HFS_UNUSED_EXPORT', 'HFS_UNUSED_FILE'];
|
|
43
44
|
|
|
@@ -241,10 +242,12 @@ function inIntegrationOf(graph, to, provider) {
|
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
/** 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) {
|
|
245
|
+
function consumerFileEdges({ ts, options, graph, config }, rel, provider, indexedText) {
|
|
245
246
|
const abs = path.join(config.root, ...rel.split('/'));
|
|
246
|
-
let text;
|
|
247
|
-
|
|
247
|
+
let text = indexedText;
|
|
248
|
+
if (text === undefined) {
|
|
249
|
+
try { text = fs.readFileSync(abs, 'utf8'); } catch { return []; }
|
|
250
|
+
}
|
|
248
251
|
const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true);
|
|
249
252
|
const edges = [];
|
|
250
253
|
for (const statement of sourceFile.statements) {
|
|
@@ -257,17 +260,19 @@ function consumerFileEdges({ ts, options, graph, config }, rel, provider) {
|
|
|
257
260
|
}
|
|
258
261
|
|
|
259
262
|
/**
|
|
260
|
-
* Pseudo edges (read from disk, outside the production program) to graph files: from the unit role specs, the fixture builders and
|
|
263
|
+
* 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
264
|
* the test world files to anything, and from an integration spec only to the integration of its own provider folder.
|
|
262
265
|
*/
|
|
263
266
|
function testConsumerEdges({ context, graph, config }) {
|
|
264
267
|
const resolution = { ts: context.ts, options: context.projects?.[0]?.options ?? {}, graph, config };
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
+
// Live lint keeps tracked disk consumers; staged snapshots add the consumers present in the same commit.
|
|
269
|
+
const staged = stagedConsumerSources(config.root, file => testConsumer(graph, file) || integrationSpecProvider(graph, file) !== null);
|
|
270
|
+
const consumers = [...[...treeOf(config.root).files].map(file => ({ file })), ...staged]
|
|
271
|
+
.map(source => ({ ...source, any: testConsumer(graph, source.file) }))
|
|
272
|
+
.map(({ file, text, any }) => ({ file, text, provider: any ? null : integrationSpecProvider(graph, file), any }))
|
|
268
273
|
.filter(({ any, provider }) => any || provider !== null)
|
|
269
274
|
.sort((a, b) => a.file.localeCompare(b.file));
|
|
270
|
-
return consumers.flatMap(({ file: rel, provider }) => consumerFileEdges(resolution, rel, provider));
|
|
275
|
+
return consumers.flatMap(({ file: rel, text, provider }) => consumerFileEdges(resolution, rel, provider, text));
|
|
271
276
|
}
|
|
272
277
|
|
|
273
278
|
const edgeIndex = edges => {
|
|
@@ -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;
|