@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,903 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.1 -> 0.2: the twelve 0.1 components Astryx now covers move to their
|
|
3
|
+
* Astryx counterparts, and their props are reshaped.
|
|
4
|
+
*
|
|
5
|
+
* - Imports from the root barrel and from `@lablup/ui-common/components/<Name>`
|
|
6
|
+
* move to `@lablup/ui-common/<Astryx subpath>` (Drawer to `/lab`). Kept
|
|
7
|
+
* components (PageHeader, StatCard, the Skeleton composites, …) keep the
|
|
8
|
+
* import they had. `usePrefersReducedMotion` moves from `/hooks` to the root.
|
|
9
|
+
* - Re-exports (`export { Select } from …`) keep their public name.
|
|
10
|
+
* - Every JSX element of a moved component goes through ./elements.mjs.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
freeName,
|
|
14
|
+
hasSpread,
|
|
15
|
+
identifierNames,
|
|
16
|
+
renameElement,
|
|
17
|
+
tagName,
|
|
18
|
+
TODO_TAG,
|
|
19
|
+
} from "../lib/jsx.mjs";
|
|
20
|
+
import { addTodo, printSource } from "../lib/todo.mjs";
|
|
21
|
+
import { ELEMENT_TRANSFORMS } from "./elements.mjs";
|
|
22
|
+
import { importsLocalLegacy, localExports, registerWrapper } from "./local-modules.mjs";
|
|
23
|
+
import { MOVED, REMOVED, REMOVED_TYPES, UIC } from "./map.mjs";
|
|
24
|
+
|
|
25
|
+
export const meta = {
|
|
26
|
+
id: "components",
|
|
27
|
+
title:
|
|
28
|
+
"Move removed 0.1 components to their Astryx counterparts and reshape their props",
|
|
29
|
+
extensions: [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs", ".mts", ".cts"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} source
|
|
34
|
+
* @returns {{kind: 'root'} | {kind: 'component', dir: string} | {kind: 'hooks'} | null}
|
|
35
|
+
*/
|
|
36
|
+
function classify(source) {
|
|
37
|
+
if (source === UIC) return { kind: "root" };
|
|
38
|
+
if (source === `${UIC}/hooks`) return { kind: "hooks" };
|
|
39
|
+
const match = /^@lablup\/ui-common\/components\/([A-Za-z]+)$/.exec(source);
|
|
40
|
+
if (match) return { kind: "component", dir: match[1] };
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} name exported name
|
|
46
|
+
* @param {NonNullable<ReturnType<typeof classify>>} from
|
|
47
|
+
*/
|
|
48
|
+
function resolveName(name, from) {
|
|
49
|
+
if (from.kind === "hooks") {
|
|
50
|
+
const moved = MOVED.get(name);
|
|
51
|
+
return moved && moved.from === `${UIC}/hooks`
|
|
52
|
+
? { action: "move", source: moved.to }
|
|
53
|
+
: { action: "keep" };
|
|
54
|
+
}
|
|
55
|
+
if (REMOVED.has(name)) return { action: "component", entry: REMOVED.get(name) };
|
|
56
|
+
if (REMOVED_TYPES.has(name))
|
|
57
|
+
return { action: "type", .../** @type {any} */ (REMOVED_TYPES.get(name)) };
|
|
58
|
+
return { action: "keep" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @param {string} source */
|
|
62
|
+
function detectQuote(source) {
|
|
63
|
+
const double = (source.match(/from "/g) ?? []).length;
|
|
64
|
+
const single = (source.match(/from '/g) ?? []).length;
|
|
65
|
+
return single > double ? "single" : "double";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Names a new import binding must not take: every import binding, and every
|
|
70
|
+
* name bound or read in any scope of the file (a function-local `const Card`,
|
|
71
|
+
* a parameter, a global). A rename onto one of them would capture the
|
|
72
|
+
* other's references, or be captured by it.
|
|
73
|
+
*
|
|
74
|
+
* @param {any} j
|
|
75
|
+
* @param {any} root
|
|
76
|
+
*/
|
|
77
|
+
function takenNames(j, root) {
|
|
78
|
+
const names = new Set();
|
|
79
|
+
root.find(j.ImportDeclaration).forEach((/** @type {any} */ p) => {
|
|
80
|
+
for (const s of p.node.specifiers ?? []) if (s.local) names.add(s.local.name);
|
|
81
|
+
});
|
|
82
|
+
root.find(j.Identifier).forEach((/** @type {any} */ p) => {
|
|
83
|
+
if (p.node.type === "JSXIdentifier") {
|
|
84
|
+
const parent = p.parent?.node;
|
|
85
|
+
const isTag =
|
|
86
|
+
parent?.type === "JSXOpeningElement" ||
|
|
87
|
+
parent?.type === "JSXClosingElement" ||
|
|
88
|
+
(parent?.type === "JSXMemberExpression" && parent.object === p.node);
|
|
89
|
+
if (isTag) names.add(p.node.name);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (isReference(p)) names.add(p.node.name);
|
|
93
|
+
});
|
|
94
|
+
return names;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether `name`, read at `path`, is the module-level binding (an import)
|
|
99
|
+
* rather than a local that shadows it.
|
|
100
|
+
*
|
|
101
|
+
* @param {any} path
|
|
102
|
+
* @param {string} name
|
|
103
|
+
* @param {any} programScope
|
|
104
|
+
*/
|
|
105
|
+
function isModuleBinding(path, name, programScope) {
|
|
106
|
+
const scope = path.scope?.lookup(name);
|
|
107
|
+
return scope == null || scope === programScope;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether an Identifier path is a reference to a binding, not a property
|
|
112
|
+
* name, an import/export name slot, or a declaration key.
|
|
113
|
+
*
|
|
114
|
+
* @param {any} path
|
|
115
|
+
*/
|
|
116
|
+
function isReference(path) {
|
|
117
|
+
const parent = path.parent?.node;
|
|
118
|
+
const node = path.node;
|
|
119
|
+
if (!parent) return true;
|
|
120
|
+
switch (parent.type) {
|
|
121
|
+
case "ImportSpecifier":
|
|
122
|
+
case "ImportDefaultSpecifier":
|
|
123
|
+
case "ImportNamespaceSpecifier":
|
|
124
|
+
case "ExportSpecifier":
|
|
125
|
+
return false;
|
|
126
|
+
case "MemberExpression":
|
|
127
|
+
case "OptionalMemberExpression":
|
|
128
|
+
return parent.object === node || parent.computed;
|
|
129
|
+
case "ObjectProperty":
|
|
130
|
+
case "Property":
|
|
131
|
+
return parent.value === node || parent.computed;
|
|
132
|
+
case "TSPropertySignature":
|
|
133
|
+
case "ClassProperty":
|
|
134
|
+
case "PropertyDefinition":
|
|
135
|
+
case "ClassMethod":
|
|
136
|
+
case "MethodDefinition":
|
|
137
|
+
case "ObjectMethod":
|
|
138
|
+
return parent.computed === true && parent.key === node;
|
|
139
|
+
case "TSQualifiedName":
|
|
140
|
+
return parent.left === node;
|
|
141
|
+
default:
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Rename every reference to a module-level binding, JSX tags included. A
|
|
148
|
+
* local that shadows it keeps its name.
|
|
149
|
+
*
|
|
150
|
+
* @param {any} j
|
|
151
|
+
* @param {any} root
|
|
152
|
+
* @param {string} from
|
|
153
|
+
* @param {string} to
|
|
154
|
+
* @param {any} programScope
|
|
155
|
+
*/
|
|
156
|
+
function renameReferences(j, root, from, to, programScope) {
|
|
157
|
+
root.find(j.Identifier, { name: from }).forEach((/** @type {any} */ p) => {
|
|
158
|
+
if (p.node.type === "JSXIdentifier") return;
|
|
159
|
+
if (!isModuleBinding(p, from, programScope)) return;
|
|
160
|
+
const parent = p.parent?.node;
|
|
161
|
+
if (
|
|
162
|
+
(parent?.type === "ObjectProperty" || parent?.type === "Property") &&
|
|
163
|
+
parent.shorthand &&
|
|
164
|
+
parent.value === p.node
|
|
165
|
+
) {
|
|
166
|
+
parent.shorthand = false;
|
|
167
|
+
parent.value = j.identifier(to);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (parent?.type === "ExportSpecifier" && parent.local === p.node) {
|
|
171
|
+
// A fresh node: recast prints a parsed specifier from its original
|
|
172
|
+
// fields, so editing local/exported in place loses the public name.
|
|
173
|
+
const spec = j.exportSpecifier.from({
|
|
174
|
+
local: j.identifier(to),
|
|
175
|
+
exported: j.identifier(parent.exported?.name ?? from),
|
|
176
|
+
});
|
|
177
|
+
if (parent.exportKind) spec.exportKind = parent.exportKind;
|
|
178
|
+
p.parent.replace(spec);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!isReference(p)) return;
|
|
182
|
+
p.node.name = to;
|
|
183
|
+
});
|
|
184
|
+
root.find(j.JSXIdentifier, { name: from }).forEach((/** @type {any} */ p) => {
|
|
185
|
+
if (!isModuleBinding(p, from, programScope)) return;
|
|
186
|
+
const parent = p.parent?.node;
|
|
187
|
+
if (
|
|
188
|
+
parent?.type === "JSXOpeningElement" ||
|
|
189
|
+
parent?.type === "JSXClosingElement" ||
|
|
190
|
+
(parent?.type === "JSXMemberExpression" && parent.object === p.node)
|
|
191
|
+
) {
|
|
192
|
+
p.node.name = to;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {any} j
|
|
199
|
+
* @param {string} imported
|
|
200
|
+
* @param {string} local
|
|
201
|
+
*/
|
|
202
|
+
function importSpecifier(j, imported, local) {
|
|
203
|
+
return imported === local
|
|
204
|
+
? j.importSpecifier(j.identifier(imported))
|
|
205
|
+
: j.importSpecifier(j.identifier(imported), j.identifier(local));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {{source: string, kind: 'value'|'type', imported: string, local: string}[]} adds
|
|
210
|
+
* @param {any} j
|
|
211
|
+
*/
|
|
212
|
+
function buildImports(j, adds) {
|
|
213
|
+
/** @type {Map<string, any[]>} */
|
|
214
|
+
const groups = new Map();
|
|
215
|
+
for (const add of adds) {
|
|
216
|
+
const key = `${add.kind}\u0000${add.source}`;
|
|
217
|
+
const list = groups.get(key) ?? [];
|
|
218
|
+
if (!list.some((s) => s.imported === add.imported && s.local === add.local))
|
|
219
|
+
list.push(add);
|
|
220
|
+
groups.set(key, list);
|
|
221
|
+
}
|
|
222
|
+
return [...groups.entries()].map(([key, list]) => {
|
|
223
|
+
const [kind, source] = key.split("\u0000");
|
|
224
|
+
const decl = j.importDeclaration(
|
|
225
|
+
list.map((a) => importSpecifier(j, a.imported, a.local)),
|
|
226
|
+
j.stringLiteral(source),
|
|
227
|
+
);
|
|
228
|
+
if (kind === "type") decl.importKind = "type";
|
|
229
|
+
return decl;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Put `decls` before `path`'s statement. Leading comments of the statement
|
|
235
|
+
* (a file header, say) move to the first inserted declaration, so they stay
|
|
236
|
+
* on top. Inserting before an existing statement, not after it, keeps recast
|
|
237
|
+
* from moving the blank line that follows the import block.
|
|
238
|
+
*
|
|
239
|
+
* @param {any} path
|
|
240
|
+
* @param {any[]} decls
|
|
241
|
+
*/
|
|
242
|
+
function insertBeforeKeepingComments(path, decls) {
|
|
243
|
+
const node = path.node;
|
|
244
|
+
const leading = (node.comments ?? []).filter(
|
|
245
|
+
(/** @type {any} */ c) => c.leading !== false,
|
|
246
|
+
);
|
|
247
|
+
if (leading.length > 0) {
|
|
248
|
+
decls[0].comments = [...leading, ...(decls[0].comments ?? [])];
|
|
249
|
+
node.comments = (node.comments ?? []).filter(
|
|
250
|
+
(/** @type {any} */ c) => !leading.includes(c),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
path.insertBefore(...decls);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Replace a declaration with `decls` by reusing its node for the last one, so
|
|
258
|
+
* recast keeps it in place without inventing blank lines around it.
|
|
259
|
+
*
|
|
260
|
+
* @param {any} path
|
|
261
|
+
* @param {any[]} decls
|
|
262
|
+
* @param {"importKind" | "exportKind"} kindKey
|
|
263
|
+
*/
|
|
264
|
+
function reuseNode(path, decls, kindKey) {
|
|
265
|
+
const node = path.node;
|
|
266
|
+
const last = decls[decls.length - 1];
|
|
267
|
+
const before = decls.slice(0, -1);
|
|
268
|
+
const own = node.comments ?? [];
|
|
269
|
+
node.specifiers = last.specifiers;
|
|
270
|
+
node.source = last.source;
|
|
271
|
+
node[kindKey] = last[kindKey] ?? "value";
|
|
272
|
+
if (before.length > 0) {
|
|
273
|
+
node.comments = last.comments ?? [];
|
|
274
|
+
before[0].comments = [...own, ...(before[0].comments ?? [])];
|
|
275
|
+
path.insertBefore(...before);
|
|
276
|
+
} else {
|
|
277
|
+
node.comments = [...own, ...(last.comments ?? [])];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** @param {any} entry a local-module export */
|
|
282
|
+
const isRemovedType = (entry) => entry?.kind === "type" && entry.removed === true;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Remove a top-level statement, handing its comments (TODOs included) to the
|
|
286
|
+
* statement after it, or before it when it was the last.
|
|
287
|
+
*
|
|
288
|
+
* @param {any} j
|
|
289
|
+
* @param {any} root
|
|
290
|
+
* @param {any} path
|
|
291
|
+
*/
|
|
292
|
+
function pruneKeepingComments(j, root, path) {
|
|
293
|
+
const node = path.node;
|
|
294
|
+
const comments = node.comments ?? [];
|
|
295
|
+
const body = root.find(j.Program).get().node.body;
|
|
296
|
+
const index = body.indexOf(node);
|
|
297
|
+
path.prune();
|
|
298
|
+
if (comments.length === 0 || index === -1) return;
|
|
299
|
+
const next = body[index];
|
|
300
|
+
if (next) {
|
|
301
|
+
next.comments = [...comments, ...(next.comments ?? [])];
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const previous = body[index - 1];
|
|
305
|
+
if (previous) {
|
|
306
|
+
previous.comments = [
|
|
307
|
+
...(previous.comments ?? []),
|
|
308
|
+
...comments.map((/** @type {any} */ c) => ({
|
|
309
|
+
...c,
|
|
310
|
+
leading: false,
|
|
311
|
+
trailing: true,
|
|
312
|
+
})),
|
|
313
|
+
];
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
keepCommentsInEmptyModule(j, root.find(j.Program).get().node, comments);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* A module the codemod emptied keeps its TODOs on an `export {};`, which also
|
|
321
|
+
* keeps it a module.
|
|
322
|
+
*
|
|
323
|
+
* @param {any} j
|
|
324
|
+
* @param {any} program
|
|
325
|
+
* @param {any[]} comments
|
|
326
|
+
*/
|
|
327
|
+
function keepCommentsInEmptyModule(j, program, comments) {
|
|
328
|
+
const decl = j.exportNamedDeclaration(null, []);
|
|
329
|
+
decl.comments = comments.map((/** @type {any} */ c) => ({
|
|
330
|
+
...c,
|
|
331
|
+
leading: true,
|
|
332
|
+
trailing: false,
|
|
333
|
+
}));
|
|
334
|
+
program.body.push(decl);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* A removed 0.1 type with no Astryx counterpart loses its import. A local
|
|
339
|
+
* re-export of it (`import type { X } from …; export type { X };`) goes too,
|
|
340
|
+
* the same way a re-export straight from ui-common does: left behind it names
|
|
341
|
+
* nothing, and the file no longer parses. No placeholder type is declared in
|
|
342
|
+
* its place: an `unknown` alias would keep every importer compiling against a
|
|
343
|
+
* type that no longer exists. The TODO goes on the export, or where it stood.
|
|
344
|
+
*
|
|
345
|
+
* @param {any} j
|
|
346
|
+
* @param {any} root
|
|
347
|
+
* @param {Map<string, {imported: string, component: string}>} dropped
|
|
348
|
+
* @param {any} programScope
|
|
349
|
+
*/
|
|
350
|
+
function dropLocalReexports(j, root, dropped, programScope) {
|
|
351
|
+
root.find(j.ExportNamedDeclaration).forEach((/** @type {any} */ path) => {
|
|
352
|
+
const node = path.node;
|
|
353
|
+
if (node.source || node.declaration) return;
|
|
354
|
+
const specifiers = node.specifiers ?? [];
|
|
355
|
+
const gone = specifiers.filter(
|
|
356
|
+
(/** @type {any} */ s) =>
|
|
357
|
+
s.local?.name &&
|
|
358
|
+
dropped.has(s.local.name) &&
|
|
359
|
+
isModuleBinding(path, s.local.name, programScope),
|
|
360
|
+
);
|
|
361
|
+
if (gone.length === 0) return;
|
|
362
|
+
const names = gone.map((/** @type {any} */ s) => s.exported?.name ?? s.local.name);
|
|
363
|
+
const components = [
|
|
364
|
+
...new Set(
|
|
365
|
+
gone.map((/** @type {any} */ s) => dropped.get(s.local.name)?.component),
|
|
366
|
+
),
|
|
367
|
+
];
|
|
368
|
+
const message = `${names.join(", ")} (removed with ${components.join(", ")} in 0.2, no Astryx counterpart) ${names.length === 1 ? "is" : "are"} no longer re-exported from here; modules importing ${names.length === 1 ? "it" : "them"} from here need a type of their own.`;
|
|
369
|
+
const rest = specifiers.filter((/** @type {any} */ s) => !gone.includes(s));
|
|
370
|
+
if (rest.length > 0) {
|
|
371
|
+
node.specifiers = rest;
|
|
372
|
+
addTodo(j, path, message);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
node.comments = [
|
|
376
|
+
...(node.comments ?? []),
|
|
377
|
+
j.commentLine(` ${TODO_TAG}: ${message}`, true, false),
|
|
378
|
+
];
|
|
379
|
+
pruneKeepingComments(j, root, path);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* @param {{source: string, path: string}} file
|
|
385
|
+
* @param {{jscodeshift: any}} api
|
|
386
|
+
* @param {{flags: {packages: Map<string, string>, touched: Set<string>}}} ctx
|
|
387
|
+
*/
|
|
388
|
+
export default function transform(file, api, ctx) {
|
|
389
|
+
const direct = file.source.includes(UIC);
|
|
390
|
+
const viaLocal = importsLocalLegacy(api.jscodeshift, ctx, file.path, file.source);
|
|
391
|
+
if (!direct && !viaLocal) return undefined;
|
|
392
|
+
const j = api.jscodeshift;
|
|
393
|
+
// Registers the wrappers this module defines, for the report.
|
|
394
|
+
if (direct && ctx.source) localExports(j, ctx, file.path);
|
|
395
|
+
const root = j(file.source);
|
|
396
|
+
const isTS = /\.[cm]?tsx?$/.test(file.path);
|
|
397
|
+
const taken = takenNames(j, root);
|
|
398
|
+
const programScope = root.find(j.Program).get().scope;
|
|
399
|
+
let touched = false;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The local name for an import of `target` that replaces `local`: the
|
|
403
|
+
* target itself when nothing in any scope of the file uses it, else a
|
|
404
|
+
* `Uic`-prefixed alias that is free.
|
|
405
|
+
*
|
|
406
|
+
* @param {string} target
|
|
407
|
+
*/
|
|
408
|
+
const localFor = (target) =>
|
|
409
|
+
freeName(taken, taken.has(target) ? [`Uic${target}`] : [target]);
|
|
410
|
+
|
|
411
|
+
/** @type {Map<string, string>} */
|
|
412
|
+
const renames = new Map();
|
|
413
|
+
/** @type {Map<string, {component: string, entry: any, local: string, jsxCount: number, valueRefs: number, add: any}>} */
|
|
414
|
+
const bindings = new Map();
|
|
415
|
+
/** @type {Array<{path: any, keep: any[], adds: any[], pruned?: boolean}>} */
|
|
416
|
+
const plans = [];
|
|
417
|
+
/** @type {any[]} */
|
|
418
|
+
const extra = [];
|
|
419
|
+
/**
|
|
420
|
+
* Local bindings of removed 0.1 types whose import was dropped: a local
|
|
421
|
+
* `export type { X }` of one goes with it (see dropLocalReexports).
|
|
422
|
+
*
|
|
423
|
+
* @type {Map<string, {imported: string, component: string}>}
|
|
424
|
+
*/
|
|
425
|
+
const droppedTypes = new Map();
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The local name to use for `name` from `@lablup/ui-common/<subpath>`,
|
|
429
|
+
* importing it if nothing does yet.
|
|
430
|
+
*
|
|
431
|
+
* @param {string} name
|
|
432
|
+
* @param {string} subpath
|
|
433
|
+
*/
|
|
434
|
+
const ensureImport = (name, subpath) => {
|
|
435
|
+
const source = `${UIC}/${subpath}`;
|
|
436
|
+
for (const plan of plans) {
|
|
437
|
+
const hit = plan.adds.find(
|
|
438
|
+
(a) => a.source === source && a.imported === name && a.kind === "value",
|
|
439
|
+
);
|
|
440
|
+
if (hit) {
|
|
441
|
+
hit.used = true;
|
|
442
|
+
return hit.local;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const existing = extra.find((a) => a.source === source && a.imported === name);
|
|
446
|
+
if (existing) return existing.local;
|
|
447
|
+
const local = localFor(name);
|
|
448
|
+
extra.push({ source, kind: "value", imported: name, local });
|
|
449
|
+
return local;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* An import from a project module that hands 0.1 components on (a barrel
|
|
454
|
+
* the codemod rewrites under the 0.1 names): its components get the same
|
|
455
|
+
* element rewrite as a direct import, and the import itself stays. Its
|
|
456
|
+
* removed types go, as from ui-common. A wrapper's import is only counted.
|
|
457
|
+
*
|
|
458
|
+
* @param {any} path
|
|
459
|
+
* @param {string} source
|
|
460
|
+
*/
|
|
461
|
+
const localImport = (path, source) => {
|
|
462
|
+
const target = ctx.resolveImport?.(file.path, source);
|
|
463
|
+
const entries = target ? localExports(j, ctx, target) : null;
|
|
464
|
+
if (!entries || entries.size === 0) return;
|
|
465
|
+
const declIsType = path.node.importKind === "type";
|
|
466
|
+
const kept = [];
|
|
467
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
468
|
+
const name =
|
|
469
|
+
spec.type === "ImportSpecifier"
|
|
470
|
+
? spec.imported.name
|
|
471
|
+
: spec.type === "ImportDefaultSpecifier"
|
|
472
|
+
? "default"
|
|
473
|
+
: null;
|
|
474
|
+
const entry = name == null ? undefined : entries.get(name);
|
|
475
|
+
if (entry?.kind === "type" && entry.removed) {
|
|
476
|
+
touched = true;
|
|
477
|
+
addTodo(
|
|
478
|
+
j,
|
|
479
|
+
path,
|
|
480
|
+
`type ${entry.type} was removed with ${entry.component} in 0.2 and has no Astryx counterpart; ${source} no longer exports it.`,
|
|
481
|
+
);
|
|
482
|
+
droppedTypes.set(spec.local.name, {
|
|
483
|
+
imported: entry.type,
|
|
484
|
+
component: entry.component,
|
|
485
|
+
});
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
kept.push(spec);
|
|
489
|
+
if (entry?.kind === "wrapper") registerWrapper(ctx, entry, file.path);
|
|
490
|
+
if (entry?.kind !== "component") continue;
|
|
491
|
+
if (declIsType || spec.importKind === "type") continue;
|
|
492
|
+
touched = true;
|
|
493
|
+
ctx.flags.touched.add(entry.component);
|
|
494
|
+
bindings.set(spec.local.name, {
|
|
495
|
+
component: entry.component,
|
|
496
|
+
entry: REMOVED.get(entry.component),
|
|
497
|
+
local: spec.local.name,
|
|
498
|
+
jsxCount: 0,
|
|
499
|
+
valueRefs: 0,
|
|
500
|
+
add: null,
|
|
501
|
+
via: path,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (kept.length > 0) path.node.specifiers = kept;
|
|
505
|
+
else pruneKeepingComments(j, root, path);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
|
|
509
|
+
const source = path.node.source.value;
|
|
510
|
+
const from = typeof source === "string" ? classify(source) : null;
|
|
511
|
+
if (!from) {
|
|
512
|
+
if (viaLocal && typeof source === "string") localImport(path, source);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const specifiers = path.node.specifiers ?? [];
|
|
516
|
+
if (specifiers.length === 0) return;
|
|
517
|
+
const declIsType = path.node.importKind === "type";
|
|
518
|
+
const keep = [];
|
|
519
|
+
const adds = [];
|
|
520
|
+
for (const spec of specifiers) {
|
|
521
|
+
if (spec.type === "ImportNamespaceSpecifier") {
|
|
522
|
+
keep.push(spec);
|
|
523
|
+
addTodo(
|
|
524
|
+
j,
|
|
525
|
+
path,
|
|
526
|
+
`namespace import of ${source}: in 0.2 Badge, Button, Select, Tabs and the other removed 0.1 components are Astryx's (or gone). Rewrite the ${spec.local.name}.X uses by hand.`,
|
|
527
|
+
);
|
|
528
|
+
touched = true;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const imported =
|
|
532
|
+
spec.type === "ImportDefaultSpecifier"
|
|
533
|
+
? from.kind === "component"
|
|
534
|
+
? from.dir
|
|
535
|
+
: null
|
|
536
|
+
: spec.imported.name;
|
|
537
|
+
if (imported == null) {
|
|
538
|
+
keep.push(spec);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const local = spec.local?.name ?? imported;
|
|
542
|
+
const kind = declIsType || spec.importKind === "type" ? "type" : "value";
|
|
543
|
+
const resolved = resolveName(imported, from);
|
|
544
|
+
|
|
545
|
+
if (resolved.action === "keep") {
|
|
546
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
547
|
+
adds.push({ source, kind, imported, local });
|
|
548
|
+
touched = true;
|
|
549
|
+
} else {
|
|
550
|
+
keep.push(spec);
|
|
551
|
+
}
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
touched = true;
|
|
555
|
+
if (resolved.action === "move") {
|
|
556
|
+
adds.push({ source: resolved.source, kind, imported, local });
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (resolved.action === "component") {
|
|
560
|
+
const entry = resolved.entry;
|
|
561
|
+
let finalLocal = local;
|
|
562
|
+
if (local === imported && entry.to !== local) {
|
|
563
|
+
finalLocal = localFor(entry.to);
|
|
564
|
+
renames.set(local, finalLocal);
|
|
565
|
+
}
|
|
566
|
+
const add = {
|
|
567
|
+
source: `${UIC}/${entry.subpath}`,
|
|
568
|
+
kind,
|
|
569
|
+
imported: entry.to,
|
|
570
|
+
local: finalLocal,
|
|
571
|
+
};
|
|
572
|
+
adds.push(add);
|
|
573
|
+
ctx.flags.touched.add(imported);
|
|
574
|
+
if (kind === "value") {
|
|
575
|
+
bindings.set(local, {
|
|
576
|
+
component: imported,
|
|
577
|
+
entry,
|
|
578
|
+
local: finalLocal,
|
|
579
|
+
jsxCount: 0,
|
|
580
|
+
valueRefs: 0,
|
|
581
|
+
add,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
for (const [name, range] of Object.entries(entry.requiresPackages)) {
|
|
585
|
+
ctx.flags.packages.set(name, range);
|
|
586
|
+
}
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
// A 0.1 type.
|
|
590
|
+
if (resolved.to == null) {
|
|
591
|
+
addTodo(
|
|
592
|
+
j,
|
|
593
|
+
path,
|
|
594
|
+
`type ${imported} was removed with ${resolved.component} in 0.2 and has no Astryx counterpart.`,
|
|
595
|
+
);
|
|
596
|
+
droppedTypes.set(local, { imported, component: resolved.component });
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
let finalLocal = local;
|
|
600
|
+
if (local === imported && resolved.to !== local) {
|
|
601
|
+
finalLocal = localFor(resolved.to);
|
|
602
|
+
renames.set(local, finalLocal);
|
|
603
|
+
}
|
|
604
|
+
adds.push({
|
|
605
|
+
source: `${UIC}/${REMOVED.get(resolved.component).subpath}`,
|
|
606
|
+
kind: "type",
|
|
607
|
+
imported: resolved.to,
|
|
608
|
+
local: finalLocal,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
plans.push({ path, keep, adds });
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* A type re-export from a project module (`export type { X }` from `./DataTable`)
|
|
616
|
+
* of a type that module no longer exports (its own import of X from ui-common
|
|
617
|
+
* was dropped): drop it too.
|
|
618
|
+
*
|
|
619
|
+
* @param {any} path
|
|
620
|
+
* @param {string} source
|
|
621
|
+
*/
|
|
622
|
+
const localReexport = (path, source) => {
|
|
623
|
+
const target = ctx.resolveImport?.(file.path, source);
|
|
624
|
+
const entries = target ? localExports(j, ctx, target) : null;
|
|
625
|
+
if (!entries || entries.size === 0) return;
|
|
626
|
+
const specifiers = path.node.specifiers ?? [];
|
|
627
|
+
const gone = specifiers.filter(
|
|
628
|
+
(/** @type {any} */ s) =>
|
|
629
|
+
s.type === "ExportSpecifier" && isRemovedType(entries.get(s.local?.name)),
|
|
630
|
+
);
|
|
631
|
+
if (gone.length === 0) return;
|
|
632
|
+
touched = true;
|
|
633
|
+
const names = gone.map((/** @type {any} */ s) => s.exported.name);
|
|
634
|
+
const message = `${names.join(", ")} ${names.length === 1 ? "is" : "are"} no longer re-exported: ${source} does not export ${names.length === 1 ? "it" : "them"} any more (removed in 0.2, no Astryx counterpart).`;
|
|
635
|
+
const rest = specifiers.filter((/** @type {any} */ s) => !gone.includes(s));
|
|
636
|
+
if (rest.length > 0) {
|
|
637
|
+
path.node.specifiers = rest;
|
|
638
|
+
addTodo(j, path, message);
|
|
639
|
+
} else {
|
|
640
|
+
path.node.comments = [
|
|
641
|
+
...(path.node.comments ?? []),
|
|
642
|
+
j.commentLine(` ${TODO_TAG}: ${message}`, true, false),
|
|
643
|
+
];
|
|
644
|
+
pruneKeepingComments(j, root, path);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// Re-exports: `export { Select } from "@lablup/ui-common"` keeps its name.
|
|
649
|
+
root.find(j.ExportNamedDeclaration).forEach((/** @type {any} */ path) => {
|
|
650
|
+
const source = path.node.source?.value;
|
|
651
|
+
const from = typeof source === "string" ? classify(source) : null;
|
|
652
|
+
if (!from) {
|
|
653
|
+
if (viaLocal && typeof source === "string") localReexport(path, source);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const declIsType = path.node.exportKind === "type";
|
|
657
|
+
const keep = [];
|
|
658
|
+
/** @type {Map<string, any[]>} */
|
|
659
|
+
const moved = new Map();
|
|
660
|
+
let reexportedComponent = false;
|
|
661
|
+
let droppedSpecs = 0;
|
|
662
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
663
|
+
const localName = spec.local?.name ?? spec.exported.name;
|
|
664
|
+
const imported =
|
|
665
|
+
localName === "default" && from.kind === "component" ? from.dir : localName;
|
|
666
|
+
const exported = spec.exported.name;
|
|
667
|
+
const kind = declIsType || spec.exportKind === "type" ? "type" : "value";
|
|
668
|
+
const resolved = resolveName(imported, from);
|
|
669
|
+
let target = null;
|
|
670
|
+
let targetSource = null;
|
|
671
|
+
if (resolved.action === "move") {
|
|
672
|
+
target = imported;
|
|
673
|
+
targetSource = resolved.source;
|
|
674
|
+
} else if (resolved.action === "component") {
|
|
675
|
+
target = resolved.entry.to;
|
|
676
|
+
targetSource = `${UIC}/${resolved.entry.subpath}`;
|
|
677
|
+
reexportedComponent = true;
|
|
678
|
+
for (const [name, range] of Object.entries(resolved.entry.requiresPackages)) {
|
|
679
|
+
ctx.flags.packages.set(name, range);
|
|
680
|
+
}
|
|
681
|
+
} else if (resolved.action === "type") {
|
|
682
|
+
if (resolved.to == null) {
|
|
683
|
+
addTodo(
|
|
684
|
+
j,
|
|
685
|
+
path,
|
|
686
|
+
`type ${imported} was removed with ${resolved.component} in 0.2 and has no Astryx counterpart.`,
|
|
687
|
+
);
|
|
688
|
+
touched = true;
|
|
689
|
+
droppedSpecs++;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
target = resolved.to;
|
|
693
|
+
targetSource = `${UIC}/${REMOVED.get(resolved.component).subpath}`;
|
|
694
|
+
} else if (localName === "default" && from.kind === "component") {
|
|
695
|
+
target = imported;
|
|
696
|
+
targetSource = source;
|
|
697
|
+
}
|
|
698
|
+
if (target == null || targetSource == null) {
|
|
699
|
+
keep.push(spec);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
touched = true;
|
|
703
|
+
const key = `${kind}\u0000${targetSource}`;
|
|
704
|
+
const list = moved.get(key) ?? [];
|
|
705
|
+
list.push(
|
|
706
|
+
j.exportSpecifier.from({
|
|
707
|
+
local: j.identifier(target),
|
|
708
|
+
exported: j.identifier(exported),
|
|
709
|
+
}),
|
|
710
|
+
);
|
|
711
|
+
moved.set(key, list);
|
|
712
|
+
}
|
|
713
|
+
if (moved.size === 0) {
|
|
714
|
+
// Only removed types: they go, and the TODO stays where they stood.
|
|
715
|
+
if (droppedSpecs === 0) return;
|
|
716
|
+
if (keep.length > 0) path.node.specifiers = keep;
|
|
717
|
+
else pruneKeepingComments(j, root, path);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const decls = [...moved.entries()].map(([key, specs]) => {
|
|
721
|
+
const [kind, targetSource] = key.split("\u0000");
|
|
722
|
+
const decl = j.exportNamedDeclaration(null, specs, j.stringLiteral(targetSource));
|
|
723
|
+
if (kind === "type") decl.exportKind = "type";
|
|
724
|
+
return decl;
|
|
725
|
+
});
|
|
726
|
+
if (reexportedComponent) {
|
|
727
|
+
decls[0].comments = [
|
|
728
|
+
j.commentLine(
|
|
729
|
+
" TODO(ui-common-upgrade): re-exported under the 0.1 name, but the component is Astryx's now. The upgrade migrated the elements of it in the modules it scanned that import it from here; any other importer still passes 0.1 props.",
|
|
730
|
+
true,
|
|
731
|
+
false,
|
|
732
|
+
),
|
|
733
|
+
];
|
|
734
|
+
}
|
|
735
|
+
if (keep.length > 0) {
|
|
736
|
+
path.node.specifiers = keep;
|
|
737
|
+
insertBeforeKeepingComments(path, decls);
|
|
738
|
+
} else {
|
|
739
|
+
reuseNode(path, decls, "exportKind");
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
if (droppedTypes.size > 0) dropLocalReexports(j, root, droppedTypes, programScope);
|
|
744
|
+
|
|
745
|
+
root.find(j.ExportAllDeclaration).forEach((/** @type {any} */ path) => {
|
|
746
|
+
const source = path.node.source?.value;
|
|
747
|
+
if (typeof source !== "string" || !classify(source)) return;
|
|
748
|
+
touched = true;
|
|
749
|
+
addTodo(
|
|
750
|
+
j,
|
|
751
|
+
path,
|
|
752
|
+
`\`export *\` from ${source} now re-exports Astryx's Badge, Button, Tooltip, … under the 0.1 names. The upgrade migrated the elements of them in the modules it scanned that import them from here; any other importer still passes 0.1 props.`,
|
|
753
|
+
);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// Dynamic imports of a removed component's subpath.
|
|
757
|
+
root.find(j.CallExpression).forEach((/** @type {any} */ path) => {
|
|
758
|
+
const node = path.node;
|
|
759
|
+
const isDynamic = node.callee.type === "Import";
|
|
760
|
+
const isRequire =
|
|
761
|
+
node.callee.type === "Identifier" && node.callee.name === "require";
|
|
762
|
+
if (!isDynamic && !isRequire) return;
|
|
763
|
+
const arg = node.arguments[0];
|
|
764
|
+
const source = arg?.value;
|
|
765
|
+
if (typeof source !== "string") return;
|
|
766
|
+
const from = classify(source);
|
|
767
|
+
if (from?.kind === "component" && REMOVED.has(from.dir)) {
|
|
768
|
+
touched = true;
|
|
769
|
+
addTodo(
|
|
770
|
+
j,
|
|
771
|
+
path,
|
|
772
|
+
`${source} is gone in 0.2; ${from.dir} is Astryx's ${REMOVED.get(from.dir).to} at ${UIC}/${REMOVED.get(from.dir).subpath}.`,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
if (!touched) return undefined;
|
|
778
|
+
|
|
779
|
+
// Elements.
|
|
780
|
+
const spelled = new Set([...identifierNames(j, root), ...taken]);
|
|
781
|
+
for (const [local, binding] of bindings) {
|
|
782
|
+
const transformElement = /** @type {Record<string, any>} */ (ELEMENT_TRANSFORMS)[
|
|
783
|
+
binding.component
|
|
784
|
+
];
|
|
785
|
+
root.findJSXElements(local).forEach((/** @type {any} */ path) => {
|
|
786
|
+
// A local component that shadows the import is not ui-common's.
|
|
787
|
+
if (!isModuleBinding(path, local, programScope)) return;
|
|
788
|
+
const el = path.node;
|
|
789
|
+
let tag = null;
|
|
790
|
+
/** @type {import('./elements.mjs').Helpers} */
|
|
791
|
+
const helpers = {
|
|
792
|
+
j,
|
|
793
|
+
path,
|
|
794
|
+
el,
|
|
795
|
+
isTS,
|
|
796
|
+
removed: binding.entry,
|
|
797
|
+
todo: (message) => addTodo(j, path, message),
|
|
798
|
+
ensureImport,
|
|
799
|
+
setTag: (name) => {
|
|
800
|
+
tag = name;
|
|
801
|
+
},
|
|
802
|
+
freeName: (candidates) => freeName(spelled, candidates),
|
|
803
|
+
};
|
|
804
|
+
if (hasSpread(el)) {
|
|
805
|
+
addTodo(
|
|
806
|
+
j,
|
|
807
|
+
path,
|
|
808
|
+
`props spread into <${tagName(el)}> are not migrated; check them against Astryx ${binding.entry.to}'s props.`,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
transformElement?.(helpers);
|
|
812
|
+
if (tag) renameElement(el, tag);
|
|
813
|
+
else binding.jsxCount++;
|
|
814
|
+
});
|
|
815
|
+
root.find(j.Identifier, { name: local }).forEach((/** @type {any} */ path) => {
|
|
816
|
+
if (path.node.type === "JSXIdentifier" || !isReference(path)) return;
|
|
817
|
+
if (!isModuleBinding(path, local, programScope)) return;
|
|
818
|
+
const parent = path.parent?.node;
|
|
819
|
+
if (parent?.type === "TSTypeQuery" || parent?.type === "TSTypeReference") return;
|
|
820
|
+
binding.valueRefs++;
|
|
821
|
+
addTodo(
|
|
822
|
+
j,
|
|
823
|
+
path,
|
|
824
|
+
`${local} is used as a value here; props passed to it this way are not migrated to Astryx ${binding.entry.to}.`,
|
|
825
|
+
);
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// A local-barrel import whose every element became another component
|
|
830
|
+
// (Button → IconButton) is unused now.
|
|
831
|
+
for (const [local, binding] of bindings) {
|
|
832
|
+
if (!binding.via || binding.via.pruned) continue;
|
|
833
|
+
let used = false;
|
|
834
|
+
root.findJSXElements(local).forEach((/** @type {any} */ p) => {
|
|
835
|
+
if (isModuleBinding(p, local, programScope)) used = true;
|
|
836
|
+
});
|
|
837
|
+
root.find(j.Identifier, { name: local }).forEach((/** @type {any} */ p) => {
|
|
838
|
+
if (p.node.type === "JSXIdentifier" || !isReference(p)) return;
|
|
839
|
+
if (isModuleBinding(p, local, programScope)) used = true;
|
|
840
|
+
});
|
|
841
|
+
if (used) continue;
|
|
842
|
+
const node = binding.via.node;
|
|
843
|
+
node.specifiers = (node.specifiers ?? []).filter(
|
|
844
|
+
(/** @type {any} */ s) => s.local?.name !== local,
|
|
845
|
+
);
|
|
846
|
+
if (node.specifiers.length === 0) {
|
|
847
|
+
pruneKeepingComments(j, root, binding.via);
|
|
848
|
+
binding.via.pruned = true;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
for (const [from, to] of renames) renameReferences(j, root, from, to, programScope);
|
|
853
|
+
|
|
854
|
+
// Imports: the planned moves, minus a Card import every BaseCard outgrew.
|
|
855
|
+
const unused = new Set(
|
|
856
|
+
[...bindings.values()]
|
|
857
|
+
.filter(
|
|
858
|
+
(b) =>
|
|
859
|
+
b.add != null &&
|
|
860
|
+
b.component === "BaseCard" &&
|
|
861
|
+
b.jsxCount === 0 &&
|
|
862
|
+
b.valueRefs === 0 &&
|
|
863
|
+
!b.add.used,
|
|
864
|
+
)
|
|
865
|
+
.map((b) => b.add),
|
|
866
|
+
);
|
|
867
|
+
for (const plan of plans) {
|
|
868
|
+
const adds = plan.adds.filter((a) => !unused.has(a));
|
|
869
|
+
const decls = buildImports(j, adds);
|
|
870
|
+
const node = plan.path.node;
|
|
871
|
+
if (plan.keep.length > 0) {
|
|
872
|
+
node.specifiers = plan.keep;
|
|
873
|
+
if (decls.length > 0) insertBeforeKeepingComments(plan.path, decls);
|
|
874
|
+
} else if (decls.length > 0) {
|
|
875
|
+
reuseNode(plan.path, decls, "importKind");
|
|
876
|
+
} else {
|
|
877
|
+
const comments = node.comments;
|
|
878
|
+
plan.path.prune();
|
|
879
|
+
plan.pruned = true;
|
|
880
|
+
if (comments?.length) {
|
|
881
|
+
const program = root.find(j.Program).get().node;
|
|
882
|
+
const first = program.body[0];
|
|
883
|
+
if (first) first.comments = [...comments, ...(first.comments ?? [])];
|
|
884
|
+
else keepCommentsInEmptyModule(j, program, comments);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (extra.length > 0) {
|
|
889
|
+
// Imports a rewrite needed that no 0.1 import mapped to (EmptyState's
|
|
890
|
+
// actions render a Button): after the last rewritten import.
|
|
891
|
+
const anchor = plans.find((plan) => !plan.pruned);
|
|
892
|
+
const decls = buildImports(j, extra);
|
|
893
|
+
if (anchor) anchor.path.insertAfter(...decls);
|
|
894
|
+
else
|
|
895
|
+
root
|
|
896
|
+
.find(j.Program)
|
|
897
|
+
.get()
|
|
898
|
+
.node.body.unshift(...decls);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const out = printSource(j, root, { quote: detectQuote(file.source) });
|
|
902
|
+
return file.source.endsWith("\n") && !out.endsWith("\n") ? `${out}\n` : out;
|
|
903
|
+
}
|