@lablup/ui-common-cli 0.2.0-alpha.15

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.
@@ -0,0 +1,510 @@
1
+ /**
2
+ * The project's own modules that hand ui-common 0.1 components on: barrels
3
+ * (`export { Button } from "@lablup/ui-common/components/Button"`,
4
+ * `export * from …`, an import then `export { … }`, `export const X = Y`) and
5
+ * wrappers (an exported component that renders one).
6
+ *
7
+ * The components codemod rewrites such a barrel in place and keeps the 0.1
8
+ * public name, so an import of `Select` from the barrel (`@/components/common`) now gets
9
+ * Astryx's Selector. Its call sites still pass 0.1 props: this module tells
10
+ * the codemod which local imports stand for which 0.1 component, so the same
11
+ * element rewrite runs on them. A wrapper is not a re-export: its props are
12
+ * its own, so its call sites are left alone and the wrapper is reported.
13
+ *
14
+ * Every module is read as it was before this run (the barrel itself is being
15
+ * rewritten), through the runner's `ctx.source` and `ctx.resolveImport`.
16
+ */
17
+ import { REMOVED, REMOVED_TYPES, UIC } from "./map.mjs";
18
+
19
+ /**
20
+ * @typedef {{kind: 'component', component: string}
21
+ * | {kind: 'type', component: string, type: string, removed: boolean}
22
+ * | {kind: 'wrapper', components: string[], file: string, line: number, name: string}} LocalExport
23
+ *
24
+ * @typedef {object} LocalModuleContext
25
+ * @property {(path: string) => string | null} [source] a file's content before this run
26
+ * @property {(from: string, specifier: string, options?: {probe?: boolean}) => string | null} [resolveImport]
27
+ */
28
+
29
+ /** @type {WeakMap<object, {modules: Map<string, Map<string, LocalExport>>, wrappers: Map<string, {def: LocalExport & {kind: 'wrapper'}, importers: Set<string>}>}>} */
30
+ const caches = new WeakMap();
31
+
32
+ /** @param {object} ctx */
33
+ function cacheFor(ctx) {
34
+ let cache = caches.get(ctx);
35
+ if (!cache) {
36
+ cache = { modules: new Map(), wrappers: new Map() };
37
+ caches.set(ctx, cache);
38
+ }
39
+ return cache;
40
+ }
41
+
42
+ /**
43
+ * What a ui-common specifier's name stands for in 0.1.
44
+ *
45
+ * @param {string} source
46
+ * @param {string} name imported name (`default` for a default import)
47
+ * @returns {LocalExport | null}
48
+ */
49
+ function uicName(source, name) {
50
+ let imported = name;
51
+ if (source !== UIC) {
52
+ const match = /^@lablup\/ui-common\/components\/([A-Za-z]+)$/.exec(source);
53
+ if (!match) return null;
54
+ if (name === "default") imported = match[1];
55
+ }
56
+ if (REMOVED.has(imported)) return { kind: "component", component: imported };
57
+ const type = REMOVED_TYPES.get(imported);
58
+ if (type)
59
+ return {
60
+ kind: "type",
61
+ component: type.component,
62
+ type: imported,
63
+ removed: type.to == null,
64
+ };
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * Every removed name a ui-common specifier offers, for `export *`.
70
+ *
71
+ * @param {string} source
72
+ * @returns {Map<string, LocalExport>}
73
+ */
74
+ function uicStar(source) {
75
+ /** @type {Map<string, LocalExport>} */
76
+ const out = new Map();
77
+ const match = /^@lablup\/ui-common\/components\/([A-Za-z]+)$/.exec(source);
78
+ if (source !== UIC && !match) return out;
79
+ for (const [name, removed] of REMOVED) {
80
+ if (match && name !== match[1]) continue;
81
+ out.set(name, { kind: "component", component: name });
82
+ for (const [type, to] of Object.entries(removed.types)) {
83
+ out.set(type, { kind: "type", component: name, type, removed: to == null });
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+
89
+ /** @param {string} source */
90
+ const isUic = (source) => source === UIC || source.startsWith(`${UIC}/components/`);
91
+
92
+ /**
93
+ * Import sources a file names, without parsing it.
94
+ *
95
+ * @param {string} source
96
+ */
97
+ function importSources(source) {
98
+ const out = new Set();
99
+ for (const m of source.matchAll(/\b(?:from|import)\s*["']([^"']+)["']/g))
100
+ out.add(m[1]);
101
+ return out;
102
+ }
103
+
104
+ /** Strip parentheses and TS casts. @param {any} node */
105
+ function unwrap(node) {
106
+ let n = node;
107
+ while (
108
+ n &&
109
+ (n.type === "TSAsExpression" ||
110
+ n.type === "TSSatisfiesExpression" ||
111
+ n.type === "TSNonNullExpression" ||
112
+ n.type === "ParenthesizedExpression")
113
+ )
114
+ n = n.expression;
115
+ return n;
116
+ }
117
+
118
+ /**
119
+ * The function a component declaration defines: a function declaration, or
120
+ * the function a `const` holds, through `forwardRef(…)` / `memo(…)`, or the
121
+ * declaration `memo(Name)` names.
122
+ *
123
+ * @param {any} decl
124
+ * @param {Map<string, any>} [declarations] the module's top-level declarations
125
+ * @param {number} [depth]
126
+ */
127
+ function componentFunction(decl, declarations = new Map(), depth = 0) {
128
+ if (decl.type === "FunctionDeclaration") return decl;
129
+ let init = decl.type === "VariableDeclarator" ? unwrap(decl.init) : null;
130
+ while (init?.type === "CallExpression") {
131
+ init = unwrap(
132
+ init.arguments.find(
133
+ (/** @type {any} */ a) =>
134
+ a.type === "ArrowFunctionExpression" ||
135
+ a.type === "FunctionExpression" ||
136
+ a.type === "CallExpression" ||
137
+ a.type === "Identifier",
138
+ ),
139
+ );
140
+ }
141
+ // `memo(EmptyStateComponent)`: the component is declared on its own.
142
+ if (init?.type === "Identifier" && depth < 3) {
143
+ const target = declarations.get(init.name);
144
+ return target ? componentFunction(target, declarations, depth + 1) : null;
145
+ }
146
+ return init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression"
147
+ ? init
148
+ : null;
149
+ }
150
+
151
+ /**
152
+ * The names a component's props parameter binds: the props object, or each
153
+ * destructured prop and the rest.
154
+ *
155
+ * @param {any} param
156
+ * @returns {Set<string>}
157
+ */
158
+ function paramNames(param) {
159
+ const out = new Set();
160
+ const p = param?.type === "AssignmentPattern" ? param.left : param;
161
+ if (p?.type === "Identifier") out.add(p.name);
162
+ else if (p?.type === "ObjectPattern") {
163
+ for (const prop of p.properties) {
164
+ if (prop.type === "RestElement" && prop.argument.type === "Identifier")
165
+ out.add(prop.argument.name);
166
+ else if (prop.value?.type === "Identifier") out.add(prop.value.name);
167
+ else if (
168
+ prop.value?.type === "AssignmentPattern" &&
169
+ prop.value.left.type === "Identifier"
170
+ )
171
+ out.add(prop.value.left.name);
172
+ }
173
+ }
174
+ return out;
175
+ }
176
+
177
+ /**
178
+ * The 0.1 components (and removed types) a project module exports, by
179
+ * exported name. Empty for a module that hands on none.
180
+ *
181
+ * @param {any} j jscodeshift
182
+ * @param {LocalModuleContext} ctx
183
+ * @param {string} file absolute
184
+ * @param {Set<string>} [stack] modules being read, against cycles
185
+ * @returns {Map<string, LocalExport>}
186
+ */
187
+ export function localExports(j, ctx, file, stack = new Set()) {
188
+ const cache = cacheFor(ctx);
189
+ const hit = cache.modules.get(file);
190
+ if (hit) return hit;
191
+ /** @type {Map<string, LocalExport>} */
192
+ const out = new Map();
193
+ const source = ctx.source?.(file);
194
+ if (source == null || stack.has(file) || !/\bexport\b/.test(source)) {
195
+ if (!stack.has(file)) cache.modules.set(file, out);
196
+ return out;
197
+ }
198
+ // Cheap bail-out: nothing from ui-common, directly or through a module the
199
+ // resolver finds (relative, tsconfig `paths` or `baseUrl` alike). A probe,
200
+ // since the text scan also matches specifiers in comments.
201
+ /** @type {Map<string, string>} */
202
+ const targets = new Map();
203
+ let worthReading = false;
204
+ for (const s of importSources(source)) {
205
+ const target = isUic(s) ? null : ctx.resolveImport?.(file, s, { probe: true });
206
+ if (target) targets.set(s, target);
207
+ if (target || isUic(s)) worthReading = true;
208
+ }
209
+ if (!worthReading) {
210
+ cache.modules.set(file, out);
211
+ return out;
212
+ }
213
+ stack.add(file);
214
+ let root;
215
+ try {
216
+ // Parse by the module's own extension, not the importer's.
217
+ const parser = /\.[cm]?tsx?$/.test(file) ? "tsx" : "babel";
218
+ root = (typeof j.withParser === "function" ? j.withParser(parser) : j)(source);
219
+ } catch {
220
+ stack.delete(file);
221
+ cache.modules.set(file, out);
222
+ return out;
223
+ }
224
+
225
+ /** @param {string} specifier */
226
+ const moduleExports = (specifier) => {
227
+ const target = targets.get(specifier) ?? ctx.resolveImport?.(file, specifier);
228
+ return target ? localExports(j, ctx, target, stack) : new Map();
229
+ };
230
+
231
+ // Local bindings that stand for a 0.1 component or type.
232
+ /** @type {Map<string, LocalExport>} */
233
+ const bindings = new Map();
234
+ root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
235
+ const from = path.node.source.value;
236
+ if (typeof from !== "string") return;
237
+ for (const spec of path.node.specifiers ?? []) {
238
+ if (spec.type === "ImportNamespaceSpecifier") continue;
239
+ const name =
240
+ spec.type === "ImportDefaultSpecifier" ? "default" : spec.imported.name;
241
+ const entry = isUic(from) ? uicName(from, name) : moduleExports(from).get(name);
242
+ if (entry) bindings.set(spec.local.name, entry);
243
+ }
244
+ });
245
+
246
+ // Top-level declarations, for `export { X }` of a local and for wrappers.
247
+ /** @type {Map<string, any>} */
248
+ const declarations = new Map();
249
+ /** @type {Map<string, any>} */
250
+ const typeDeclarations = new Map();
251
+ for (const statement of root.find(j.Program).get().node.body) {
252
+ const node =
253
+ statement.type === "ExportNamedDeclaration" ||
254
+ statement.type === "ExportDefaultDeclaration"
255
+ ? statement.declaration
256
+ : statement;
257
+ if (!node) continue;
258
+ if (
259
+ (node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") &&
260
+ node.id
261
+ ) {
262
+ declarations.set(node.id.name, node);
263
+ } else if (node.type === "VariableDeclaration") {
264
+ for (const d of node.declarations) {
265
+ if (d.id?.type === "Identifier") declarations.set(d.id.name, d);
266
+ }
267
+ } else if (
268
+ (node.type === "TSInterfaceDeclaration" ||
269
+ node.type === "TSTypeAliasDeclaration") &&
270
+ node.id
271
+ ) {
272
+ typeDeclarations.set(node.id.name, node);
273
+ }
274
+ }
275
+
276
+ /**
277
+ * The 0.1 components whose props type a props parameter's type is built
278
+ * on: `SelectProps`, `Omit<DataTableProps<T>, …>`, an intersection with
279
+ * one, or a local interface or type alias that extends or aliases one.
280
+ *
281
+ * @param {any} param
282
+ * @returns {Set<string>}
283
+ */
284
+ const propsTypeComponents = (param) => {
285
+ const out = new Set();
286
+ const seen = new Set();
287
+ const WRAPPING = new Set(["Omit", "Partial", "Pick", "Readonly", "Required"]);
288
+ /**
289
+ * @param {string} name
290
+ * @param {any} args type arguments
291
+ */
292
+ const reference = (name, args) => {
293
+ const bound = bindings.get(name);
294
+ if (bound?.kind === "type" && /Props$/.test(bound.type)) out.add(bound.component);
295
+ if (WRAPPING.has(name)) visit(args?.params?.[0]);
296
+ const local = typeDeclarations.get(name);
297
+ if (local && !seen.has(name)) {
298
+ seen.add(name);
299
+ if (local.type === "TSTypeAliasDeclaration") visit(local.typeAnnotation);
300
+ for (const heritage of local.extends ?? []) {
301
+ const id = heritage.expression;
302
+ if (id?.type === "Identifier")
303
+ reference(id.name, heritage.typeParameters ?? heritage.typeArguments);
304
+ }
305
+ }
306
+ };
307
+ /** Only what the props type is made of, not the types of its members. @param {any} t */
308
+ const visit = (t) => {
309
+ if (!t) return;
310
+ switch (t.type) {
311
+ case "TSTypeAnnotation":
312
+ case "TSParenthesizedType":
313
+ visit(t.typeAnnotation);
314
+ break;
315
+ case "TSIntersectionType":
316
+ case "TSUnionType":
317
+ t.types.forEach(visit);
318
+ break;
319
+ case "TSTypeReference":
320
+ if (t.typeName?.type === "Identifier")
321
+ reference(t.typeName.name, t.typeParameters ?? t.typeArguments);
322
+ break;
323
+ default:
324
+ }
325
+ };
326
+ const p = param?.type === "AssignmentPattern" ? param.left : param;
327
+ visit(p?.typeAnnotation);
328
+ return out;
329
+ };
330
+
331
+ /**
332
+ * What exporting local `name` hands on: an aliased 0.1 binding, or a
333
+ * wrapper that renders one.
334
+ *
335
+ * @param {string} name
336
+ * @param {string} exported
337
+ * @returns {LocalExport | null}
338
+ */
339
+ const describeLocal = (name, exported) => {
340
+ const bound = bindings.get(name);
341
+ if (bound) return bound;
342
+ const decl = declarations.get(name);
343
+ if (!decl) return null;
344
+ if (decl.type === "VariableDeclarator" && decl.init?.type === "Identifier") {
345
+ return bindings.get(decl.init.name) ?? describeLocal(decl.init.name, exported);
346
+ }
347
+ const fn = componentFunction(decl, declarations);
348
+ if (!fn) return null;
349
+ const params = paramNames(fn.params[0]);
350
+ // Its callers pass 0.1 props when its props type is built on a 0.1 one…
351
+ const typed = propsTypeComponents(fn.params[0]);
352
+ /** @param {any} expression */
353
+ const fromParams = (expression) => {
354
+ const e = unwrap(expression);
355
+ return e?.type === "Identifier" && params.has(e.name);
356
+ };
357
+ const rendered = new Set();
358
+ j(fn)
359
+ .find(j.JSXElement)
360
+ .forEach((/** @type {any} */ p) => {
361
+ const tag = p.node.openingElement.name;
362
+ if (tag.type !== "JSXIdentifier") return;
363
+ const entry = bindings.get(tag.name);
364
+ const components =
365
+ entry?.kind === "component"
366
+ ? [entry.component]
367
+ : entry?.kind === "wrapper"
368
+ ? entry.components
369
+ : [];
370
+ if (components.length === 0) return;
371
+ // …or when it spreads its own props (or their rest) into one.
372
+ const spreads = (p.node.openingElement.attributes ?? []).some(
373
+ (/** @type {any} */ a) =>
374
+ a.type === "JSXSpreadAttribute" && fromParams(a.argument),
375
+ );
376
+ if (spreads || components.some((c) => typed.has(c)))
377
+ for (const c of components) rendered.add(c);
378
+ });
379
+ if (rendered.size === 0) return null;
380
+ return {
381
+ kind: "wrapper",
382
+ components: [...rendered].sort(),
383
+ file,
384
+ line:
385
+ decl.loc?.start.line ??
386
+ (typeof decl.start === "number"
387
+ ? source.slice(0, decl.start).split("\n").length
388
+ : 0),
389
+ name: exported === "default" ? name : exported,
390
+ };
391
+ };
392
+
393
+ root.find(j.ExportNamedDeclaration).forEach((/** @type {any} */ path) => {
394
+ const node = path.node;
395
+ const from = node.source?.value;
396
+ if (typeof from === "string") {
397
+ const theirs = isUic(from) ? null : moduleExports(from);
398
+ for (const spec of node.specifiers ?? []) {
399
+ if (spec.type !== "ExportSpecifier") continue;
400
+ const name = spec.local?.name ?? spec.exported.name;
401
+ const entry = theirs ? theirs.get(name) : uicName(from, name);
402
+ if (entry) out.set(spec.exported.name, entry);
403
+ }
404
+ return;
405
+ }
406
+ if (node.declaration) {
407
+ const decl = node.declaration;
408
+ const names =
409
+ decl.type === "VariableDeclaration"
410
+ ? decl.declarations
411
+ .filter((/** @type {any} */ d) => d.id?.type === "Identifier")
412
+ .map((/** @type {any} */ d) => d.id.name)
413
+ : decl.id
414
+ ? [decl.id.name]
415
+ : [];
416
+ for (const name of names) {
417
+ const entry = describeLocal(name, name);
418
+ if (entry) out.set(name, entry);
419
+ }
420
+ return;
421
+ }
422
+ for (const spec of node.specifiers ?? []) {
423
+ if (spec.type !== "ExportSpecifier") continue;
424
+ const entry = describeLocal(spec.local.name, spec.exported.name);
425
+ if (entry) out.set(spec.exported.name, entry);
426
+ }
427
+ });
428
+
429
+ root.find(j.ExportDefaultDeclaration).forEach((/** @type {any} */ path) => {
430
+ const decl = path.node.declaration;
431
+ let entry = null;
432
+ if (decl.type === "Identifier") entry = describeLocal(decl.name, "default");
433
+ else if (decl.id?.name) entry = describeLocal(decl.id.name, "default");
434
+ if (entry) out.set("default", entry);
435
+ });
436
+
437
+ root.find(j.ExportAllDeclaration).forEach((/** @type {any} */ path) => {
438
+ const from = path.node.source?.value;
439
+ if (typeof from !== "string" || path.node.exported) return;
440
+ const theirs = isUic(from) ? uicStar(from) : moduleExports(from);
441
+ for (const [name, entry] of theirs) {
442
+ if (name !== "default" && !out.has(name)) out.set(name, entry);
443
+ }
444
+ });
445
+
446
+ stack.delete(file);
447
+ cache.modules.set(file, out);
448
+ for (const entry of out.values()) {
449
+ if (entry.kind === "wrapper") registerWrapper(ctx, entry);
450
+ }
451
+ return out;
452
+ }
453
+
454
+ /**
455
+ * @param {object} ctx
456
+ * @param {LocalExport & {kind: 'wrapper'}} def
457
+ * @param {string} [importer]
458
+ */
459
+ export function registerWrapper(ctx, def, importer) {
460
+ const { wrappers } = cacheFor(ctx);
461
+ const key = `${def.file}\u0000${def.name}`;
462
+ const known = wrappers.get(key) ?? { def, importers: new Set() };
463
+ if (importer) known.importers.add(importer);
464
+ wrappers.set(key, known);
465
+ }
466
+
467
+ /**
468
+ * Whether a file imports anything from a project module that hands on a 0.1
469
+ * component or type, without parsing the file itself.
470
+ *
471
+ * @param {any} j
472
+ * @param {LocalModuleContext} ctx
473
+ * @param {string} file
474
+ * @param {string} source
475
+ */
476
+ export function importsLocalLegacy(j, ctx, file, source) {
477
+ if (!ctx.resolveImport) return false;
478
+ for (const specifier of importSources(source)) {
479
+ if (specifier.startsWith(UIC)) continue;
480
+ const target = ctx.resolveImport(file, specifier);
481
+ if (target && localExports(j, ctx, target).size > 0) return true;
482
+ }
483
+ return false;
484
+ }
485
+
486
+ /**
487
+ * The wrappers the run met, for the report: one finding per wrapper.
488
+ *
489
+ * @param {object} ctx
490
+ * @param {(file: string) => string} rel
491
+ */
492
+ export function wrapperFindings(ctx, rel) {
493
+ const cache = caches.get(ctx);
494
+ if (!cache) return [];
495
+ return [...cache.wrappers.values()]
496
+ .sort((a, b) => a.def.file.localeCompare(b.def.file) || a.def.line - b.def.line)
497
+ .map(({ def, importers }) => {
498
+ const targets = def.components.map((c) => {
499
+ const to = REMOVED.get(c)?.to;
500
+ return to && to !== c ? `${c} (Astryx ${to})` : c;
501
+ });
502
+ return {
503
+ category: "local-wrapper",
504
+ file: rel(def.file),
505
+ line: def.line,
506
+ text: def.name,
507
+ detail: `local wrapper around ${targets.join(", ")}: review its props.${importers.size > 0 ? ` Imported by ${importers.size} scanned module${importers.size === 1 ? "" : "s"}, whose props were not migrated.` : ""}`,
508
+ };
509
+ });
510
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The 0.1 -> 0.2 codemods' view of `migration/0.1-to-0.2.json`, the one
3
+ * machine-readable account of what 0.2 removed, renamed and restyled. The
4
+ * codemods read their data from it and from nowhere else; this module only
5
+ * reshapes it into lookups.
6
+ */
7
+ import { join } from "node:path";
8
+
9
+ import {
10
+ CLI_ROOT,
11
+ readJson,
12
+ targetUiCommonRoot,
13
+ uiCommonPackageJson,
14
+ } from "../../cli/paths.mjs";
15
+
16
+ export const UIC = "@lablup/ui-common";
17
+ export const MIGRATION_FILE = join(CLI_ROOT, "migration", "0.1-to-0.2.json");
18
+
19
+ /**
20
+ * @typedef {object} PropRename
21
+ * @property {string} from
22
+ * @property {string} to
23
+ * @property {string} [kind] children-to-prop | signature-change | value-map
24
+ * @property {string} [onlyWhen]
25
+ * @property {string} [value]
26
+ * @property {string} [note]
27
+ *
28
+ * @typedef {object} RemovedSpec
29
+ * @property {string} name
30
+ * @property {Array<{specifier: string, names: string[]}>} oldImports
31
+ * @property {{specifier: string, names: Record<string, string>}} replacement
32
+ * @property {Array<{specifier: string, names: Record<string, string>, when: string}>} alternatives
33
+ * @property {PropRename[]} propRenames
34
+ * @property {Record<string, Record<string, string|number|null>>} valueMaps
35
+ * @property {Array<{from: string, to: string, kind?: string}>} [columnRenames]
36
+ * @property {Record<string, string>} [requiresPackages]
37
+ * @property {Record<string, {whenState: string[], value: unknown}>} [defaultsToMaterialize]
38
+ * @property {string[]} manual
39
+ */
40
+
41
+ /** @type {{removedComponents: RemovedSpec[], keptComponents: Array<{name: string, specifiers: string[], classRenames: Record<string, string>, notes: string[]}>, movedExports?: Array<{name: string, from: string, to: string, note: string}>, shapeClassRenames: Record<string, string|null>, stylesheets: Array<{specifier: string, replaceWith: null | {layerOrder: string, imports: string[]}, note?: string}>}} */
42
+ export const migration = readJson(MIGRATION_FILE);
43
+
44
+ /** `@lablup/ui-common/Badge` -> `Badge`; the root -> "". @param {string} specifier */
45
+ export function subpathOf(specifier) {
46
+ return specifier === UIC ? "" : specifier.slice(UIC.length + 1);
47
+ }
48
+
49
+ /**
50
+ * @typedef {object} Removed
51
+ * @property {string} name 0.1 component
52
+ * @property {string} to Astryx component
53
+ * @property {string} subpath where `to` lives under @lablup/ui-common
54
+ * @property {Record<string, string|null>} types 0.1 type -> Astryx type, or null
55
+ * @property {Record<string, string>} alternates alternative component -> subpath
56
+ * @property {Record<string, string>} requiresPackages
57
+ * @property {RemovedSpec} spec the raw entry
58
+ */
59
+
60
+ /** @type {Map<string, Removed>} */
61
+ export const REMOVED = new Map();
62
+ /** @type {Map<string, {component: string, to: string|null}>} */
63
+ export const REMOVED_TYPES = new Map();
64
+
65
+ for (const spec of migration.removedComponents) {
66
+ const names = spec.replacement.names;
67
+ /** @type {Record<string, string|null>} */
68
+ const types = {};
69
+ for (const imp of spec.oldImports) {
70
+ for (const name of imp.names) {
71
+ if (name === spec.name || name === "default") continue;
72
+ types[name] = names[name] ?? null;
73
+ }
74
+ }
75
+ /** @type {Record<string, string>} */
76
+ const alternates = {};
77
+ for (const alt of spec.alternatives ?? []) {
78
+ const component = alt.names[spec.name];
79
+ if (component) alternates[component] = subpathOf(alt.specifier);
80
+ }
81
+ REMOVED.set(spec.name, {
82
+ name: spec.name,
83
+ to: names[spec.name],
84
+ subpath: subpathOf(spec.replacement.specifier),
85
+ types,
86
+ alternates,
87
+ requiresPackages: spec.requiresPackages ?? {},
88
+ spec,
89
+ });
90
+ for (const [type, to] of Object.entries(types)) {
91
+ REMOVED_TYPES.set(type, { component: spec.name, to });
92
+ }
93
+ }
94
+
95
+ /** Exports that stay but move to another subpath. */
96
+ export const MOVED = new Map((migration.movedExports ?? []).map((m) => [m.name, m]));
97
+
98
+ const baseEntry = migration.stylesheets.find((s) => s.specifier.endsWith("/base.css"));
99
+ const themesEntry = migration.stylesheets.find((s) => s.specifier.includes("/themes/"));
100
+ if (!baseEntry?.replaceWith) {
101
+ throw new Error(`${MIGRATION_FILE}: no styles/base.css entry with replaceWith.`);
102
+ }
103
+
104
+ export const STYLESHEETS = {
105
+ base: baseEntry.specifier,
106
+ layerOrder: baseEntry.replaceWith.layerOrder,
107
+ imports: baseEntry.replaceWith.imports,
108
+ /** The dropped theme sheets: the `*` in the specifier matches one path segment. */
109
+ dropped: new RegExp(
110
+ `^${(themesEntry?.specifier ?? "@lablup/ui-common/styles/themes/*.css")
111
+ .replace(/[.+?^${}()|[\]\\/]/g, "\\$&")
112
+ .replace("*", "[^/]+")}$`,
113
+ ),
114
+ /** What the codemod writes beside a script that imported base.css. */
115
+ entryFile: "ui-common-entry.css",
116
+ };
117
+
118
+ export const LAB_PACKAGE = "@astryxdesign/lab";
119
+ export const LAB_CSS = `${UIC}/lab/lab.css`;
120
+
121
+ /** The StyleX peer range the target ui-common declares. */
122
+ export function stylexPeer() {
123
+ return {
124
+ name: "@stylexjs/stylex",
125
+ range:
126
+ uiCommonPackageJson(targetUiCommonRoot()).peerDependencies?.[
127
+ "@stylexjs/stylex"
128
+ ] ?? "^0.19.0",
129
+ };
130
+ }
131
+
132
+ /**
133
+ * The first `manual` note of a removed component that mentions `word`.
134
+ *
135
+ * @param {Removed} removed
136
+ * @param {string} word a prop name or value
137
+ */
138
+ export function manualNote(removed, word) {
139
+ const pattern = new RegExp(
140
+ `(^|[^\\w-])${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^\\w-]|$)`,
141
+ );
142
+ return removed.spec.manual.find((m) => pattern.test(m)) ?? null;
143
+ }
144
+
145
+ /**
146
+ * Where a kept component's 0.1 class name went, by `classRenameRule`:
147
+ * an explicit key wins, else `.<block>`, `.<block>__x` and `.<block>--x`
148
+ * move to the renamed block with the same suffix. `shapeClassRenames` covers
149
+ * the base skeleton shape the composites share.
150
+ *
151
+ * @param {string} name a 0.1 class name
152
+ * @returns {{to: string | null} | null} null when no kept component owned it
153
+ */
154
+ export function keptClassRename(name) {
155
+ /** @type {Record<string, string|null>} */
156
+ const explicit = { ...migration.shapeClassRenames };
157
+ /** @type {Array<[string, string]>} */
158
+ const blocks = [];
159
+ for (const kept of migration.keptComponents) {
160
+ for (const [from, to] of Object.entries(kept.classRenames)) {
161
+ explicit[from] = to;
162
+ blocks.push([from, to]);
163
+ }
164
+ }
165
+ if (name in explicit) return { to: explicit[name] };
166
+ let best = null;
167
+ for (const [block, to] of blocks) {
168
+ if (
169
+ name === block ||
170
+ name.startsWith(`${block}__`) ||
171
+ name.startsWith(`${block}--`)
172
+ ) {
173
+ if (!best || block.length > best[0].length) best = [block, to];
174
+ }
175
+ }
176
+ if (!best) return null;
177
+ return { to: `${best[1]}${name.slice(best[0].length)}` };
178
+ }