@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.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +28 -0
- package/bin/ui-common.mjs +12 -0
- package/cli/agents.mjs +335 -0
- package/cli/diff.mjs +125 -0
- package/cli/lab-peer.mjs +226 -0
- package/cli/main.mjs +97 -0
- package/cli/passthrough.mjs +153 -0
- package/cli/paths.mjs +290 -0
- package/cli/report.mjs +157 -0
- package/cli/resolve.mjs +257 -0
- package/cli/rewrite.mjs +133 -0
- package/cli/semver.mjs +278 -0
- package/cli/shadow.mjs +161 -0
- package/cli/sync-astryx.mjs +417 -0
- package/cli/upgrade.mjs +776 -0
- package/codemods/0.2/components.mjs +903 -0
- package/codemods/0.2/elements.mjs +745 -0
- package/codemods/0.2/index.mjs +68 -0
- package/codemods/0.2/legacy-classes.json +292 -0
- package/codemods/0.2/local-modules.mjs +510 -0
- package/codemods/0.2/map.mjs +178 -0
- package/codemods/0.2/package-json.mjs +333 -0
- package/codemods/0.2/scan.mjs +397 -0
- package/codemods/0.2/stylesheets.mjs +479 -0
- package/codemods/0.2/theme.mjs +325 -0
- package/codemods/lib/jsx.mjs +352 -0
- package/codemods/lib/todo.mjs +151 -0
- package/codemods/registry.mjs +95 -0
- package/codemods/upstream.mjs +155 -0
- package/migration/0.1-to-0.2.json +629 -0
- package/package.json +50 -0
|
@@ -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
|
+
}
|