@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,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.1 -> 0.2 package.json edits:
|
|
3
|
+
* - bump @lablup/ui-common to the target version (keeping `^`/`~`);
|
|
4
|
+
* - add @lablup/ui-common-cli, the `ui-common` bin, to devDependencies at
|
|
5
|
+
* exactly the target version (0.1 shipped the bin inside ui-common);
|
|
6
|
+
* - add the @stylexjs/stylex peer ui-common 0.2 needs, when missing;
|
|
7
|
+
* - in a library, drop React 18 from the react / react-dom peer ranges;
|
|
8
|
+
* - in a pnpm project, decline the Astryx postinstalls in `allowBuilds`;
|
|
9
|
+
* - add @astryxdesign/lab, pinned to the canary ui-common is built against,
|
|
10
|
+
* when a Drawer import was moved to `@lablup/ui-common/lab`, and point its
|
|
11
|
+
* core peer at ui-common's core with the project's package manager's
|
|
12
|
+
* override (../../cli/lab-peer.mjs).
|
|
13
|
+
*/
|
|
14
|
+
import { applyLabOverride, CORE, detectPackageManager } from "../../cli/lab-peer.mjs";
|
|
15
|
+
import { targetUiCommonRoot, uiCommonPackageJson } from "../../cli/paths.mjs";
|
|
16
|
+
import { coerce, narrowToFloor } from "../../cli/semver.mjs";
|
|
17
|
+
import { LAB_PACKAGE, stylexPeer, UIC } from "./map.mjs";
|
|
18
|
+
|
|
19
|
+
/** The CLI package, released in lockstep with ui-common. */
|
|
20
|
+
export const CLI_PACKAGE = "@lablup/ui-common-cli";
|
|
21
|
+
|
|
22
|
+
const FIELDS = /** @type {const} */ ([
|
|
23
|
+
"dependencies",
|
|
24
|
+
"devDependencies",
|
|
25
|
+
"peerDependencies",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/** @param {Record<string, string>} object */
|
|
29
|
+
function isSorted(object) {
|
|
30
|
+
const keys = Object.keys(object);
|
|
31
|
+
return keys.every((k, i) => i === 0 || keys[i - 1].localeCompare(k) <= 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {any} pkg
|
|
36
|
+
* @param {string} field
|
|
37
|
+
* @param {string} name
|
|
38
|
+
* @param {string} range
|
|
39
|
+
*/
|
|
40
|
+
function addDependency(pkg, field, name, range) {
|
|
41
|
+
const current = pkg[field] ?? {};
|
|
42
|
+
const next = { ...current, [name]: range };
|
|
43
|
+
pkg[field] = isSorted(current)
|
|
44
|
+
? Object.fromEntries(Object.entries(next).sort(([a], [b]) => a.localeCompare(b)))
|
|
45
|
+
: next;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} spec
|
|
50
|
+
* @param {string} to
|
|
51
|
+
* @returns {{value: string, note?: string} | null}
|
|
52
|
+
*/
|
|
53
|
+
function bumpSpec(spec, to) {
|
|
54
|
+
if (/^(workspace:|link:|file:|npm:|git|https?:)/.test(spec)) return null;
|
|
55
|
+
const simple = /^([\^~]?)v?\d+(\.\d+){0,2}(-[0-9A-Za-z.-]+)?$/.exec(spec.trim());
|
|
56
|
+
if (simple) return { value: `${simple[1]}${to}` };
|
|
57
|
+
return {
|
|
58
|
+
value: `^${to}`,
|
|
59
|
+
note: `the range "${spec}" was replaced with "^${to}"; widen it again if this package must still accept 0.1.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Packages whose postinstall pnpm 11 must be told to run or skip. */
|
|
64
|
+
export const DECLINED_BUILDS = ["@astryxdesign/core", "@astryxdesign/cli"];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* `pnpm-workspace.yaml` with `allowBuilds` declining each of `names` it does
|
|
68
|
+
* not decide yet. An entry set to true or false is left as it is; one with
|
|
69
|
+
* any other value (pnpm writes "set this to true or false" when it stops an
|
|
70
|
+
* install) counts as undecided and is set to false. Comments and every other
|
|
71
|
+
* line are kept.
|
|
72
|
+
*
|
|
73
|
+
* @param {string | null} yaml current text, null when there is no file
|
|
74
|
+
* @param {string[]} names
|
|
75
|
+
* @returns {{yaml?: string, notes: string[]}}
|
|
76
|
+
*/
|
|
77
|
+
export function applyAllowBuilds(yaml, names) {
|
|
78
|
+
const line = (/** @type {string} */ name, indent = " ") =>
|
|
79
|
+
`${indent}"${name}": false`;
|
|
80
|
+
const why =
|
|
81
|
+
"their postinstall only prints an `astryx init` nudge, and pnpm 11 stops the install (ERR_PNPM_IGNORED_BUILDS) until each is allowed or declined";
|
|
82
|
+
if (yaml == null) {
|
|
83
|
+
return {
|
|
84
|
+
yaml: `allowBuilds:\n${names.map((n) => line(n)).join("\n")}\n`,
|
|
85
|
+
notes: [
|
|
86
|
+
`wrote pnpm-workspace.yaml with allowBuilds declining ${names.join(", ")}: ${why}.`,
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const block = /^allowBuilds:[ \t]*(#.*)?(\r?\n)/m.exec(yaml);
|
|
91
|
+
if (!block) {
|
|
92
|
+
if (/^allowBuilds\s*:/m.test(yaml)) {
|
|
93
|
+
return {
|
|
94
|
+
notes: [
|
|
95
|
+
`pnpm-workspace.yaml has an allowBuilds entry this cannot edit; decline ${names.join(", ")} in it: ${why}.`,
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const base = yaml.replace(/\s*$/, "");
|
|
100
|
+
return {
|
|
101
|
+
yaml: `${base}${base ? "\n\n" : ""}allowBuilds:\n${names.map((n) => line(n)).join("\n")}\n`,
|
|
102
|
+
notes: [
|
|
103
|
+
`added allowBuilds declining ${names.join(", ")} to pnpm-workspace.yaml: ${why}.`,
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const at = block.index + block[0].length;
|
|
108
|
+
const after = yaml.slice(at);
|
|
109
|
+
const end = /^\S/m.exec(after)?.index ?? after.length;
|
|
110
|
+
let body = after.slice(0, end);
|
|
111
|
+
const indent = /^([ \t]+)\S/m.exec(body)?.[1] ?? " ";
|
|
112
|
+
const notes = [];
|
|
113
|
+
const added = [];
|
|
114
|
+
for (const name of names) {
|
|
115
|
+
const esc = name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
116
|
+
const entry = new RegExp(
|
|
117
|
+
`^([ \\t]+["']?${esc}["']?[ \\t]*:[ \\t]*)([^\\r\\n#]*?)([ \\t]*(#.*)?)$`,
|
|
118
|
+
"m",
|
|
119
|
+
);
|
|
120
|
+
const match = entry.exec(body);
|
|
121
|
+
if (!match) {
|
|
122
|
+
added.push(name);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
126
|
+
if (value === "true" || value === "false") continue;
|
|
127
|
+
body = body.replace(entry, (_m, head, _v, tail) => `${head}false${tail}`);
|
|
128
|
+
notes.push(
|
|
129
|
+
`pnpm-workspace.yaml allowBuilds["${name}"] was "${match[2].trim()}", which pnpm does not accept; set it to false.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (added.length > 0) {
|
|
133
|
+
const trimmed = body.replace(/\s*$/, "");
|
|
134
|
+
const rest = body.slice(trimmed.length);
|
|
135
|
+
body = `${trimmed}${trimmed ? "\n" : ""}${added.map((n) => line(n, indent)).join("\n")}${rest.includes("\n") ? rest : "\n"}`;
|
|
136
|
+
notes.push(
|
|
137
|
+
`added ${added.join(", ")} to allowBuilds in pnpm-workspace.yaml, declined: ${why}.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const next = `${yaml.slice(0, at)}${body}${after.slice(end)}`;
|
|
141
|
+
return next === yaml ? { notes } : { yaml: next, notes };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** React's floor when the target ui-common's peer cannot be read. */
|
|
145
|
+
const REACT_FLOOR = "19.2.0";
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A React range narrowed to `floor` and up (`>=18 <21 || ^22` →
|
|
149
|
+
* `>=19.2.0 <21 || ^22`); null when it admits no version that high, the
|
|
150
|
+
* range itself when it already starts there or is not a version range.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} range
|
|
153
|
+
* @param {string} floor
|
|
154
|
+
* @returns {string | null}
|
|
155
|
+
*/
|
|
156
|
+
export function narrowReactRange(range, floor) {
|
|
157
|
+
if (/^(workspace:|link:|file:|npm:|catalog:)/.test(range.trim())) return range;
|
|
158
|
+
return narrowToFloor(range, floor);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Point the lab canary's core peer at ui-common's core: `overrides` in
|
|
163
|
+
* package.json for npm (the project's own, when it is the install root),
|
|
164
|
+
* `overrides` in pnpm-workspace.yaml for pnpm, a note otherwise.
|
|
165
|
+
*
|
|
166
|
+
* @param {any} pkg parsed package.json, edited in place
|
|
167
|
+
* @param {{projectDir?: string, note: (message: string) => void, editFile?: (path: string, edit: (current: string | null) => string | undefined) => void}} ctx
|
|
168
|
+
*/
|
|
169
|
+
function addLabOverride(pkg, ctx) {
|
|
170
|
+
const pin = uiCommonPackageJson(targetUiCommonRoot(ctx.projectDir)).dependencies?.[
|
|
171
|
+
CORE
|
|
172
|
+
];
|
|
173
|
+
if (!pin || !ctx.projectDir || !ctx.editFile) return;
|
|
174
|
+
const { manager, root, workspaceYaml } = detectPackageManager(ctx.projectDir, pkg);
|
|
175
|
+
if (manager === "npm" && root !== ctx.projectDir) {
|
|
176
|
+
// npm reads overrides from the install root's package.json only.
|
|
177
|
+
const { note } = applyLabOverride({ manager: null, pin });
|
|
178
|
+
ctx.note(`npm installs from ${root}, not this package: ${note}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (manager === "pnpm" && workspaceYaml) {
|
|
182
|
+
const yamlFile = workspaceYaml;
|
|
183
|
+
ctx.editFile(yamlFile, (current) => {
|
|
184
|
+
const edit = applyLabOverride({ manager, pin, workspaceYaml: current });
|
|
185
|
+
if (edit.note) ctx.note(edit.note);
|
|
186
|
+
return edit.workspaceYaml;
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const { note } = applyLabOverride({ manager, pin, pkg });
|
|
191
|
+
ctx.note(note);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {string} text package.json source
|
|
196
|
+
* @param {{to: string, flags: {packages: Map<string, string>}, note: (message: string) => void}} ctx
|
|
197
|
+
*/
|
|
198
|
+
export function transformPackageJson(text, ctx) {
|
|
199
|
+
const pkg = JSON.parse(text);
|
|
200
|
+
const indent = /^([ \t]+)"/m.exec(text)?.[1] ?? " ";
|
|
201
|
+
const fields = FIELDS.filter((f) => pkg[f]?.[UIC] != null);
|
|
202
|
+
if (fields.length === 0) return undefined;
|
|
203
|
+
|
|
204
|
+
for (const field of fields) {
|
|
205
|
+
const spec = pkg[field][UIC];
|
|
206
|
+
const bumped = bumpSpec(spec, ctx.to);
|
|
207
|
+
if (!bumped) {
|
|
208
|
+
ctx.note(`${field}["${UIC}"] is "${spec}"; not a version, so it was left alone.`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (bumped.value !== spec) {
|
|
212
|
+
pkg[field][UIC] = bumped.value;
|
|
213
|
+
ctx.note(`${field}["${UIC}"]: "${spec}" → "${bumped.value}".`);
|
|
214
|
+
}
|
|
215
|
+
if (bumped.note) ctx.note(`${field}["${UIC}"]: ${bumped.note}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const has = (/** @type {string} */ name) =>
|
|
219
|
+
FIELDS.some((f) => pkg[f]?.[name] != null);
|
|
220
|
+
|
|
221
|
+
// The bin moved out of ui-common into its own package. It is a dev-time
|
|
222
|
+
// tool, pinned exactly: it upgrades to and reads the ui-common of its own
|
|
223
|
+
// version.
|
|
224
|
+
const cliField = FIELDS.find((f) => pkg[f]?.[CLI_PACKAGE] != null);
|
|
225
|
+
if (!cliField) {
|
|
226
|
+
addDependency(pkg, "devDependencies", CLI_PACKAGE, ctx.to);
|
|
227
|
+
ctx.note(
|
|
228
|
+
`added ${CLI_PACKAGE} ${ctx.to} to devDependencies: the \`ui-common\` bin ships in its own package since 0.2, released at the same version as ${UIC}.`,
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
const spec = pkg[cliField][CLI_PACKAGE];
|
|
232
|
+
const bumped = bumpSpec(spec, ctx.to);
|
|
233
|
+
if (!bumped) {
|
|
234
|
+
ctx.note(
|
|
235
|
+
`${cliField}["${CLI_PACKAGE}"] is "${spec}"; not a version, so it was left alone.`,
|
|
236
|
+
);
|
|
237
|
+
} else if (spec !== ctx.to) {
|
|
238
|
+
pkg[cliField][CLI_PACKAGE] = ctx.to;
|
|
239
|
+
ctx.note(`${cliField}["${CLI_PACKAGE}"]: "${spec}" → "${ctx.to}".`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// A library that takes ui-common as a peer takes StyleX as a peer too;
|
|
243
|
+
// an application depends on it.
|
|
244
|
+
const library = fields.includes("peerDependencies");
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Add `name` where this project needs it, and return the fields it went
|
|
248
|
+
* into. A library needs it as a peer and, when it develops against
|
|
249
|
+
* ui-common, as a devDependency: each is checked on its own, since one does
|
|
250
|
+
* not stand in for the other. An application needs it anywhere.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} name
|
|
253
|
+
* @param {string} range
|
|
254
|
+
*/
|
|
255
|
+
const ensure = (name, range) => {
|
|
256
|
+
/** @type {string[]} */
|
|
257
|
+
const wanted = library
|
|
258
|
+
? [
|
|
259
|
+
...(pkg.peerDependencies?.[name] == null && pkg.dependencies?.[name] == null
|
|
260
|
+
? ["peerDependencies"]
|
|
261
|
+
: []),
|
|
262
|
+
...(fields.includes("devDependencies") && pkg.devDependencies?.[name] == null
|
|
263
|
+
? ["devDependencies"]
|
|
264
|
+
: []),
|
|
265
|
+
]
|
|
266
|
+
: has(name)
|
|
267
|
+
? []
|
|
268
|
+
: [fields.includes("dependencies") ? "dependencies" : fields[0]];
|
|
269
|
+
for (const field of wanted) addDependency(pkg, field, name, range);
|
|
270
|
+
return wanted;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const stylex = stylexPeer();
|
|
274
|
+
const stylexAdded = ensure(stylex.name, stylex.range);
|
|
275
|
+
if (stylexAdded.length > 0)
|
|
276
|
+
ctx.note(`added ${stylex.name} ${stylex.range} to ${stylexAdded.join(" and ")}.`);
|
|
277
|
+
|
|
278
|
+
// 0.2 needs React 19.2: a library that still accepts older React in its
|
|
279
|
+
// peers would install beside such an app and break there.
|
|
280
|
+
const reactPeers =
|
|
281
|
+
uiCommonPackageJson(targetUiCommonRoot(ctx.projectDir)).peerDependencies ?? {};
|
|
282
|
+
for (const name of ["react", "react-dom"]) {
|
|
283
|
+
const floor = coerce(reactPeers[name] ?? "") ?? REACT_FLOOR;
|
|
284
|
+
const spec = pkg.peerDependencies?.[name];
|
|
285
|
+
if (library && typeof spec === "string") {
|
|
286
|
+
const next = narrowReactRange(spec, floor);
|
|
287
|
+
if (next == null) {
|
|
288
|
+
ctx.note(
|
|
289
|
+
`peerDependencies["${name}"] is "${spec}", which admits no React ${floor} or later; ${UIC} 0.2 needs it, so move the range up by hand.`,
|
|
290
|
+
);
|
|
291
|
+
} else if (next !== spec) {
|
|
292
|
+
pkg.peerDependencies[name] = next;
|
|
293
|
+
ctx.note(
|
|
294
|
+
`peerDependencies["${name}"]: "${spec}" → "${next}": ${UIC} 0.2 needs React ${floor} or later.`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const own = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
|
|
299
|
+
if (typeof own === "string" && narrowReactRange(own, floor) !== own) {
|
|
300
|
+
ctx.note(
|
|
301
|
+
`${pkg.dependencies?.[name] ? "dependencies" : "devDependencies"}["${name}"] is "${own}": ${UIC} 0.2 needs React ${floor} or later; upgrade React too.`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// pnpm 11 refuses to install until the Astryx postinstalls are decided.
|
|
307
|
+
if (ctx.projectDir && ctx.editFile) {
|
|
308
|
+
const { manager, workspaceYaml } = detectPackageManager(ctx.projectDir, pkg);
|
|
309
|
+
if (manager === "pnpm" && workspaceYaml) {
|
|
310
|
+
ctx.editFile(workspaceYaml, (current) => {
|
|
311
|
+
const edit = applyAllowBuilds(current, DECLINED_BUILDS);
|
|
312
|
+
for (const note of edit.notes) ctx.note(note);
|
|
313
|
+
return edit.yaml;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Packages the map says a moved component needs (the lab Drawer).
|
|
319
|
+
for (const [lab, range] of ctx.flags.packages) {
|
|
320
|
+
const added = ensure(lab, range);
|
|
321
|
+
if (added.length === 0) continue;
|
|
322
|
+
const field = added.join(" and ");
|
|
323
|
+
ctx.note(
|
|
324
|
+
lab === LAB_PACKAGE
|
|
325
|
+
? `added ${lab} ${range} to ${field}: a Drawer moved to ${UIC}/lab, and ui-common pins the lab canary exactly.`
|
|
326
|
+
: `added ${lab} ${range} to ${field}.`,
|
|
327
|
+
);
|
|
328
|
+
if (lab === LAB_PACKAGE) addLabOverride(pkg, ctx);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const out = `${JSON.stringify(pkg, null, indent)}${text.endsWith("\n") ? "\n" : ""}`;
|
|
332
|
+
return out === text ? undefined : out;
|
|
333
|
+
}
|