@trackunit/iris-app 2.5.12 → 2.5.13
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 +11 -0
- package/migrations/p-content-space-codemod/invent-content-space.js +277 -0
- package/migrations/p-content-space-codemod/orchestrator.js +140 -0
- package/migrations/p-content-space-codemod/remove-redundant-padding.js +37 -0
- package/migrations/p-content-space-codemod/report-run-messaging.js +73 -0
- package/migrations/p-content-space-codemod/report-unresolved.js +87 -0
- package/migrations/p-content-space-codemod/resolve-content-shell.js +968 -0
- package/migrations/p-content-space-codemod/rewrite-page-content.js +42 -0
- package/migrations/p-content-space-codemod/shell-skip-reason.js +41 -0
- package/migrations/utils/classname-ast-rewrite.js +220 -0
- package/migrations/utils/resolve-extension-types.js +208 -0
- package/migrations.json +7 -1
- package/package.json +8 -6
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveContentShell = exports.resolveContentShells = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const css_classname_utils_1 = require("@trackunit/css-classname-utils");
|
|
6
|
+
const types_1 = require("@typescript-eslint/types");
|
|
7
|
+
const typescript_estree_1 = require("@typescript-eslint/typescript-estree");
|
|
8
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
9
|
+
const classname_ast_rewrite_1 = require("../utils/classname-ast-rewrite");
|
|
10
|
+
const shell_skip_reason_1 = require("./shell-skip-reason");
|
|
11
|
+
const DEFAULT_ENTRY_COMPONENT_FILE = "index.tsx";
|
|
12
|
+
const unresolvedShell = (code, detail) => ({
|
|
13
|
+
status: "unresolved",
|
|
14
|
+
reason: (0, shell_skip_reason_1.shellSkipReason)(code, detail),
|
|
15
|
+
});
|
|
16
|
+
const unresolvedShells = (code, detail) => ({
|
|
17
|
+
status: "unresolved",
|
|
18
|
+
reason: (0, shell_skip_reason_1.shellSkipReason)(code, detail),
|
|
19
|
+
});
|
|
20
|
+
/** Intrinsic hosts treated as content-region shells for this walking skeleton. */
|
|
21
|
+
const DIV_LIKE_ELEMENTS = new Set(["div", "main", "section"]);
|
|
22
|
+
/**
|
|
23
|
+
* Known non-DOM wrappers to skip when walking from an extension entry toward
|
|
24
|
+
* the content shell (providers, Suspense, fragments).
|
|
25
|
+
* `RouterProvider` is intentionally NOT a wrapper — it branches into light-touch
|
|
26
|
+
* multi-route discovery instead of being skipped.
|
|
27
|
+
*/
|
|
28
|
+
const isKnownWrapperName = (name) => {
|
|
29
|
+
if (name === "Suspense" ||
|
|
30
|
+
name === "Fragment" ||
|
|
31
|
+
name === "TrackunitInternalProviders" ||
|
|
32
|
+
name === "TrackunitProviders") {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return name.endsWith("Provider") && name !== "RouterProvider";
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Walks from `extension.main` through known non-DOM wrappers and static
|
|
39
|
+
* imports to identify editable content-region classname host(s).
|
|
40
|
+
*
|
|
41
|
+
* Multi-route policy (light-touch, static imports only):
|
|
42
|
+
* 1. Shared layout shell (div-like host wrapping `<Outlet />`) wins → one target.
|
|
43
|
+
* 2. Else each resolvable route leaf with a clear shell is targeted.
|
|
44
|
+
* 3. Exotic / unclear graphs → `{ status: "unresolved" }` (never throws; no first-div guess).
|
|
45
|
+
*/
|
|
46
|
+
const resolveContentShells = (tree, extension) => {
|
|
47
|
+
const entryPath = `${extension.sourceRoot}/${extension.main ?? DEFAULT_ENTRY_COMPONENT_FILE}`;
|
|
48
|
+
if (!tree.exists(entryPath)) {
|
|
49
|
+
return unresolvedShells("entry-missing", entryPath);
|
|
50
|
+
}
|
|
51
|
+
return resolveShellsFromFile(tree, entryPath, extension.sourceRoot, new Set());
|
|
52
|
+
};
|
|
53
|
+
exports.resolveContentShells = resolveContentShells;
|
|
54
|
+
/**
|
|
55
|
+
* Single-shell convenience wrapper over {@link resolveContentShells}.
|
|
56
|
+
* Returns the first resolved shell when discovery finds one or more; unresolved
|
|
57
|
+
* when none. Prefer {@link resolveContentShells} for RouterProvider / multi-route
|
|
58
|
+
* graphs — an arbitrary one of N leaves is not a useful production answer.
|
|
59
|
+
*/
|
|
60
|
+
const resolveContentShell = (tree, extension) => {
|
|
61
|
+
const result = (0, exports.resolveContentShells)(tree, extension);
|
|
62
|
+
if (result.status === "unresolved") {
|
|
63
|
+
return { status: "unresolved", reason: result.reason };
|
|
64
|
+
}
|
|
65
|
+
const shell = result.shells[0];
|
|
66
|
+
if (shell === undefined) {
|
|
67
|
+
return unresolvedShell("unclear-graph", "resolved status with empty shells list");
|
|
68
|
+
}
|
|
69
|
+
return { status: "resolved", shell };
|
|
70
|
+
};
|
|
71
|
+
exports.resolveContentShell = resolveContentShell;
|
|
72
|
+
const resolveShellsFromFile = (tree, filePath, sourceRoot, visited) => {
|
|
73
|
+
if (visited.has(filePath)) {
|
|
74
|
+
return unresolvedShells("cycle", filePath);
|
|
75
|
+
}
|
|
76
|
+
visited.add(filePath);
|
|
77
|
+
const content = tree.read(filePath, "utf-8");
|
|
78
|
+
if (content === null) {
|
|
79
|
+
return unresolvedShells("entry-missing", filePath);
|
|
80
|
+
}
|
|
81
|
+
let program;
|
|
82
|
+
try {
|
|
83
|
+
program = (0, classname_ast_rewrite_1.parseTsestreeSource)(content);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return unresolvedShells("parse-failed", filePath);
|
|
87
|
+
}
|
|
88
|
+
const rootJsx = findRootJsxElement(program);
|
|
89
|
+
if (rootJsx === null) {
|
|
90
|
+
return unresolvedShells("no-root-jsx", filePath);
|
|
91
|
+
}
|
|
92
|
+
const leaf = unwrapWrappers(rootJsx);
|
|
93
|
+
if (leaf === null) {
|
|
94
|
+
return unresolvedShells("empty-wrapper", filePath);
|
|
95
|
+
}
|
|
96
|
+
if (isDivLikeHost(leaf) && leaf.type === types_1.AST_NODE_TYPES.JSXElement) {
|
|
97
|
+
const single = pickContentShellInFile(filePath, program, leaf);
|
|
98
|
+
if (single.status === "unresolved") {
|
|
99
|
+
return { status: "unresolved", reason: single.reason };
|
|
100
|
+
}
|
|
101
|
+
return { status: "resolved", shells: [single.shell], skippedRouteNames: [] };
|
|
102
|
+
}
|
|
103
|
+
const componentName = getJsxComponentName(leaf);
|
|
104
|
+
if (componentName === null) {
|
|
105
|
+
return unresolvedShells("non-host-element", filePath);
|
|
106
|
+
}
|
|
107
|
+
if (componentName === "RouterProvider" && leaf.type === types_1.AST_NODE_TYPES.JSXElement) {
|
|
108
|
+
return resolveFromRouterProvider(tree, filePath, program, leaf, sourceRoot, visited);
|
|
109
|
+
}
|
|
110
|
+
if (isDynamicComponent(program, componentName)) {
|
|
111
|
+
return unresolvedShells("dynamic-component", componentName);
|
|
112
|
+
}
|
|
113
|
+
const importPath = resolveStaticLocalImport(tree, program, filePath, sourceRoot, componentName);
|
|
114
|
+
if (importPath === null) {
|
|
115
|
+
return unresolvedShells("unresolvable-import", componentName);
|
|
116
|
+
}
|
|
117
|
+
return resolveShellsFromFile(tree, importPath, sourceRoot, visited);
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Light-touch RouterProvider follow: read `router={name}`, resolve the router
|
|
121
|
+
* module (import or same-file), collect Identifier `component:` targets from
|
|
122
|
+
* `createRootRoute` / `createRoute` / `createRootRouteWithContext`. Skip arrow
|
|
123
|
+
* wrappers and exotic graphs rather than guessing.
|
|
124
|
+
*/
|
|
125
|
+
const resolveFromRouterProvider = (tree, filePath, program, routerProvider, sourceRoot, visited) => {
|
|
126
|
+
const routerName = getRouterPropIdentifier(routerProvider);
|
|
127
|
+
if (routerName === null) {
|
|
128
|
+
return unresolvedShells("router-prop-unresolved");
|
|
129
|
+
}
|
|
130
|
+
const routerModulePath = resolveStaticLocalImport(tree, program, filePath, sourceRoot, routerName);
|
|
131
|
+
if (routerModulePath !== null) {
|
|
132
|
+
return collectShellsFromRouterModule(tree, routerModulePath, sourceRoot, visited);
|
|
133
|
+
}
|
|
134
|
+
// Same-file router (route factories live alongside RouterProvider).
|
|
135
|
+
return collectShellsFromRouterProgram(tree, filePath, program, sourceRoot, visited);
|
|
136
|
+
};
|
|
137
|
+
const collectShellsFromRouterModule = (tree, routerModulePath, sourceRoot, visited) => {
|
|
138
|
+
if (visited.has(routerModulePath)) {
|
|
139
|
+
return unresolvedShells("cycle", routerModulePath);
|
|
140
|
+
}
|
|
141
|
+
visited.add(routerModulePath);
|
|
142
|
+
const content = tree.read(routerModulePath, "utf-8");
|
|
143
|
+
if (content === null) {
|
|
144
|
+
return unresolvedShells("entry-missing", routerModulePath);
|
|
145
|
+
}
|
|
146
|
+
let program;
|
|
147
|
+
try {
|
|
148
|
+
program = (0, classname_ast_rewrite_1.parseTsestreeSource)(content);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return unresolvedShells("parse-failed", routerModulePath);
|
|
152
|
+
}
|
|
153
|
+
return collectShellsFromRouterProgram(tree, routerModulePath, program, sourceRoot, visited);
|
|
154
|
+
};
|
|
155
|
+
const collectShellsFromRouterProgram = (tree, routerFilePath, program, sourceRoot, visited) => {
|
|
156
|
+
const routes = collectRouterRoutes(program);
|
|
157
|
+
if (routes.length === 0) {
|
|
158
|
+
return unresolvedShells("router-no-analysable-leaves", "no createRoute/createRootRoute factories found");
|
|
159
|
+
}
|
|
160
|
+
if (routes.every(route => route.componentName === null)) {
|
|
161
|
+
return unresolvedShells("router-inline-arrow-components", routerFilePath);
|
|
162
|
+
}
|
|
163
|
+
const definedVariableNames = new Set(routes.flatMap(route => (route.variableName === null ? [] : [route.variableName])));
|
|
164
|
+
const skippedRouteNames = collectAddChildrenIdentifiers(program).filter(name => !definedVariableNames.has(name));
|
|
165
|
+
const analyzed = [];
|
|
166
|
+
for (const route of routes) {
|
|
167
|
+
if (route.componentName === null) {
|
|
168
|
+
analyzed.push({ ...route, analysis: null });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const componentFile = resolveComponentModulePath(tree, program, routerFilePath, sourceRoot, route.componentName);
|
|
172
|
+
if (componentFile === null) {
|
|
173
|
+
skippedRouteNames.push(route.componentName);
|
|
174
|
+
analyzed.push({ ...route, analysis: null });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const analysis = analyzeRouteComponent(tree, componentFile, route.componentName, sourceRoot, new Set(visited));
|
|
178
|
+
if (analysis === null) {
|
|
179
|
+
skippedRouteNames.push(route.componentName);
|
|
180
|
+
analyzed.push({ ...route, analysis: null });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
analyzed.push({ ...route, analysis });
|
|
184
|
+
}
|
|
185
|
+
const shells = selectShellsForInvent(analyzed);
|
|
186
|
+
if (shells.length === 0) {
|
|
187
|
+
const skipped = uniqueNames(skippedRouteNames);
|
|
188
|
+
return unresolvedShells("router-no-analysable-leaves", skipped.length > 0 ? `skipped: ${skipped.join(", ")}` : routerFilePath);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
status: "resolved",
|
|
192
|
+
shells: dedupeShells(shells),
|
|
193
|
+
skippedRouteNames: uniqueNames(skippedRouteNames),
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const analyzeRouteComponent = (tree, filePath, componentName, sourceRoot, visited) => {
|
|
197
|
+
if (visited.has(`${filePath}#${componentName}`)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
visited.add(`${filePath}#${componentName}`);
|
|
201
|
+
const content = tree.read(filePath, "utf-8");
|
|
202
|
+
if (content === null) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
let program;
|
|
206
|
+
try {
|
|
207
|
+
program = (0, classname_ast_rewrite_1.parseTsestreeSource)(content);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const rootJsx = findJsxReturnedByName(program, componentName);
|
|
213
|
+
if (rootJsx === null) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const leaf = unwrapWrappers(rootJsx);
|
|
217
|
+
if (leaf === null) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if (isDivLikeHost(leaf) && leaf.type === types_1.AST_NODE_TYPES.JSXElement) {
|
|
221
|
+
const shellResult = pickContentShellInFile(filePath, program, leaf);
|
|
222
|
+
if (shellResult.status === "unresolved") {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const wrapsOutlet = elementContainsOutlet(leaf) || hostWrapsOutletOneHop(tree, filePath, program, leaf, sourceRoot, visited);
|
|
226
|
+
return { kind: wrapsOutlet ? "layout-shell" : "leaf-shell", shell: shellResult.shell };
|
|
227
|
+
}
|
|
228
|
+
const childName = getJsxComponentName(leaf);
|
|
229
|
+
if (childName === null || childName === "Outlet") {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (isDynamicComponent(program, childName)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const importPath = resolveStaticLocalImport(tree, program, filePath, sourceRoot, childName);
|
|
236
|
+
if (importPath === null) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
// Preserve nested kind: a one-hop wrapper around an Outlet layout
|
|
240
|
+
// (`Root` → `Shell` with Outlet) must stay `layout-shell` so shared layout
|
|
241
|
+
// wins over clear leaves. Forcing `leaf-shell` here double-invents.
|
|
242
|
+
return analyzeRouteComponent(tree, importPath, childName, sourceRoot, visited);
|
|
243
|
+
};
|
|
244
|
+
const getRouterPropIdentifier = (element) => {
|
|
245
|
+
for (const attribute of element.openingElement.attributes) {
|
|
246
|
+
if (attribute.type !== types_1.AST_NODE_TYPES.JSXAttribute)
|
|
247
|
+
continue;
|
|
248
|
+
if (attribute.name.type !== types_1.AST_NODE_TYPES.JSXIdentifier || attribute.name.name !== "router")
|
|
249
|
+
continue;
|
|
250
|
+
if (attribute.value?.type !== types_1.AST_NODE_TYPES.JSXExpressionContainer)
|
|
251
|
+
continue;
|
|
252
|
+
if (attribute.value.expression.type === types_1.AST_NODE_TYPES.Identifier) {
|
|
253
|
+
return attribute.value.expression.name;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
};
|
|
258
|
+
const getVariableDeclaratorInit = (declarator) => {
|
|
259
|
+
if (!("init" in declarator)) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return declarator.init ?? null;
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Collects TanStack route factory bindings (`createRootRoute` / `createRoute` /
|
|
266
|
+
* `createRootRouteWithContext`) with parent links from `getParentRoute`.
|
|
267
|
+
* Arrow `component` wrappers have `componentName: null` (not guessed).
|
|
268
|
+
*/
|
|
269
|
+
const collectRouterRoutes = (program) => {
|
|
270
|
+
const coveredRanges = new Set();
|
|
271
|
+
const routes = [];
|
|
272
|
+
(0, typescript_estree_1.simpleTraverse)(program, {
|
|
273
|
+
enter: node => {
|
|
274
|
+
if (node.type !== types_1.AST_NODE_TYPES.VariableDeclarator)
|
|
275
|
+
return;
|
|
276
|
+
if (node.id.type !== types_1.AST_NODE_TYPES.Identifier)
|
|
277
|
+
return;
|
|
278
|
+
const init = getVariableDeclaratorInit(node);
|
|
279
|
+
if (init === null || init.type !== types_1.AST_NODE_TYPES.CallExpression)
|
|
280
|
+
return;
|
|
281
|
+
if (!isRouteFactoryCall(init))
|
|
282
|
+
return;
|
|
283
|
+
const options = init.arguments[0];
|
|
284
|
+
if (options?.type !== types_1.AST_NODE_TYPES.ObjectExpression)
|
|
285
|
+
return;
|
|
286
|
+
coveredRanges.add(`${init.range[0]}:${init.range[1]}`);
|
|
287
|
+
routes.push({
|
|
288
|
+
variableName: node.id.name,
|
|
289
|
+
componentName: getComponentIdentifierFromOptions(options),
|
|
290
|
+
parentVariableName: getParentRouteIdentifier(options),
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
(0, typescript_estree_1.simpleTraverse)(program, {
|
|
295
|
+
enter: node => {
|
|
296
|
+
if (node.type !== types_1.AST_NODE_TYPES.CallExpression)
|
|
297
|
+
return;
|
|
298
|
+
if (!isRouteFactoryCall(node))
|
|
299
|
+
return;
|
|
300
|
+
if (coveredRanges.has(`${node.range[0]}:${node.range[1]}`))
|
|
301
|
+
return;
|
|
302
|
+
const options = node.arguments[0];
|
|
303
|
+
if (options?.type !== types_1.AST_NODE_TYPES.ObjectExpression)
|
|
304
|
+
return;
|
|
305
|
+
routes.push({
|
|
306
|
+
variableName: null,
|
|
307
|
+
componentName: getComponentIdentifierFromOptions(options),
|
|
308
|
+
parentVariableName: getParentRouteIdentifier(options),
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
return routes;
|
|
313
|
+
};
|
|
314
|
+
const getParentRouteIdentifier = (options) => {
|
|
315
|
+
for (const property of options.properties) {
|
|
316
|
+
if (property.type !== types_1.AST_NODE_TYPES.Property)
|
|
317
|
+
continue;
|
|
318
|
+
if (property.key.type !== types_1.AST_NODE_TYPES.Identifier || property.key.name !== "getParentRoute")
|
|
319
|
+
continue;
|
|
320
|
+
if (property.value.type !== types_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
321
|
+
property.value.type !== types_1.AST_NODE_TYPES.FunctionExpression) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (property.value.body.type === types_1.AST_NODE_TYPES.Identifier) {
|
|
325
|
+
return property.value.body.name;
|
|
326
|
+
}
|
|
327
|
+
if (property.value.body.type === types_1.AST_NODE_TYPES.BlockStatement) {
|
|
328
|
+
for (const statement of property.value.body.body) {
|
|
329
|
+
if (statement.type !== types_1.AST_NODE_TYPES.ReturnStatement || statement.argument === null)
|
|
330
|
+
continue;
|
|
331
|
+
const arg = unwrapParentheses(statement.argument);
|
|
332
|
+
if (arg.type === types_1.AST_NODE_TYPES.Identifier) {
|
|
333
|
+
return arg.name;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
};
|
|
340
|
+
const collectAddChildrenIdentifiers = (program) => {
|
|
341
|
+
const names = [];
|
|
342
|
+
(0, typescript_estree_1.simpleTraverse)(program, {
|
|
343
|
+
enter: node => {
|
|
344
|
+
if (node.type !== types_1.AST_NODE_TYPES.CallExpression)
|
|
345
|
+
return;
|
|
346
|
+
if (node.callee.type !== types_1.AST_NODE_TYPES.MemberExpression || node.callee.computed)
|
|
347
|
+
return;
|
|
348
|
+
if (node.callee.property.type !== types_1.AST_NODE_TYPES.Identifier)
|
|
349
|
+
return;
|
|
350
|
+
if (node.callee.property.name !== "addChildren")
|
|
351
|
+
return;
|
|
352
|
+
const arg = node.arguments[0];
|
|
353
|
+
if (arg?.type !== types_1.AST_NODE_TYPES.ArrayExpression)
|
|
354
|
+
return;
|
|
355
|
+
for (const element of arg.elements) {
|
|
356
|
+
if (element?.type === types_1.AST_NODE_TYPES.Identifier) {
|
|
357
|
+
names.push(element.name);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
return names;
|
|
363
|
+
};
|
|
364
|
+
const CONTENT_SPACE_TOKEN = "p-content-space";
|
|
365
|
+
const shellHasContentSpace = (shell) => (0, css_classname_utils_1.splitClasses)(shell.classnameValue).includes(CONTENT_SPACE_TOKEN);
|
|
366
|
+
/**
|
|
367
|
+
* Layout-wins is per route subtree: a layout covers only descendant leaves.
|
|
368
|
+
* Already-migrated descendant leaves must not get a second inset on the layout.
|
|
369
|
+
*/
|
|
370
|
+
const selectShellsForInvent = (routes) => {
|
|
371
|
+
const byVariable = new Map();
|
|
372
|
+
for (const route of routes) {
|
|
373
|
+
if (route.variableName !== null) {
|
|
374
|
+
byVariable.set(route.variableName, route);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const layouts = routes.filter(route => route.analysis?.kind === "layout-shell");
|
|
378
|
+
const leaves = routes.filter(route => route.analysis?.kind === "leaf-shell");
|
|
379
|
+
const coveredLeaves = new Set();
|
|
380
|
+
const selected = [];
|
|
381
|
+
for (const layout of layouts) {
|
|
382
|
+
if (layout.analysis?.kind !== "layout-shell")
|
|
383
|
+
continue;
|
|
384
|
+
const descendants = leaves.filter(leaf => isDescendantOf(leaf, layout, byVariable));
|
|
385
|
+
const descendantsAlreadyMigrated = descendants.some(leaf => leaf.analysis !== null && shellHasContentSpace(leaf.analysis.shell));
|
|
386
|
+
if (descendantsAlreadyMigrated) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
selected.push(layout.analysis.shell);
|
|
390
|
+
for (const descendant of descendants) {
|
|
391
|
+
coveredLeaves.add(descendant);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const leaf of leaves) {
|
|
395
|
+
if (coveredLeaves.has(leaf) || leaf.analysis?.kind !== "leaf-shell")
|
|
396
|
+
continue;
|
|
397
|
+
selected.push(leaf.analysis.shell);
|
|
398
|
+
}
|
|
399
|
+
return selected;
|
|
400
|
+
};
|
|
401
|
+
const isDescendantOf = (leaf, layout, byVariable) => {
|
|
402
|
+
if (layout.variableName === null)
|
|
403
|
+
return false;
|
|
404
|
+
let current = leaf.parentVariableName;
|
|
405
|
+
const seen = new Set();
|
|
406
|
+
while (current !== null && !seen.has(current)) {
|
|
407
|
+
if (current === layout.variableName)
|
|
408
|
+
return true;
|
|
409
|
+
seen.add(current);
|
|
410
|
+
current = byVariable.get(current)?.parentVariableName ?? null;
|
|
411
|
+
}
|
|
412
|
+
return false;
|
|
413
|
+
};
|
|
414
|
+
const uniqueNames = (names) => {
|
|
415
|
+
const seen = new Set();
|
|
416
|
+
const result = [];
|
|
417
|
+
for (const name of names) {
|
|
418
|
+
if (seen.has(name))
|
|
419
|
+
continue;
|
|
420
|
+
seen.add(name);
|
|
421
|
+
result.push(name);
|
|
422
|
+
}
|
|
423
|
+
return result;
|
|
424
|
+
};
|
|
425
|
+
const isRouteFactoryCall = (node) => {
|
|
426
|
+
const directName = getCalleeIdentifierName(node.callee);
|
|
427
|
+
if (directName === "createRootRoute" || directName === "createRoute") {
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
// createRootRouteWithContext<T>()({ ... })
|
|
431
|
+
if (node.callee.type === types_1.AST_NODE_TYPES.CallExpression) {
|
|
432
|
+
const innerName = getCalleeIdentifierName(node.callee.callee);
|
|
433
|
+
return innerName === "createRootRouteWithContext";
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
};
|
|
437
|
+
const getCalleeIdentifierName = (callee) => {
|
|
438
|
+
if (callee.type === types_1.AST_NODE_TYPES.Identifier) {
|
|
439
|
+
return callee.name;
|
|
440
|
+
}
|
|
441
|
+
if (callee.type === types_1.AST_NODE_TYPES.TSInstantiationExpression) {
|
|
442
|
+
return getCalleeIdentifierName(callee.expression);
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
};
|
|
446
|
+
const getComponentIdentifierFromOptions = (options) => {
|
|
447
|
+
for (const property of options.properties) {
|
|
448
|
+
if (property.type !== types_1.AST_NODE_TYPES.Property)
|
|
449
|
+
continue;
|
|
450
|
+
if (property.key.type !== types_1.AST_NODE_TYPES.Identifier || property.key.name !== "component")
|
|
451
|
+
continue;
|
|
452
|
+
if (property.value.type === types_1.AST_NODE_TYPES.Identifier) {
|
|
453
|
+
return property.value.name;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
};
|
|
458
|
+
const resolveComponentModulePath = (tree, program, fromFilePath, sourceRoot, componentName) => {
|
|
459
|
+
const imported = resolveStaticLocalImport(tree, program, fromFilePath, sourceRoot, componentName);
|
|
460
|
+
if (imported !== null) {
|
|
461
|
+
return imported;
|
|
462
|
+
}
|
|
463
|
+
if (findJsxReturnedByName(program, componentName) !== null) {
|
|
464
|
+
return fromFilePath;
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
};
|
|
468
|
+
const elementContainsOutlet = (element) => {
|
|
469
|
+
let found = false;
|
|
470
|
+
(0, typescript_estree_1.simpleTraverse)(element, {
|
|
471
|
+
enter: node => {
|
|
472
|
+
if (found)
|
|
473
|
+
return;
|
|
474
|
+
if (node.type !== types_1.AST_NODE_TYPES.JSXOpeningElement)
|
|
475
|
+
return;
|
|
476
|
+
const name = getJsxOpeningName(node.name);
|
|
477
|
+
if (name === "Outlet") {
|
|
478
|
+
found = true;
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
return found;
|
|
483
|
+
};
|
|
484
|
+
/**
|
|
485
|
+
* True when a div-like shell's `<Outlet />` lives one custom-component hop
|
|
486
|
+
* below (`<div><Main /></div>` where `Main` returns `<Outlet />`). Without this,
|
|
487
|
+
* the ancestor is padded as a leaf alongside real route leaves.
|
|
488
|
+
*/
|
|
489
|
+
const hostWrapsOutletOneHop = (tree, filePath, program, host, sourceRoot, visited) => {
|
|
490
|
+
for (const childName of collectCustomChildComponentNames(host)) {
|
|
491
|
+
if (childName === "Outlet") {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
if (componentReturnsOutletOneHop(tree, filePath, program, childName, sourceRoot, visited)) {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
};
|
|
500
|
+
const collectCustomChildComponentNames = (host) => {
|
|
501
|
+
const names = [];
|
|
502
|
+
for (const child of host.children) {
|
|
503
|
+
if (child.type !== types_1.AST_NODE_TYPES.JSXElement)
|
|
504
|
+
continue;
|
|
505
|
+
const name = getJsxComponentName(child);
|
|
506
|
+
if (name !== null) {
|
|
507
|
+
names.push(name);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return names;
|
|
511
|
+
};
|
|
512
|
+
const componentReturnsOutletOneHop = (tree, fromFilePath, fromProgram, componentName, sourceRoot, visited) => {
|
|
513
|
+
const visitKey = `${fromFilePath}#outlet-hop#${componentName}`;
|
|
514
|
+
if (visited.has(visitKey)) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
visited.add(visitKey);
|
|
518
|
+
let program = fromProgram;
|
|
519
|
+
if (findJsxReturnedByName(fromProgram, componentName) === null) {
|
|
520
|
+
const importPath = resolveStaticLocalImport(tree, fromProgram, fromFilePath, sourceRoot, componentName);
|
|
521
|
+
if (importPath === null) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
const content = tree.read(importPath, "utf-8");
|
|
525
|
+
if (content === null) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
program = (0, classname_ast_rewrite_1.parseTsestreeSource)(content);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const jsx = findJsxReturnedByName(program, componentName);
|
|
536
|
+
if (jsx === null) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
if (getJsxComponentName(jsx) === "Outlet") {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
return elementContainsOutlet(jsx);
|
|
543
|
+
};
|
|
544
|
+
const dedupeShells = (shells) => {
|
|
545
|
+
const seen = new Set();
|
|
546
|
+
const result = [];
|
|
547
|
+
for (const shell of shells) {
|
|
548
|
+
const key = `${shell.filePath}:${shell.location.reportNode.range[0]}:${shell.location.reportNode.range[1]}`;
|
|
549
|
+
if (seen.has(key))
|
|
550
|
+
continue;
|
|
551
|
+
seen.add(key);
|
|
552
|
+
result.push(shell);
|
|
553
|
+
}
|
|
554
|
+
return result;
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Finds the JSX element returned by the entry's primary component:
|
|
558
|
+
* `singleSpaReact({ rootComponent })` when present, otherwise the first
|
|
559
|
+
* exported function/const whose body returns JSX.
|
|
560
|
+
*/
|
|
561
|
+
const findRootJsxElement = (program) => {
|
|
562
|
+
const singleSpaRootName = findSingleSpaRootComponentName(program);
|
|
563
|
+
if (singleSpaRootName !== null) {
|
|
564
|
+
return findJsxReturnedByName(program, singleSpaRootName);
|
|
565
|
+
}
|
|
566
|
+
for (const statement of program.body) {
|
|
567
|
+
const name = getExportedComponentName(statement);
|
|
568
|
+
if (name === null)
|
|
569
|
+
continue;
|
|
570
|
+
const jsx = findJsxReturnedByName(program, name);
|
|
571
|
+
if (jsx !== null)
|
|
572
|
+
return jsx;
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
};
|
|
576
|
+
const findSingleSpaRootComponentName = (program) => {
|
|
577
|
+
let result = null;
|
|
578
|
+
(0, typescript_estree_1.simpleTraverse)(program, {
|
|
579
|
+
enter: node => {
|
|
580
|
+
if (result !== null)
|
|
581
|
+
return;
|
|
582
|
+
if (node.type !== types_1.AST_NODE_TYPES.CallExpression)
|
|
583
|
+
return;
|
|
584
|
+
if (!isCalleeNamed(node.callee, "singleSpaReact"))
|
|
585
|
+
return;
|
|
586
|
+
if (node.arguments[0]?.type !== types_1.AST_NODE_TYPES.ObjectExpression)
|
|
587
|
+
return;
|
|
588
|
+
for (const property of node.arguments[0].properties) {
|
|
589
|
+
if (property.type !== types_1.AST_NODE_TYPES.Property)
|
|
590
|
+
continue;
|
|
591
|
+
if (property.key.type !== types_1.AST_NODE_TYPES.Identifier || property.key.name !== "rootComponent")
|
|
592
|
+
continue;
|
|
593
|
+
if (property.value.type === types_1.AST_NODE_TYPES.Identifier) {
|
|
594
|
+
result = property.value.name;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
return result;
|
|
600
|
+
};
|
|
601
|
+
const getExportedComponentName = (statement) => {
|
|
602
|
+
if (statement.type === types_1.AST_NODE_TYPES.ExportNamedDeclaration) {
|
|
603
|
+
if (statement.declaration?.type === types_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
604
|
+
return statement.declaration.id?.name ?? null;
|
|
605
|
+
}
|
|
606
|
+
if (statement.declaration?.type === types_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
607
|
+
const declarator = statement.declaration.declarations[0];
|
|
608
|
+
if (declarator.id.type === types_1.AST_NODE_TYPES.Identifier) {
|
|
609
|
+
return declarator.id.name;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (statement.type === types_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
614
|
+
if (statement.declaration.type === types_1.AST_NODE_TYPES.Identifier) {
|
|
615
|
+
return statement.declaration.name;
|
|
616
|
+
}
|
|
617
|
+
if (statement.declaration.type === types_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
618
|
+
return statement.declaration.id?.name ?? null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
};
|
|
623
|
+
const findJsxReturnedByName = (program, name) => {
|
|
624
|
+
for (const statement of program.body) {
|
|
625
|
+
const fn = matchNamedFunctionLike(statement, name);
|
|
626
|
+
if (fn !== null) {
|
|
627
|
+
return findReturnedJsx(fn);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return null;
|
|
631
|
+
};
|
|
632
|
+
const matchNamedFunctionLike = (statement, name) => {
|
|
633
|
+
const decl = statement.type === types_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
634
|
+
statement.type === types_1.AST_NODE_TYPES.ExportDefaultDeclaration
|
|
635
|
+
? statement.declaration
|
|
636
|
+
: statement;
|
|
637
|
+
if (decl === null)
|
|
638
|
+
return null;
|
|
639
|
+
if (decl.type === types_1.AST_NODE_TYPES.FunctionDeclaration && decl.id !== null && decl.id.name === name) {
|
|
640
|
+
return decl.body;
|
|
641
|
+
}
|
|
642
|
+
if (decl.type === types_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
643
|
+
for (const declarator of decl.declarations) {
|
|
644
|
+
if (declarator.id.type !== types_1.AST_NODE_TYPES.Identifier || declarator.id.name !== name)
|
|
645
|
+
continue;
|
|
646
|
+
const init = declarator.init;
|
|
647
|
+
if (init === null)
|
|
648
|
+
return null;
|
|
649
|
+
if (init.type === types_1.AST_NODE_TYPES.ArrowFunctionExpression || init.type === types_1.AST_NODE_TYPES.FunctionExpression) {
|
|
650
|
+
return init.body;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (statement.type === types_1.AST_NODE_TYPES.FunctionDeclaration && statement.id.name === name) {
|
|
655
|
+
return statement.body;
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
};
|
|
659
|
+
const findReturnedJsx = (body) => {
|
|
660
|
+
if (body.type === types_1.AST_NODE_TYPES.JSXElement || body.type === types_1.AST_NODE_TYPES.JSXFragment) {
|
|
661
|
+
return body;
|
|
662
|
+
}
|
|
663
|
+
if (body.type === types_1.AST_NODE_TYPES.BlockStatement) {
|
|
664
|
+
for (const statement of body.body) {
|
|
665
|
+
if (statement.type !== types_1.AST_NODE_TYPES.ReturnStatement || statement.argument === null)
|
|
666
|
+
continue;
|
|
667
|
+
const arg = unwrapParentheses(statement.argument);
|
|
668
|
+
if (arg.type === types_1.AST_NODE_TYPES.JSXElement || arg.type === types_1.AST_NODE_TYPES.JSXFragment) {
|
|
669
|
+
return arg;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
};
|
|
675
|
+
const unwrapParentheses = (expression) => {
|
|
676
|
+
let current = expression;
|
|
677
|
+
while (current.type === types_1.AST_NODE_TYPES.TSAsExpression || current.type === types_1.AST_NODE_TYPES.TSSatisfiesExpression) {
|
|
678
|
+
current = current.expression;
|
|
679
|
+
}
|
|
680
|
+
return current;
|
|
681
|
+
};
|
|
682
|
+
/**
|
|
683
|
+
* Skips known provider / Suspense / fragment wrappers down to the first
|
|
684
|
+
* meaningful child (host element or custom component).
|
|
685
|
+
*/
|
|
686
|
+
const unwrapWrappers = (node) => {
|
|
687
|
+
let current = node;
|
|
688
|
+
for (;;) {
|
|
689
|
+
if (current.type === types_1.AST_NODE_TYPES.JSXFragment) {
|
|
690
|
+
const child = firstJsxChild(current.children);
|
|
691
|
+
if (child === null)
|
|
692
|
+
return null;
|
|
693
|
+
current = child;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const name = getJsxComponentName(current);
|
|
697
|
+
if (name !== null && isKnownWrapperName(name)) {
|
|
698
|
+
const child = firstJsxChild(current.children);
|
|
699
|
+
if (child === null)
|
|
700
|
+
return null;
|
|
701
|
+
current = child;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
return current;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
const firstJsxChild = (children) => {
|
|
708
|
+
for (const child of children) {
|
|
709
|
+
if (child.type === types_1.AST_NODE_TYPES.JSXElement || child.type === types_1.AST_NODE_TYPES.JSXFragment) {
|
|
710
|
+
return child;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
};
|
|
715
|
+
const isDivLikeHost = (element) => {
|
|
716
|
+
if (element.type !== types_1.AST_NODE_TYPES.JSXElement)
|
|
717
|
+
return false;
|
|
718
|
+
const name = getJsxOpeningName(element.openingElement.name);
|
|
719
|
+
return name !== null && DIV_LIKE_ELEMENTS.has(name);
|
|
720
|
+
};
|
|
721
|
+
const getJsxComponentName = (element) => {
|
|
722
|
+
if (element.type !== types_1.AST_NODE_TYPES.JSXElement)
|
|
723
|
+
return null;
|
|
724
|
+
const name = getJsxOpeningName(element.openingElement.name);
|
|
725
|
+
if (name === null)
|
|
726
|
+
return null;
|
|
727
|
+
if (DIV_LIKE_ELEMENTS.has(name))
|
|
728
|
+
return null;
|
|
729
|
+
if (/^[a-z]/.test(name))
|
|
730
|
+
return null;
|
|
731
|
+
return name;
|
|
732
|
+
};
|
|
733
|
+
const getJsxOpeningName = (name) => {
|
|
734
|
+
if (name.type === types_1.AST_NODE_TYPES.JSXIdentifier)
|
|
735
|
+
return name.name;
|
|
736
|
+
return null;
|
|
737
|
+
};
|
|
738
|
+
/**
|
|
739
|
+
* Confidence rule for this phase: a single editable content-region classname
|
|
740
|
+
* host that itself owns scrollable overflow (`overflow-auto` / `scroll` /
|
|
741
|
+
* `overlay`, including axis-prefixed forms). Clipping overflow (`hidden` /
|
|
742
|
+
* `clip`), a scrolling descendant, or a header-ish child (`PageHeader` /
|
|
743
|
+
* `Topbar` / `Header`) make the host unresolved — those cases go to the
|
|
744
|
+
* unresolved-surface report for a human to place the inset.
|
|
745
|
+
*
|
|
746
|
+
* Classnames must be bound to the host's `className`/`class` attribute (literal
|
|
747
|
+
* on the attribute, direct Tailwind call in the attribute, or the local
|
|
748
|
+
* binding of that attribute's identifier/callee). Never invent from unrelated
|
|
749
|
+
* file-wide classname locations.
|
|
750
|
+
*/
|
|
751
|
+
const pickContentShellInFile = (filePath, program, host) => {
|
|
752
|
+
const hostClassnames = collectEditableClassnamesForHost(program, host);
|
|
753
|
+
if (hostClassnames.length === 0) {
|
|
754
|
+
return unresolvedShell("no-editable-classname", filePath);
|
|
755
|
+
}
|
|
756
|
+
if (hostClassnames.some(classname => classListHasClippingOverflow((0, css_classname_utils_1.splitClasses)(classname.value)))) {
|
|
757
|
+
return unresolvedShell("clipping-overflow-on-host", filePath);
|
|
758
|
+
}
|
|
759
|
+
const candidates = hostClassnames.filter(isContentShellClassname);
|
|
760
|
+
if (candidates.length === 0) {
|
|
761
|
+
return unresolvedShell("no-overflow-on-host", filePath);
|
|
762
|
+
}
|
|
763
|
+
if (candidates.length > 1) {
|
|
764
|
+
return unresolvedShell("multiple-overflow-candidates", filePath);
|
|
765
|
+
}
|
|
766
|
+
if (hostRendersHeader(host)) {
|
|
767
|
+
return unresolvedShell("header-on-shell", filePath);
|
|
768
|
+
}
|
|
769
|
+
if (descendantOwnsScroll(program, host)) {
|
|
770
|
+
return unresolvedShell("descendant-owns-scroll", filePath);
|
|
771
|
+
}
|
|
772
|
+
const location = candidates[0];
|
|
773
|
+
if (location === undefined) {
|
|
774
|
+
return unresolvedShell("unclear-graph", filePath);
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
status: "resolved",
|
|
778
|
+
shell: {
|
|
779
|
+
filePath,
|
|
780
|
+
classnameValue: location.value,
|
|
781
|
+
combinedClassnameValue: hostClassnames.map(hostLocation => hostLocation.value).join(" "),
|
|
782
|
+
location,
|
|
783
|
+
},
|
|
784
|
+
};
|
|
785
|
+
};
|
|
786
|
+
const collectEditableClassnamesForHost = (program, host) => {
|
|
787
|
+
const attribute = host.openingElement.attributes.find((attr) => attr.type === types_1.AST_NODE_TYPES.JSXAttribute &&
|
|
788
|
+
attr.name.type === types_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
789
|
+
(attr.name.name === "className" || attr.name.name === "class"));
|
|
790
|
+
if (attribute === undefined)
|
|
791
|
+
return [];
|
|
792
|
+
// Direct literal classnames on the attribute.
|
|
793
|
+
const fromAttribute = (0, classname_ast_rewrite_1.findClassnameLocations)(program).filter(location => {
|
|
794
|
+
if (location.context.type !== "jsx-attribute")
|
|
795
|
+
return false;
|
|
796
|
+
return location.reportNode.range[0] >= attribute.range[0] && location.reportNode.range[1] <= attribute.range[1];
|
|
797
|
+
});
|
|
798
|
+
if (fromAttribute.length > 0)
|
|
799
|
+
return fromAttribute;
|
|
800
|
+
const value = attribute.value;
|
|
801
|
+
if (value?.type !== types_1.AST_NODE_TYPES.JSXExpressionContainer)
|
|
802
|
+
return [];
|
|
803
|
+
const expression = value.expression;
|
|
804
|
+
if (expression.type === types_1.AST_NODE_TYPES.CallExpression) {
|
|
805
|
+
return collectClassnamesBoundToCallExpression(program, expression);
|
|
806
|
+
}
|
|
807
|
+
if (expression.type === types_1.AST_NODE_TYPES.Identifier) {
|
|
808
|
+
return collectClassnamesBoundToIdentifier(program, expression.name);
|
|
809
|
+
}
|
|
810
|
+
return [];
|
|
811
|
+
};
|
|
812
|
+
/**
|
|
813
|
+
* `className={cva("...")}` — locations inside this call only.
|
|
814
|
+
* `className={layout()}` — locations inside the local binding of `layout` only.
|
|
815
|
+
*/
|
|
816
|
+
const collectClassnamesBoundToCallExpression = (program, callExpression) => {
|
|
817
|
+
if (callExpression.callee.type !== types_1.AST_NODE_TYPES.Identifier) {
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
const calleeName = callExpression.callee.name;
|
|
821
|
+
if ((0, css_classname_utils_1.isTailwindFunction)(calleeName)) {
|
|
822
|
+
return (0, classname_ast_rewrite_1.findClassnameLocations)(program).filter(location => location.reportNode.range[0] >= callExpression.range[0] &&
|
|
823
|
+
location.reportNode.range[1] <= callExpression.range[1]);
|
|
824
|
+
}
|
|
825
|
+
return collectClassnamesBoundToIdentifier(program, calleeName);
|
|
826
|
+
};
|
|
827
|
+
const collectClassnamesBoundToIdentifier = (program, name) => {
|
|
828
|
+
const bindingRange = findLocalBindingInitializerRange(program, name);
|
|
829
|
+
if (bindingRange === null) {
|
|
830
|
+
return [];
|
|
831
|
+
}
|
|
832
|
+
return (0, classname_ast_rewrite_1.findClassnameLocations)(program).filter(location => location.reportNode.range[0] >= bindingRange[0] && location.reportNode.range[1] <= bindingRange[1]);
|
|
833
|
+
};
|
|
834
|
+
const findLocalBindingInitializerRange = (program, name) => {
|
|
835
|
+
let result = null;
|
|
836
|
+
(0, typescript_estree_1.simpleTraverse)(program, {
|
|
837
|
+
enter: node => {
|
|
838
|
+
if (result !== null)
|
|
839
|
+
return;
|
|
840
|
+
if (node.type !== types_1.AST_NODE_TYPES.VariableDeclarator)
|
|
841
|
+
return;
|
|
842
|
+
if (node.id.type !== types_1.AST_NODE_TYPES.Identifier || node.id.name !== name)
|
|
843
|
+
return;
|
|
844
|
+
if (node.init === null)
|
|
845
|
+
return;
|
|
846
|
+
result = node.init.range;
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
return result;
|
|
850
|
+
};
|
|
851
|
+
const SCROLLABLE_OVERFLOW = /(^|:)overflow-(x-|y-)?(auto|scroll|overlay)$/;
|
|
852
|
+
const CLIPPING_OVERFLOW = /(^|:)overflow-(x-|y-)?(hidden|clip)$/;
|
|
853
|
+
const classListHasScrollableOverflow = (classes) => classes.some(cls => SCROLLABLE_OVERFLOW.test(cls));
|
|
854
|
+
const classListHasClippingOverflow = (classes) => classes.some(cls => CLIPPING_OVERFLOW.test(cls));
|
|
855
|
+
const isContentShellClassname = (location) => {
|
|
856
|
+
const classes = (0, css_classname_utils_1.splitClasses)(location.value);
|
|
857
|
+
if (classListHasClippingOverflow(classes))
|
|
858
|
+
return false;
|
|
859
|
+
return classListHasScrollableOverflow(classes);
|
|
860
|
+
};
|
|
861
|
+
const HEADER_ISH_NAMES = new Set(["PageHeader", "Topbar", "Header"]);
|
|
862
|
+
const hostRendersHeader = (host) => jsxElementsUnder(host).some(element => {
|
|
863
|
+
const name = getJsxOpeningName(element.openingElement.name);
|
|
864
|
+
return name !== null && HEADER_ISH_NAMES.has(name);
|
|
865
|
+
});
|
|
866
|
+
const descendantOwnsScroll = (program, host) => jsxElementsUnder(host).some(element => collectEditableClassnamesForHost(program, element).some(isContentShellClassname));
|
|
867
|
+
const jsxElementsUnder = (host) => {
|
|
868
|
+
const found = [];
|
|
869
|
+
const visit = (children) => {
|
|
870
|
+
for (const child of children) {
|
|
871
|
+
if (child.type === types_1.AST_NODE_TYPES.JSXElement) {
|
|
872
|
+
found.push(child);
|
|
873
|
+
visit(child.children);
|
|
874
|
+
}
|
|
875
|
+
else if (child.type === types_1.AST_NODE_TYPES.JSXFragment) {
|
|
876
|
+
visit(child.children);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
visit(host.children);
|
|
881
|
+
return found;
|
|
882
|
+
};
|
|
883
|
+
const isDynamicComponent = (program, componentName) => {
|
|
884
|
+
let isDynamic = false;
|
|
885
|
+
(0, typescript_estree_1.simpleTraverse)(program, {
|
|
886
|
+
enter: node => {
|
|
887
|
+
if (isDynamic)
|
|
888
|
+
return;
|
|
889
|
+
if (node.type !== types_1.AST_NODE_TYPES.VariableDeclarator)
|
|
890
|
+
return;
|
|
891
|
+
if (node.id.type !== types_1.AST_NODE_TYPES.Identifier || node.id.name !== componentName)
|
|
892
|
+
return;
|
|
893
|
+
if (node.init === null)
|
|
894
|
+
return;
|
|
895
|
+
if (containsDynamicImport(node.init)) {
|
|
896
|
+
isDynamic = true;
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
return isDynamic;
|
|
901
|
+
};
|
|
902
|
+
const containsDynamicImport = (node) => {
|
|
903
|
+
let found = false;
|
|
904
|
+
(0, typescript_estree_1.simpleTraverse)(node, {
|
|
905
|
+
enter: child => {
|
|
906
|
+
if (found)
|
|
907
|
+
return;
|
|
908
|
+
if (child.type === types_1.AST_NODE_TYPES.ImportExpression) {
|
|
909
|
+
found = true;
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
});
|
|
913
|
+
return found;
|
|
914
|
+
};
|
|
915
|
+
/**
|
|
916
|
+
* Resolves a component name to a static relative/local import under
|
|
917
|
+
* `sourceRoot`. Returns null for external packages or missing bindings.
|
|
918
|
+
*/
|
|
919
|
+
const resolveStaticLocalImport = (tree, program, fromFilePath, sourceRoot, componentName) => {
|
|
920
|
+
for (const statement of program.body) {
|
|
921
|
+
if (statement.type !== types_1.AST_NODE_TYPES.ImportDeclaration)
|
|
922
|
+
continue;
|
|
923
|
+
const specifier = statement.source.value;
|
|
924
|
+
if (typeof specifier !== "string")
|
|
925
|
+
continue;
|
|
926
|
+
const bindsComponent = statement.specifiers.some(spec => {
|
|
927
|
+
if (spec.type === types_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
928
|
+
return spec.local.name === componentName;
|
|
929
|
+
}
|
|
930
|
+
if (spec.type === types_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
931
|
+
return spec.local.name === componentName;
|
|
932
|
+
}
|
|
933
|
+
return false;
|
|
934
|
+
});
|
|
935
|
+
if (!bindsComponent)
|
|
936
|
+
continue;
|
|
937
|
+
if (!(specifier.startsWith("./") || specifier.startsWith("../"))) {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
const fromDir = path_1.default.posix.dirname(fromFilePath);
|
|
941
|
+
const resolvedWithoutExt = path_1.default.posix.normalize(path_1.default.posix.join(fromDir, specifier));
|
|
942
|
+
const candidates = [
|
|
943
|
+
resolvedWithoutExt,
|
|
944
|
+
`${resolvedWithoutExt}.tsx`,
|
|
945
|
+
`${resolvedWithoutExt}.ts`,
|
|
946
|
+
`${resolvedWithoutExt}/index.tsx`,
|
|
947
|
+
`${resolvedWithoutExt}/index.ts`,
|
|
948
|
+
];
|
|
949
|
+
for (const candidate of candidates) {
|
|
950
|
+
if (candidate === fromFilePath)
|
|
951
|
+
continue;
|
|
952
|
+
if (!candidate.startsWith(`${sourceRoot}/`) && candidate !== sourceRoot)
|
|
953
|
+
continue;
|
|
954
|
+
if (!(candidate.endsWith(".tsx") || candidate.endsWith(".ts")))
|
|
955
|
+
continue;
|
|
956
|
+
if (tree.exists(candidate)) {
|
|
957
|
+
return candidate;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
// Local const binding (e.g. LazyPage) without an import — not followable as a
|
|
962
|
+
// static local module.
|
|
963
|
+
return null;
|
|
964
|
+
};
|
|
965
|
+
const isCalleeNamed = (callee, name) => {
|
|
966
|
+
return callee.type === types_1.AST_NODE_TYPES.Identifier && callee.name === name;
|
|
967
|
+
};
|
|
968
|
+
//# sourceMappingURL=resolve-content-shell.js.map
|