@trackunit/iris-app 2.5.11 → 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 +22 -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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## 2.5.13 (2026-08-20)
|
|
2
|
+
|
|
3
|
+
### 🧱 Updated Dependencies
|
|
4
|
+
|
|
5
|
+
- Updated iris-app-build-utilities to 2.4.13
|
|
6
|
+
- Updated iris-app-api to 2.4.9
|
|
7
|
+
- Updated react-vite-test-setup to 0.0.112
|
|
8
|
+
- Updated css-classname-utils to 0.0.20
|
|
9
|
+
- Updated react-graphql-tools to 1.15.17
|
|
10
|
+
- Updated shared-utils to 1.16.20
|
|
11
|
+
|
|
12
|
+
## 2.5.12 (2026-08-20)
|
|
13
|
+
|
|
14
|
+
### 🧱 Updated Dependencies
|
|
15
|
+
|
|
16
|
+
- Updated iris-app-build-utilities to 2.4.12
|
|
17
|
+
- Updated iris-app-api to 2.4.8
|
|
18
|
+
- Updated react-vite-test-setup to 0.0.111
|
|
19
|
+
- Updated css-classname-utils to 0.0.19
|
|
20
|
+
- Updated react-graphql-tools to 1.15.16
|
|
21
|
+
- Updated shared-utils to 1.16.19
|
|
22
|
+
|
|
1
23
|
## 2.5.11 (2026-08-19)
|
|
2
24
|
|
|
3
25
|
### 🧱 Updated Dependencies
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inventContentSpace = exports.ASSET_SITE_ADMIN_TYPES = void 0;
|
|
4
|
+
const css_classname_utils_1 = require("@trackunit/css-classname-utils");
|
|
5
|
+
const types_1 = require("@typescript-eslint/types");
|
|
6
|
+
const typescript_estree_1 = require("@typescript-eslint/typescript-estree");
|
|
7
|
+
const classname_ast_rewrite_1 = require("../utils/classname-ast-rewrite");
|
|
8
|
+
const report_unresolved_1 = require("./report-unresolved");
|
|
9
|
+
const resolve_content_shell_1 = require("./resolve-content-shell");
|
|
10
|
+
const CONTENT_SPACE_TOKEN = "p-content-space";
|
|
11
|
+
/**
|
|
12
|
+
* Extension types that may invent/convert content-shell padding. Fleet/Customer
|
|
13
|
+
* remain swap-only (page-content rewrite) and must never invent or convert.
|
|
14
|
+
* Shared with the orchestrator so the ASA gate lives in exactly one place.
|
|
15
|
+
*/
|
|
16
|
+
exports.ASSET_SITE_ADMIN_TYPES = new Set([
|
|
17
|
+
"ASSET_HOME_EXTENSION",
|
|
18
|
+
"SITE_HOME_EXTENSION",
|
|
19
|
+
"ADMIN_EXTENSION",
|
|
20
|
+
]);
|
|
21
|
+
const SHELL_INSET_SIDES = ["p", "px", "py", "pt", "pr", "pb", "pl"];
|
|
22
|
+
const SHELL_INSET_VALUES = [
|
|
23
|
+
"1",
|
|
24
|
+
"1.5",
|
|
25
|
+
"2",
|
|
26
|
+
"2.5",
|
|
27
|
+
"3",
|
|
28
|
+
"3.5",
|
|
29
|
+
"4",
|
|
30
|
+
"5",
|
|
31
|
+
"6",
|
|
32
|
+
"7",
|
|
33
|
+
"8",
|
|
34
|
+
"9",
|
|
35
|
+
"10",
|
|
36
|
+
"11",
|
|
37
|
+
"12",
|
|
38
|
+
"14",
|
|
39
|
+
"16",
|
|
40
|
+
"20",
|
|
41
|
+
"24",
|
|
42
|
+
"28",
|
|
43
|
+
"32",
|
|
44
|
+
"36",
|
|
45
|
+
"40",
|
|
46
|
+
"44",
|
|
47
|
+
"48",
|
|
48
|
+
"52",
|
|
49
|
+
"56",
|
|
50
|
+
"60",
|
|
51
|
+
"64",
|
|
52
|
+
"72",
|
|
53
|
+
"80",
|
|
54
|
+
"96",
|
|
55
|
+
"responsive-space",
|
|
56
|
+
];
|
|
57
|
+
const BREAKPOINT_VARIANTS = ["sm", "md", "lg", "xl", "2xl"];
|
|
58
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
59
|
+
/**
|
|
60
|
+
* Tailwind padding utilities treated as content-region shell inset candidates.
|
|
61
|
+
* Enumerated (not `.+`) so reset tokens (`px-0`, `pb-0.5`) and state/print
|
|
62
|
+
* variants (`hover:`, `focus:`, `print:`) are left alone. Breakpoint variants
|
|
63
|
+
* (`md:p-6`) are still converted — those are lossy and logged as such.
|
|
64
|
+
*/
|
|
65
|
+
const SHELL_PADDING_TOKEN = new RegExp(`^(?:(?:${BREAKPOINT_VARIANTS.join("|")}):)?(?:${SHELL_INSET_SIDES.join("|")})-(?:${SHELL_INSET_VALUES.map(escapeRegExp).join("|")})$`);
|
|
66
|
+
const tokenBase = (token) => (token.includes(":") ? token.slice(token.lastIndexOf(":") + 1) : token);
|
|
67
|
+
const isContentSpaceToken = (token) => tokenBase(token) === CONTENT_SPACE_TOKEN;
|
|
68
|
+
const isShellPaddingToken = (token) => {
|
|
69
|
+
if (isContentSpaceToken(token)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return SHELL_PADDING_TOKEN.test(token);
|
|
73
|
+
};
|
|
74
|
+
const emptyResult = (unresolvedReason = null) => ({
|
|
75
|
+
filesChanged: [],
|
|
76
|
+
changes: [],
|
|
77
|
+
skippedRouteNames: [],
|
|
78
|
+
unresolvedReason,
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* Ensures each resolved ASA content shell carries `p-content-space`:
|
|
82
|
+
* - invents the token on a bare shell (reported as `addition`)
|
|
83
|
+
* - converts custom shell padding (`p-*` / `px-*` / `py-*` / sides /
|
|
84
|
+
* `p-responsive-space` / `pb-responsive-space`) to `p-content-space`,
|
|
85
|
+
* removing those custom tokens (reported as `conversion`)
|
|
86
|
+
*
|
|
87
|
+
* Uses {@link resolveContentShells}: shared layout Outlet shells yield one
|
|
88
|
+
* target; multi-route without a shared layout invents/converts each clear
|
|
89
|
+
* leaf. Only classname locations bound to each host are edited — inner layout
|
|
90
|
+
* padding is left alone. Idempotent when a shell already has `p-content-space`
|
|
91
|
+
* (including a variant-prefixed form) and no custom shell padding (including
|
|
92
|
+
* whitespace-only differences). Never invents an `overflow-*` token — existing
|
|
93
|
+
* scroll ownership is preserved. Fleet/Customer types are a no-op (swap-only
|
|
94
|
+
* surfaces).
|
|
95
|
+
*
|
|
96
|
+
* Returns empty `filesChanged` / `changes` (never throws) when shells are
|
|
97
|
+
* unresolved, the extension type is out of scope, or already in the desired state.
|
|
98
|
+
* `skippedRouteNames` names unanalysable routes on a partial graph so the
|
|
99
|
+
* orchestrator can report them instead of treating the extension as fully migrated.
|
|
100
|
+
*/
|
|
101
|
+
const inventContentSpace = (tree, extension) => {
|
|
102
|
+
if (!exports.ASSET_SITE_ADMIN_TYPES.has(extension.type)) {
|
|
103
|
+
return emptyResult();
|
|
104
|
+
}
|
|
105
|
+
const resolved = (0, resolve_content_shell_1.resolveContentShells)(tree, extension);
|
|
106
|
+
if (resolved.status === "unresolved") {
|
|
107
|
+
return emptyResult(resolved.reason);
|
|
108
|
+
}
|
|
109
|
+
const skippedRouteNames = [...resolved.skippedRouteNames];
|
|
110
|
+
// Single resolved shell that already carries the token (or page-content):
|
|
111
|
+
// do not invent a second inset. A token on a *nested* file is not enough —
|
|
112
|
+
// that hid Device Administration's layout shell (GLU-1903). Multi-route
|
|
113
|
+
// graphs skip this so a padded sibling leaf does not leave other leaves flush.
|
|
114
|
+
if (resolved.shells.length === 1 && (0, report_unresolved_1.findUnresolvedSurface)(tree, extension) === null) {
|
|
115
|
+
return { filesChanged: [], changes: [], skippedRouteNames, unresolvedReason: null };
|
|
116
|
+
}
|
|
117
|
+
const shellsByFile = groupShellsByFile(resolved.shells);
|
|
118
|
+
const filesChanged = [];
|
|
119
|
+
const changes = [];
|
|
120
|
+
for (const [filePath, shells] of shellsByFile) {
|
|
121
|
+
const content = tree.read(filePath, "utf-8");
|
|
122
|
+
if (content === null) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
let program;
|
|
126
|
+
try {
|
|
127
|
+
program = (0, classname_ast_rewrite_1.parseTsestreeSource)(content);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// Edit later ranges first so earlier offsets stay valid in one applyEdits pass.
|
|
133
|
+
const orderedShells = [...shells].sort((left, right) => right.location.reportNode.range[0] - left.location.reportNode.range[0]);
|
|
134
|
+
const fileEdits = [];
|
|
135
|
+
const fileChanges = [];
|
|
136
|
+
for (const shell of orderedShells) {
|
|
137
|
+
const hostLocations = collectHostClassnameLocations(program, shell.location);
|
|
138
|
+
const combinedBefore = hostLocations.map(hostLocation => hostLocation.value).join(" ");
|
|
139
|
+
const combinedPlan = planContentSpaceOnShell(combinedBefore);
|
|
140
|
+
if (!combinedPlan.changed) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const alreadyHasContentSpace = (0, css_classname_utils_1.splitClasses)(combinedBefore).some(isContentSpaceToken);
|
|
144
|
+
const primaryRange = shell.location.fixNode.range;
|
|
145
|
+
const orderedLocations = [...hostLocations].sort((left, right) => {
|
|
146
|
+
const leftIsPrimary = rangesEqual(left.fixNode.range, primaryRange);
|
|
147
|
+
const rightIsPrimary = rangesEqual(right.fixNode.range, primaryRange);
|
|
148
|
+
if (leftIsPrimary === rightIsPrimary) {
|
|
149
|
+
return left.fixNode.range[0] - right.fixNode.range[0];
|
|
150
|
+
}
|
|
151
|
+
return leftIsPrimary ? -1 : 1;
|
|
152
|
+
});
|
|
153
|
+
let addedToken = alreadyHasContentSpace;
|
|
154
|
+
const shellEdits = [];
|
|
155
|
+
for (const hostLocation of orderedLocations) {
|
|
156
|
+
const locationEdits = (0, classname_ast_rewrite_1.buildEditsForLocation)(hostLocation, value => {
|
|
157
|
+
const next = transformClassnamePreservingWhitespace(value, { addContentSpace: !addedToken });
|
|
158
|
+
if (!addedToken && (0, css_classname_utils_1.splitClasses)(next).some(isContentSpaceToken)) {
|
|
159
|
+
addedToken = true;
|
|
160
|
+
}
|
|
161
|
+
return next;
|
|
162
|
+
}, content);
|
|
163
|
+
shellEdits.push(...locationEdits);
|
|
164
|
+
}
|
|
165
|
+
if (shellEdits.length === 0) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
fileEdits.push(...shellEdits);
|
|
169
|
+
fileChanges.push({
|
|
170
|
+
extensionId: extension.id,
|
|
171
|
+
sourceRoot: extension.sourceRoot,
|
|
172
|
+
filePath,
|
|
173
|
+
kind: combinedPlan.removedTokens.length > 0 ? "conversion" : "addition",
|
|
174
|
+
before: shell.classnameValue,
|
|
175
|
+
after: planContentSpaceOnShell(shell.location.value).next,
|
|
176
|
+
removedTokens: combinedPlan.removedTokens,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (fileEdits.length === 0) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
tree.write(filePath, (0, classname_ast_rewrite_1.applyEdits)(content, fileEdits));
|
|
183
|
+
filesChanged.push(filePath);
|
|
184
|
+
changes.push(...fileChanges.reverse());
|
|
185
|
+
}
|
|
186
|
+
if (filesChanged.length === 0) {
|
|
187
|
+
return { filesChanged: [], changes: [], skippedRouteNames, unresolvedReason: null };
|
|
188
|
+
}
|
|
189
|
+
return { filesChanged, changes, skippedRouteNames, unresolvedReason: null };
|
|
190
|
+
};
|
|
191
|
+
exports.inventContentSpace = inventContentSpace;
|
|
192
|
+
const groupShellsByFile = (shells) => {
|
|
193
|
+
const byFile = new Map();
|
|
194
|
+
for (const shell of shells) {
|
|
195
|
+
const existing = byFile.get(shell.filePath);
|
|
196
|
+
if (existing === undefined) {
|
|
197
|
+
byFile.set(shell.filePath, [shell]);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
existing.push(shell);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return byFile;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Strips custom shell-inset padding tokens and ensures `p-content-space` is
|
|
207
|
+
* present, preserving the relative order of non-padding tokens.
|
|
208
|
+
*
|
|
209
|
+
* `changed` is true only when custom padding was stripped or `p-content-space`
|
|
210
|
+
* was missing — not when the only difference would be whitespace normalization.
|
|
211
|
+
*/
|
|
212
|
+
const planContentSpaceOnShell = (classnameValue) => {
|
|
213
|
+
const classes = (0, css_classname_utils_1.splitClasses)(classnameValue);
|
|
214
|
+
const removedTokens = classes.filter(isShellPaddingToken);
|
|
215
|
+
const withoutCustomPadding = classes.filter(token => !isShellPaddingToken(token));
|
|
216
|
+
const hadContentSpace = withoutCustomPadding.some(isContentSpaceToken);
|
|
217
|
+
const nextClasses = hadContentSpace ? withoutCustomPadding : [...withoutCustomPadding, CONTENT_SPACE_TOKEN];
|
|
218
|
+
return {
|
|
219
|
+
next: nextClasses.join(" "),
|
|
220
|
+
changed: removedTokens.length > 0 || !hadContentSpace,
|
|
221
|
+
removedTokens,
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
const transformClassnamePreservingWhitespace = (value, options) => {
|
|
225
|
+
const trimmedStart = value.length - value.trimStart().length;
|
|
226
|
+
const trimmedEnd = value.length - value.trimEnd().length;
|
|
227
|
+
const leading = value.slice(0, trimmedStart);
|
|
228
|
+
const trailing = trimmedStart === value.length ? "" : value.slice(value.length - trimmedEnd);
|
|
229
|
+
const classes = (0, css_classname_utils_1.splitClasses)(value);
|
|
230
|
+
const kept = classes.filter(token => !isShellPaddingToken(token));
|
|
231
|
+
const hasToken = kept.some(isContentSpaceToken);
|
|
232
|
+
const nextClasses = options.addContentSpace && !hasToken ? [...kept, CONTENT_SPACE_TOKEN] : kept;
|
|
233
|
+
const core = nextClasses.join(" ");
|
|
234
|
+
if (core === "") {
|
|
235
|
+
return `${leading}${trailing}`;
|
|
236
|
+
}
|
|
237
|
+
return `${leading}${core}${trailing}`;
|
|
238
|
+
};
|
|
239
|
+
const rangesEqual = (left, right) => left[0] === right[0] && left[1] === right[1];
|
|
240
|
+
/**
|
|
241
|
+
* All editable classname args bound to the same host as `primary` (every
|
|
242
|
+
* string argument of a `cn()` / `cva()` call). Plain JSX literals stay a
|
|
243
|
+
* single location. CVA variant objects are excluded so variant padding is
|
|
244
|
+
* not stripped as if it were shell inset.
|
|
245
|
+
*/
|
|
246
|
+
const collectHostClassnameLocations = (program, primary) => {
|
|
247
|
+
if (primary.context.type !== "function-call" && primary.context.type !== "array-element") {
|
|
248
|
+
return [primary];
|
|
249
|
+
}
|
|
250
|
+
const callRange = findSmallestEnclosingCallRange(program, primary.reportNode);
|
|
251
|
+
if (callRange === null) {
|
|
252
|
+
return [primary];
|
|
253
|
+
}
|
|
254
|
+
const siblings = (0, classname_ast_rewrite_1.findClassnameLocations)(program).filter(location => {
|
|
255
|
+
if (location.reportNode.range[0] < callRange[0] || location.reportNode.range[1] > callRange[1]) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return location.context.type === "function-call" || location.context.type === "array-element";
|
|
259
|
+
});
|
|
260
|
+
return siblings.length > 0 ? siblings : [primary];
|
|
261
|
+
};
|
|
262
|
+
const findSmallestEnclosingCallRange = (program, inner) => {
|
|
263
|
+
let best = null;
|
|
264
|
+
(0, typescript_estree_1.simpleTraverse)(program, {
|
|
265
|
+
enter: node => {
|
|
266
|
+
if (node.type !== types_1.AST_NODE_TYPES.CallExpression)
|
|
267
|
+
return;
|
|
268
|
+
if (inner.range[0] < node.range[0] || inner.range[1] > node.range[1])
|
|
269
|
+
return;
|
|
270
|
+
if (best === null || node.range[1] - node.range[0] < best[1] - best[0]) {
|
|
271
|
+
best = node.range;
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
return best;
|
|
276
|
+
};
|
|
277
|
+
//# sourceMappingURL=invent-content-space.js.map
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pContentSpaceCodemod = pContentSpaceCodemod;
|
|
4
|
+
const devkit_1 = require("@nx/devkit");
|
|
5
|
+
const resolve_extension_types_1 = require("../utils/resolve-extension-types");
|
|
6
|
+
const invent_content_space_1 = require("./invent-content-space");
|
|
7
|
+
const remove_redundant_padding_1 = require("./remove-redundant-padding");
|
|
8
|
+
const report_run_messaging_1 = require("./report-run-messaging");
|
|
9
|
+
const report_unresolved_1 = require("./report-unresolved");
|
|
10
|
+
const rewrite_page_content_1 = require("./rewrite-page-content");
|
|
11
|
+
const MIGRATION_NAME = "p-content-space-codemod";
|
|
12
|
+
/**
|
|
13
|
+
* Extension types that get the swap-only `page-content` -> `overflow-auto
|
|
14
|
+
* p-content-space` rewrite (GLU-1732). Unlike `ASSET_SITE_ADMIN_TYPES`, these
|
|
15
|
+
* are always expected to already have a `page-content` region, so redundant-
|
|
16
|
+
* padding removal, invent/convert, and unresolved-surface reporting don't apply
|
|
17
|
+
* to this group — Fleet/Customer Home never received platform inset.
|
|
18
|
+
*/
|
|
19
|
+
const FLEET_CUSTOMER_HOME_TYPES = new Set(["FLEET_EXTENSION", "CUSTOMER_HOME_EXTENSION"]);
|
|
20
|
+
/**
|
|
21
|
+
* Composes the extension-type resolver with the page-content rewrite,
|
|
22
|
+
* redundant-padding removal, content-shell invent/convert, change reporting,
|
|
23
|
+
* and unresolved-surface report for every resolved Asset Home/Site Home/Admin
|
|
24
|
+
* extension in the workspace, and the swap-only page-content rewrite for every
|
|
25
|
+
* resolved Fleet/Customer Home extension. Every other extension type is left
|
|
26
|
+
* untouched.
|
|
27
|
+
*
|
|
28
|
+
* Ordering for Asset Home/Site Home/Admin:
|
|
29
|
+
* 1. `rewritePageContent` — so a just-migrated region already carries
|
|
30
|
+
* `p-content-space` before invent and padding cleanup look for it.
|
|
31
|
+
* 2. `inventContentSpace` — invents or converts `p-content-space` on a
|
|
32
|
+
* high-confidence content shell (ASA only); change reports distinguish
|
|
33
|
+
* addition vs conversion.
|
|
34
|
+
* 3. `removeRedundantPadding` — only strips `pb-*` from classname strings
|
|
35
|
+
* that carry `p-content-space`, including tokens invent just added, so
|
|
36
|
+
* unresolved extensions (no padded region) and local component spacing
|
|
37
|
+
* are left alone by construction.
|
|
38
|
+
* 4. `findUnresolvedSurface` — against post-rewrite/invent Tree state;
|
|
39
|
+
* invented/converted surfaces must not be reported as unresolved.
|
|
40
|
+
*
|
|
41
|
+
* Formatting is intentionally the caller's job: `@trackunit/migrations`'
|
|
42
|
+
* `run-migrations` generator already awaits `formatFiles(tree)` after every
|
|
43
|
+
* migration. Invoking this orchestrator directly (e.g. against an `FsTree`
|
|
44
|
+
* for an in-repo corpus run) must also call `formatFiles` before flushing,
|
|
45
|
+
* otherwise Prettier/Tailwind class order will leave migrated files failing
|
|
46
|
+
* the format gate.
|
|
47
|
+
*/
|
|
48
|
+
function pContentSpaceCodemod(tree) {
|
|
49
|
+
const { resolved, unresolved } = (0, resolve_extension_types_1.resolveExtensionTypes)(tree);
|
|
50
|
+
const filesChanged = new Set();
|
|
51
|
+
const changes = [];
|
|
52
|
+
const reports = [];
|
|
53
|
+
let asaWorkOccurred = false;
|
|
54
|
+
for (const extension of resolved) {
|
|
55
|
+
if (invent_content_space_1.ASSET_SITE_ADMIN_TYPES.has(extension.type)) {
|
|
56
|
+
asaWorkOccurred = true;
|
|
57
|
+
addAll(filesChanged, (0, rewrite_page_content_1.rewritePageContent)(tree, extension).filesChanged);
|
|
58
|
+
const inventResult = (0, invent_content_space_1.inventContentSpace)(tree, extension);
|
|
59
|
+
addAll(filesChanged, inventResult.filesChanged);
|
|
60
|
+
changes.push(...inventResult.changes);
|
|
61
|
+
// Runs after invent so a just-invented shell also loses redundant `pb-*`.
|
|
62
|
+
// Removal is keyed on `p-content-space` being present in the same
|
|
63
|
+
// classname string, so unresolved surfaces are a no-op.
|
|
64
|
+
addAll(filesChanged, (0, remove_redundant_padding_1.removeRedundantPadding)(tree, extension).filesChanged);
|
|
65
|
+
const entryComponentPath = `${extension.sourceRoot}/${extension.main ?? "index.tsx"}`;
|
|
66
|
+
for (const skippedRouteName of inventResult.skippedRouteNames) {
|
|
67
|
+
reports.push({
|
|
68
|
+
extensionId: extension.id,
|
|
69
|
+
sourceRoot: extension.sourceRoot,
|
|
70
|
+
entryComponentPath: tree.exists(entryComponentPath) ? entryComponentPath : null,
|
|
71
|
+
docsLink: report_unresolved_1.PADDING_DOCS_LINK,
|
|
72
|
+
skippedRouteName,
|
|
73
|
+
skipReason: {
|
|
74
|
+
code: "router-no-analysable-leaves",
|
|
75
|
+
message: `route "${skippedRouteName}" could not be analysed into an inventable overflow content shell`,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const report = (0, report_unresolved_1.findUnresolvedSurface)(tree, extension);
|
|
80
|
+
if (report !== null) {
|
|
81
|
+
reports.push({
|
|
82
|
+
...report,
|
|
83
|
+
skipReason: inventResult.unresolvedReason ?? report.skipReason,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (FLEET_CUSTOMER_HOME_TYPES.has(extension.type)) {
|
|
89
|
+
addAll(filesChanged, (0, rewrite_page_content_1.rewritePageContent)(tree, extension).filesChanged);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const lossyConversionCount = changes.filter(isLossyConversion).length;
|
|
94
|
+
logSummary(filesChanged.size, reports.length + lossyConversionCount);
|
|
95
|
+
for (const change of changes) {
|
|
96
|
+
const message = formatChangeReport(change);
|
|
97
|
+
if (isLossyConversion(change)) {
|
|
98
|
+
devkit_1.logger.warn(message);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
devkit_1.logger.info(message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const report of reports) {
|
|
105
|
+
devkit_1.logger.warn((0, report_run_messaging_1.formatUnresolvedReport)(report));
|
|
106
|
+
}
|
|
107
|
+
for (const message of (0, report_run_messaging_1.collectAsaEndOfRunMessages)({ asaWorkOccurred, changes, unresolvedReports: reports })) {
|
|
108
|
+
devkit_1.logger.info(message);
|
|
109
|
+
}
|
|
110
|
+
for (const item of unresolved) {
|
|
111
|
+
devkit_1.logger.warn(` ${MIGRATION_NAME}: could not resolve extension import "${item.specifier}" referenced by app "${item.app}"`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function addAll(target, items) {
|
|
115
|
+
for (const item of items)
|
|
116
|
+
target.add(item);
|
|
117
|
+
}
|
|
118
|
+
const isLossyConversion = (change) => change.kind === "conversion" && change.removedTokens.some(token => token.includes(":") || /-(?:0|0\.5)$/.test(token));
|
|
119
|
+
/** Mirrors the log style of `logSummary` in `libs/react/components/migrations/utils/jsx-utils.ts`. */
|
|
120
|
+
function logSummary(touched, manualReviewNeeded) {
|
|
121
|
+
if (touched === 0) {
|
|
122
|
+
devkit_1.logger.info(` ${MIGRATION_NAME}: no files changed`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const reviewSuffix = manualReviewNeeded > 0 ? ` (${manualReviewNeeded} location(s) need manual review)` : "";
|
|
126
|
+
devkit_1.logger.info(` ${MIGRATION_NAME}: updated ${touched} file(s)${reviewSuffix}`);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Explicit addition/conversion log so a third-party reading the migration
|
|
130
|
+
* output knows exactly what invent/convert did on each shell.
|
|
131
|
+
*/
|
|
132
|
+
function formatChangeReport(change) {
|
|
133
|
+
if (change.kind === "addition") {
|
|
134
|
+
return ` ${MIGRATION_NAME}: addition on "${change.extensionId}" (${change.filePath}): "${change.before}" → "${change.after}"`;
|
|
135
|
+
}
|
|
136
|
+
const removed = change.removedTokens.join(", ");
|
|
137
|
+
return ` ${MIGRATION_NAME}: conversion on "${change.extensionId}" (${change.filePath}): removed [${removed}]; "${change.before}" → "${change.after}"`;
|
|
138
|
+
}
|
|
139
|
+
exports.default = pContentSpaceCodemod;
|
|
140
|
+
//# sourceMappingURL=orchestrator.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeRedundantPadding = removeRedundantPadding;
|
|
4
|
+
const css_classname_utils_1 = require("@trackunit/css-classname-utils");
|
|
5
|
+
const classname_ast_rewrite_1 = require("../utils/classname-ast-rewrite");
|
|
6
|
+
const REDUNDANT_INSET_TOKENS = ["pb-8", "pb-6", "pb-4", "pb-responsive-space"];
|
|
7
|
+
const PADDED_REGION_TOKEN = "p-content-space";
|
|
8
|
+
/**
|
|
9
|
+
* Removes the redundant bottom-inset Tailwind tokens (`pb-8`, `pb-6`, `pb-4`,
|
|
10
|
+
* `pb-responsive-space`) from classname strings that already carry
|
|
11
|
+
* `p-content-space` under a single resolved extension's `sourceRoot`.
|
|
12
|
+
*
|
|
13
|
+
* Scoped to the padded region on purpose: local `pb-*` used as component
|
|
14
|
+
* spacing (cards, timeline entries, wizard steps) must not be stripped just
|
|
15
|
+
* because they live under the same `sourceRoot`. Callers must run
|
|
16
|
+
* `rewritePageContent` first so a just-migrated `page-content` region already
|
|
17
|
+
* reads as `p-content-space` before this transform looks for it. Extensions
|
|
18
|
+
* with no padded region (the ones `findUnresolvedSurface` will report) are
|
|
19
|
+
* therefore a complete no-op here — there is no `p-content-space` token to
|
|
20
|
+
* key off.
|
|
21
|
+
*
|
|
22
|
+
* Scoped to one extension per call — the caller (the orchestrator) loops
|
|
23
|
+
* over every Asset Home/Site Home/Admin extension.
|
|
24
|
+
*/
|
|
25
|
+
function removeRedundantPadding(tree, extension) {
|
|
26
|
+
return (0, classname_ast_rewrite_1.transformClassnamesUnderSourceRoot)(tree, extension.sourceRoot, content => content.includes(PADDED_REGION_TOKEN) && REDUNDANT_INSET_TOKENS.some(token => content.includes(token)), value => {
|
|
27
|
+
if (!(0, css_classname_utils_1.splitClasses)(value).includes(PADDED_REGION_TOKEN)) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
let result = value;
|
|
31
|
+
for (const token of REDUNDANT_INSET_TOKENS) {
|
|
32
|
+
result = (0, css_classname_utils_1.replaceClassToken)(result, token, "");
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=remove-redundant-padding.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectAsaEndOfRunMessages = exports.buildScrollbarAdvisoryMessages = exports.formatUnresolvedReport = exports.buildUnresolvedInventGuidance = exports.buildVerifyAndEdgeToEdgeReminder = void 0;
|
|
4
|
+
const css_classname_utils_1 = require("@trackunit/css-classname-utils");
|
|
5
|
+
const report_unresolved_1 = require("./report-unresolved");
|
|
6
|
+
const MIGRATION_NAME = "p-content-space-codemod";
|
|
7
|
+
/**
|
|
8
|
+
* Developer-facing end-of-run reminder: verify appearance after migration, and
|
|
9
|
+
* note that intentional edge-to-edge (no content inset) remains an allowed choice.
|
|
10
|
+
* Emitted at most once per run when any ASA work happened — including runs that
|
|
11
|
+
* only produced unresolved reports and no invent/convert edits.
|
|
12
|
+
*/
|
|
13
|
+
const buildVerifyAndEdgeToEdgeReminder = () => ` ${MIGRATION_NAME}: verify UI after migration; edge-to-edge content (no p-content-space) is an allowed choice when intentional`;
|
|
14
|
+
exports.buildVerifyAndEdgeToEdgeReminder = buildVerifyAndEdgeToEdgeReminder;
|
|
15
|
+
/**
|
|
16
|
+
* Explains what an inventable overflow content shell is when the migration
|
|
17
|
+
* could not auto-pad one or more Asset/Site/Admin surfaces. Emitted once per
|
|
18
|
+
* run that produced unresolved reports.
|
|
19
|
+
*/
|
|
20
|
+
const buildUnresolvedInventGuidance = () => ` ${MIGRATION_NAME}: could not auto-invent p-content-space on the surface(s) above — invent only targets a single inventable overflow content shell (a div/main/section with an editable className that itself owns overflow-auto/scroll, with no PageHeader/Topbar child). Card-only roots, multi-route graphs, and unclear shells need a manual p-content-space on that scroll owner (or accept edge-to-edge). See ${report_unresolved_1.PADDING_DOCS_LINK}`;
|
|
21
|
+
exports.buildUnresolvedInventGuidance = buildUnresolvedInventGuidance;
|
|
22
|
+
/**
|
|
23
|
+
* Per-extension unresolved warning. Prefer this over a bare "no token" line so
|
|
24
|
+
* developers learn *why* invent skipped them.
|
|
25
|
+
*/
|
|
26
|
+
const formatUnresolvedReport = (report) => {
|
|
27
|
+
const reasonSuffix = report.skipReason === undefined ? "" : ` — reason [${report.skipReason.code}]: ${report.skipReason.message}`;
|
|
28
|
+
if (report.skippedRouteName !== undefined) {
|
|
29
|
+
return ` ${MIGRATION_NAME}: extension "${report.extensionId}" (${report.sourceRoot}) skipped route "${report.skippedRouteName}"${reasonSuffix || " — no inventable overflow content shell on that route"}; see ${report.docsLink}`;
|
|
30
|
+
}
|
|
31
|
+
if (report.skipReason !== undefined) {
|
|
32
|
+
return ` ${MIGRATION_NAME}: extension "${report.extensionId}" (${report.sourceRoot}) not auto-invented${reasonSuffix}; add p-content-space manually on the scroll owner or accept edge-to-edge — see ${report.docsLink}`;
|
|
33
|
+
}
|
|
34
|
+
return ` ${MIGRATION_NAME}: extension "${report.extensionId}" (${report.sourceRoot}) has no page-content/p-content-space and no inventable overflow content shell (needs a scrolling div/main/section with editable className, not e.g. a bare Card) — add p-content-space manually or accept edge-to-edge; see ${report.docsLink}`;
|
|
35
|
+
};
|
|
36
|
+
exports.formatUnresolvedReport = formatUnresolvedReport;
|
|
37
|
+
/**
|
|
38
|
+
* Advisory-only scrollbar notes for invent/convert onto a self-scrolling
|
|
39
|
+
* (`overflow-*`) shell. Cannot measure real DOM scrollbars in the codemod —
|
|
40
|
+
* documents the preferred flush-scrollbar outcome after content owns padding.
|
|
41
|
+
*/
|
|
42
|
+
const buildScrollbarAdvisoryMessages = (changes) => {
|
|
43
|
+
const messages = [];
|
|
44
|
+
for (const change of changes) {
|
|
45
|
+
if (!shellHasOverflowScroll(change.before) && !shellHasOverflowScroll(change.after)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
messages.push(` ${MIGRATION_NAME}: scrollbar note on "${change.extensionId}" (${change.filePath}): shell uses overflow scrolling — prefer a flush scrollbar at the panel edge after content owns padding`);
|
|
49
|
+
}
|
|
50
|
+
return messages;
|
|
51
|
+
};
|
|
52
|
+
exports.buildScrollbarAdvisoryMessages = buildScrollbarAdvisoryMessages;
|
|
53
|
+
/**
|
|
54
|
+
* Collects end-of-run ASA developer messages (scrollbar advisories, unresolved
|
|
55
|
+
* invent guidance when needed, + single verify/edge-to-edge reminder). Empty
|
|
56
|
+
* when no ASA work occurred this run.
|
|
57
|
+
*/
|
|
58
|
+
const collectAsaEndOfRunMessages = (context) => {
|
|
59
|
+
if (!context.asaWorkOccurred) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const messages = (0, exports.buildScrollbarAdvisoryMessages)(context.changes);
|
|
63
|
+
if (context.unresolvedReports.length > 0) {
|
|
64
|
+
messages.push((0, exports.buildUnresolvedInventGuidance)());
|
|
65
|
+
}
|
|
66
|
+
messages.push((0, exports.buildVerifyAndEdgeToEdgeReminder)());
|
|
67
|
+
return messages;
|
|
68
|
+
};
|
|
69
|
+
exports.collectAsaEndOfRunMessages = collectAsaEndOfRunMessages;
|
|
70
|
+
const shellHasOverflowScroll = (classnameValue) => {
|
|
71
|
+
return (0, css_classname_utils_1.splitClasses)(classnameValue).some(cls => /(^|:)overflow-/.test(cls));
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=report-run-messaging.js.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PADDING_DOCS_LINK = void 0;
|
|
4
|
+
exports.findUnresolvedSurface = findUnresolvedSurface;
|
|
5
|
+
const css_classname_utils_1 = require("@trackunit/css-classname-utils");
|
|
6
|
+
const classname_ast_rewrite_1 = require("../utils/classname-ast-rewrite");
|
|
7
|
+
const resolve_content_shell_1 = require("./resolve-content-shell");
|
|
8
|
+
const PAGE_CONTENT_TOKEN = "page-content";
|
|
9
|
+
const CONTENT_SPACE_TOKEN = "p-content-space";
|
|
10
|
+
const RESOLVED_TOKENS = [PAGE_CONTENT_TOKEN, CONTENT_SPACE_TOKEN];
|
|
11
|
+
const tokenBase = (token) => (token.includes(":") ? token.slice(token.lastIndexOf(":") + 1) : token);
|
|
12
|
+
const isResolvedToken = (token) => RESOLVED_TOKENS.includes(tokenBase(token));
|
|
13
|
+
const DEFAULT_ENTRY_COMPONENT_FILE = "index.tsx";
|
|
14
|
+
/** In-repo docs for inventable overflow content shells (GLU-1571 / p-content-space migration). */
|
|
15
|
+
exports.PADDING_DOCS_LINK = "docs/tech/TAILWIND-ARCHITECTURE.md#p-content-space-inventable-shell";
|
|
16
|
+
/**
|
|
17
|
+
* Read-only check for whether a single file already contains `page-content`
|
|
18
|
+
* or `p-content-space` as a whole classname token (not matching inside e.g.
|
|
19
|
+
* `page-content-with-gap`). Used as a suppressor on unresolvable graphs so
|
|
20
|
+
* resolver limits do not permanently light up already-migrated extensions.
|
|
21
|
+
*/
|
|
22
|
+
function fileHasResolvedToken(content) {
|
|
23
|
+
if (!RESOLVED_TOKENS.some(token => content.includes(token)))
|
|
24
|
+
return false;
|
|
25
|
+
const program = (0, classname_ast_rewrite_1.parseTsestreeSource)(content);
|
|
26
|
+
const locations = (0, classname_ast_rewrite_1.findClassnameLocations)(program);
|
|
27
|
+
return locations.some(location => (0, css_classname_utils_1.splitClasses)(location.value).some(isResolvedToken));
|
|
28
|
+
}
|
|
29
|
+
function sourceRootHasResolvedToken(tree, extension) {
|
|
30
|
+
let found = false;
|
|
31
|
+
(0, classname_ast_rewrite_1.visitRewritableFiles)(tree, extension.sourceRoot, (_filePath, content) => {
|
|
32
|
+
if (found)
|
|
33
|
+
return;
|
|
34
|
+
if (fileHasResolvedToken(content)) {
|
|
35
|
+
found = true;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Determines whether the inventable content shell of an Asset Home/Site Home/Admin
|
|
42
|
+
* extension still lacks `page-content` / `p-content-space`, and if so, builds a
|
|
43
|
+
* report entry describing it.
|
|
44
|
+
*
|
|
45
|
+
* A token on a nested file is not enough when the graph *resolves*: Device
|
|
46
|
+
* Administration hid behind `p-content-space` on a job-details leaf while the
|
|
47
|
+
* layout shell still used `pb-responsive-space` (GLU-1903). Returns `null` only
|
|
48
|
+
* when every *resolved content shell* already carries a resolved token (or still
|
|
49
|
+
* uses `page-content`, which `rewritePageContent` handles separately so it must
|
|
50
|
+
* not be double-reported).
|
|
51
|
+
*
|
|
52
|
+
* An unresolvable graph is *not* treated as a leftover if any rewritable file
|
|
53
|
+
* already has a resolved token — otherwise Device Administration (and ~24 other
|
|
54
|
+
* ASA extensions the walker cannot follow) stay permanently-on after padding.
|
|
55
|
+
* Unresolvable graphs with no token anywhere still report.
|
|
56
|
+
*
|
|
57
|
+
* This function is read-only: it never calls `tree.write`. Callers must run it
|
|
58
|
+
* against Tree state that already reflects `rewritePageContent`'s and
|
|
59
|
+
* `removeRedundantPadding`'s edits for the same extension, since those two
|
|
60
|
+
* transforms mutate `page-content` into `p-content-space` in place — running
|
|
61
|
+
* this check first, against pre-rewrite content, would misreport a
|
|
62
|
+
* just-fixed extension as unresolved.
|
|
63
|
+
*
|
|
64
|
+
* A `sourceRoot` that doesn't exist in the Tree at all (or contains no
|
|
65
|
+
* rewritable files) is treated the same as "no page-content and no
|
|
66
|
+
* p-content-space found": it gets reported, not silently skipped.
|
|
67
|
+
*/
|
|
68
|
+
function findUnresolvedSurface(tree, extension) {
|
|
69
|
+
const resolved = (0, resolve_content_shell_1.resolveContentShells)(tree, extension);
|
|
70
|
+
if (resolved.status === "resolved") {
|
|
71
|
+
const everyShellHasResolvedToken = resolved.shells.every(shell => (0, css_classname_utils_1.splitClasses)(shell.combinedClassnameValue).some(isResolvedToken));
|
|
72
|
+
if (everyShellHasResolvedToken) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (sourceRootHasResolvedToken(tree, extension)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const entryComponentPath = `${extension.sourceRoot}/${extension.main ?? DEFAULT_ENTRY_COMPONENT_FILE}`;
|
|
80
|
+
return {
|
|
81
|
+
extensionId: extension.id,
|
|
82
|
+
sourceRoot: extension.sourceRoot,
|
|
83
|
+
entryComponentPath: tree.exists(entryComponentPath) ? entryComponentPath : null,
|
|
84
|
+
docsLink: exports.PADDING_DOCS_LINK,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=report-unresolved.js.map
|