@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
package/cli/rewrite.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite Astryx CLI output into ui-common terms.
|
|
3
|
+
*
|
|
4
|
+
* Every replacement is a plain substring swap between two strings that contain
|
|
5
|
+
* no quote, backslash or control character, so it is safe inside JSON string
|
|
6
|
+
* literals: `--json` output stays valid JSON.
|
|
7
|
+
*/
|
|
8
|
+
import { forkedExports, hiddenExports } from "./paths.mjs";
|
|
9
|
+
|
|
10
|
+
/** Astryx CLI subcommands. Used to tell `astryx component` from `astryx-base`. */
|
|
11
|
+
export const ASTRYX_COMMANDS = [
|
|
12
|
+
"init",
|
|
13
|
+
"component",
|
|
14
|
+
"docs",
|
|
15
|
+
"blog",
|
|
16
|
+
"swizzle",
|
|
17
|
+
"gap-report",
|
|
18
|
+
"template",
|
|
19
|
+
"layout",
|
|
20
|
+
"upgrade",
|
|
21
|
+
"theme",
|
|
22
|
+
"integration",
|
|
23
|
+
"hook",
|
|
24
|
+
"discover",
|
|
25
|
+
"search",
|
|
26
|
+
"build",
|
|
27
|
+
"doctor",
|
|
28
|
+
"manifest",
|
|
29
|
+
"help",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const SPECIFIERS = [
|
|
33
|
+
// Order matters: the bare package names last, so they never eat a subpath.
|
|
34
|
+
[/@astryxdesign\/core(?=[/"'`\s),;:]|$)/g, "@lablup/ui-common"],
|
|
35
|
+
[/@astryxdesign\/lab(?=[/"'`\s),;:]|$)/g, "@lablup/ui-common/lab"],
|
|
36
|
+
[
|
|
37
|
+
/@astryxdesign\/theme-neutral(?=[/"'`\s),;:]|$)/g,
|
|
38
|
+
"@lablup/ui-common/theme/neutral",
|
|
39
|
+
],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `@astryxdesign/core/Button` -> `@lablup/ui-common/Button`, and the lab and
|
|
44
|
+
* neutral-theme packages onto their ui-common mirrors.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} text
|
|
47
|
+
*/
|
|
48
|
+
export function rewriteSpecifiers(text) {
|
|
49
|
+
let out = text;
|
|
50
|
+
for (const [pattern, replacement] of SPECIFIERS) {
|
|
51
|
+
out = out.replace(
|
|
52
|
+
/** @type {RegExp} */ (pattern),
|
|
53
|
+
/** @type {string} */ (replacement),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `pnpm exec astryx component X` -> `<invocation> component X`, and a bare
|
|
61
|
+
* `astryx component` in prose -> `ui-common component`. `astryx-base`,
|
|
62
|
+
* `astryx.css` and `.astryx-badge` are left alone: only the bin followed by a
|
|
63
|
+
* real subcommand is rewritten.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} text
|
|
66
|
+
* @param {string} invocation how the project runs the ui-common bin
|
|
67
|
+
*/
|
|
68
|
+
export function rewriteCommands(text, invocation = "ui-common") {
|
|
69
|
+
const commands = ASTRYX_COMMANDS.join("|");
|
|
70
|
+
return (
|
|
71
|
+
text
|
|
72
|
+
// A one-off `npx @astryxdesign/cli` invocation, or an installed-bin
|
|
73
|
+
// invocation through a package manager.
|
|
74
|
+
.replace(
|
|
75
|
+
/\b(?:npx|pnpm dlx|yarn dlx|bunx) @astryxdesign\/cli(?=[\s`'"]|$)/g,
|
|
76
|
+
invocation,
|
|
77
|
+
)
|
|
78
|
+
.replace(
|
|
79
|
+
/\b(?:npx|pnpm exec|pnpm|yarn|bunx|bun x|npm exec)\s+astryx(?=[\s`'"]|$)/g,
|
|
80
|
+
invocation,
|
|
81
|
+
)
|
|
82
|
+
.replace(
|
|
83
|
+
new RegExp(String.raw`(?<![\w/@.\-])astryx(?= (?:${commands})\b)`, "g"),
|
|
84
|
+
"ui-common",
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Lines telling the reader that a name they are looking at is one ui-common
|
|
91
|
+
* hides, and what to use instead; or one ui-common exports from its own copy
|
|
92
|
+
* of the Astryx component, and what that copy changes.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} text rewritten output
|
|
95
|
+
* @param {string[]} args the command line, so `component Dialog` is caught
|
|
96
|
+
* even when the output never spells the name
|
|
97
|
+
*/
|
|
98
|
+
export function exclusionNotes(text, args = []) {
|
|
99
|
+
const notes = [];
|
|
100
|
+
const mentions = (name) =>
|
|
101
|
+
new RegExp(String.raw`(?<![\w.-])${name}(?![\w.-])`).test(text) ||
|
|
102
|
+
args.includes(name);
|
|
103
|
+
for (const entry of hiddenExports()) {
|
|
104
|
+
// Asking about the replacement itself needs no pointer to it.
|
|
105
|
+
if (!entry.replacedBy || args.includes(entry.replacedBy)) continue;
|
|
106
|
+
const name = entry.name;
|
|
107
|
+
if (mentions(name)) {
|
|
108
|
+
notes.push(
|
|
109
|
+
`Note: @lablup/ui-common does not export ${name}. Use ${entry.replacedBy} ` +
|
|
110
|
+
`(@lablup/ui-common/${entry.replacedBy}), not ${name}. ${entry.reason}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const fork of forkedExports()) {
|
|
115
|
+
if (fork.names.some(mentions)) {
|
|
116
|
+
notes.push(
|
|
117
|
+
`Note: ${fork.name} (${fork.from}) comes from ui-common: its own copy of ` +
|
|
118
|
+
`Astryx's, same API and import path. ${fork.reason}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return notes;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The full output rewrite: specifiers, then command invocations.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} text
|
|
129
|
+
* @param {string} [invocation]
|
|
130
|
+
*/
|
|
131
|
+
export function rewriteOutput(text, invocation) {
|
|
132
|
+
return rewriteCommands(rewriteSpecifiers(text), invocation);
|
|
133
|
+
}
|
package/cli/semver.mjs
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The little semver the upgrade registry needs: parse, compare with
|
|
3
|
+
* prerelease precedence, and coerce the loose forms a person types
|
|
4
|
+
* (`0.1`, `v0.1.0-alpha.19`) or a package.json holds (`^0.1.0-alpha.7`).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const SEMVER =
|
|
8
|
+
/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} input
|
|
12
|
+
* @returns {{major: number, minor: number, patch: number, pre: string[]} | null}
|
|
13
|
+
*/
|
|
14
|
+
export function parse(input) {
|
|
15
|
+
const match = SEMVER.exec(String(input).trim());
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
return {
|
|
18
|
+
major: Number(match[1]),
|
|
19
|
+
minor: Number(match[2] ?? 0),
|
|
20
|
+
patch: Number(match[3] ?? 0),
|
|
21
|
+
pre: match[4] ? match[4].split(".") : [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @param {{major: number, minor: number, patch: number, pre: string[]}} v */
|
|
26
|
+
export function format(v) {
|
|
27
|
+
return `${v.major}.${v.minor}.${v.patch}${v.pre.length ? `-${v.pre.join(".")}` : ""}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The first version a dependency spec admits, for `^0.1.0-alpha.7`,
|
|
32
|
+
* `~0.1.0`, `0.1.0-alpha.19`, `>=0.1.0-alpha.0 <0.2.0`. Null for
|
|
33
|
+
* `workspace:*`, tags and URLs.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} spec
|
|
36
|
+
*/
|
|
37
|
+
export function coerce(spec) {
|
|
38
|
+
const match = /(?:^|[\s^~>=<v])(\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?)/.exec(
|
|
39
|
+
` ${String(spec).trim()}`,
|
|
40
|
+
);
|
|
41
|
+
if (!match) return null;
|
|
42
|
+
const parsed = parse(match[1]);
|
|
43
|
+
return parsed ? format(parsed) : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @param {string} a @param {string} b */
|
|
47
|
+
function compareIdentifiers(a, b) {
|
|
48
|
+
const na = /^\d+$/.test(a);
|
|
49
|
+
const nb = /^\d+$/.test(b);
|
|
50
|
+
if (na && nb) return Number(a) - Number(b);
|
|
51
|
+
if (na) return -1;
|
|
52
|
+
if (nb) return 1;
|
|
53
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Semver precedence. A version with a prerelease sorts before the same
|
|
58
|
+
* version without one.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} a
|
|
61
|
+
* @param {string} b
|
|
62
|
+
*/
|
|
63
|
+
export function compare(a, b) {
|
|
64
|
+
const va = parse(a);
|
|
65
|
+
const vb = parse(b);
|
|
66
|
+
if (!va || !vb) throw new Error(`Not a version: ${!va ? a : b}`);
|
|
67
|
+
for (const key of /** @type {const} */ (["major", "minor", "patch"])) {
|
|
68
|
+
if (va[key] !== vb[key]) return va[key] - vb[key];
|
|
69
|
+
}
|
|
70
|
+
if (va.pre.length === 0 || vb.pre.length === 0) {
|
|
71
|
+
return vb.pre.length - va.pre.length;
|
|
72
|
+
}
|
|
73
|
+
for (let i = 0; i < Math.max(va.pre.length, vb.pre.length); i++) {
|
|
74
|
+
const x = va.pre[i];
|
|
75
|
+
const y = vb.pre[i];
|
|
76
|
+
if (x === undefined) return -1;
|
|
77
|
+
if (y === undefined) return 1;
|
|
78
|
+
const c = compareIdentifiers(x, y);
|
|
79
|
+
if (c !== 0) return c;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The version after `version` for a release that carries a breaking change:
|
|
86
|
+
* the next prerelease number while in prerelease, else the next minor (pre-1.0
|
|
87
|
+
* a breaking change bumps the minor).
|
|
88
|
+
*
|
|
89
|
+
* @param {string} version
|
|
90
|
+
*/
|
|
91
|
+
export function nextBreaking(version) {
|
|
92
|
+
const v = parse(version);
|
|
93
|
+
if (!v) throw new Error(`Not a version: ${version}`);
|
|
94
|
+
if (v.pre.length > 0) {
|
|
95
|
+
const last = v.pre[v.pre.length - 1];
|
|
96
|
+
const pre = /^\d+$/.test(last)
|
|
97
|
+
? [...v.pre.slice(0, -1), String(Number(last) + 1)]
|
|
98
|
+
: [...v.pre, "1"];
|
|
99
|
+
return format({ ...v, pre });
|
|
100
|
+
}
|
|
101
|
+
if (v.major === 0) return format({ major: 0, minor: v.minor + 1, patch: 0, pre: [] });
|
|
102
|
+
return format({ major: v.major + 1, minor: 0, patch: 0, pre: [] });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @typedef {{version: string, inclusive: boolean, text?: string}} Bound
|
|
107
|
+
* `text` is the comparator as written, kept when the bound survives.
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A partial version (`19`, `19.x`, `19.2.*`) as its first admitted version
|
|
112
|
+
* and the version its x-range stops before. Null for anything else.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} input
|
|
115
|
+
*/
|
|
116
|
+
function partial(input) {
|
|
117
|
+
const m =
|
|
118
|
+
/^v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
|
|
119
|
+
input,
|
|
120
|
+
);
|
|
121
|
+
if (!m) return null;
|
|
122
|
+
const wild = (/** @type {string | undefined} */ s) => s == null || /^[xX*]$/.test(s);
|
|
123
|
+
if (wild(m[1])) return { min: null, end: null, full: false, parts: 0 };
|
|
124
|
+
const major = Number(m[1]);
|
|
125
|
+
if (wild(m[2]))
|
|
126
|
+
return { min: `${major}.0.0`, end: `${major + 1}.0.0`, full: false, parts: 1 };
|
|
127
|
+
const minor = Number(m[2]);
|
|
128
|
+
if (wild(m[3]))
|
|
129
|
+
return {
|
|
130
|
+
min: `${major}.${minor}.0`,
|
|
131
|
+
end: `${major}.${minor + 1}.0`,
|
|
132
|
+
full: false,
|
|
133
|
+
parts: 2,
|
|
134
|
+
};
|
|
135
|
+
const version = `${major}.${minor}.${Number(m[3])}${m[4] ? `-${m[4]}` : ""}`;
|
|
136
|
+
return { min: version, end: null, full: true, parts: 3 };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* One `||` alternative as a lower and an upper bound; null when it is not a
|
|
141
|
+
* range this reads (a tag, a URL).
|
|
142
|
+
*
|
|
143
|
+
* @param {string} alternative
|
|
144
|
+
* @returns {{lower: Bound | null, upper: Bound | null, hyphen?: string} | null}
|
|
145
|
+
*/
|
|
146
|
+
function bounds(alternative) {
|
|
147
|
+
const text = alternative.trim();
|
|
148
|
+
if (text === "") return { lower: null, upper: null };
|
|
149
|
+
const hyphen = /^(\S+)\s+-\s+(\S+)$/.exec(text);
|
|
150
|
+
if (hyphen) {
|
|
151
|
+
const from = partial(hyphen[1]);
|
|
152
|
+
const to = partial(hyphen[2]);
|
|
153
|
+
if (!from || !to) return null;
|
|
154
|
+
return {
|
|
155
|
+
lower: from.min ? { version: from.min, inclusive: true } : null,
|
|
156
|
+
upper: to.full
|
|
157
|
+
? { version: /** @type {string} */ (to.min), inclusive: true }
|
|
158
|
+
: to.end
|
|
159
|
+
? { version: to.end, inclusive: false }
|
|
160
|
+
: null,
|
|
161
|
+
hyphen: hyphen[2],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** @type {Bound | null} */
|
|
165
|
+
let lower = null;
|
|
166
|
+
/** @type {Bound | null} */
|
|
167
|
+
let upper = null;
|
|
168
|
+
/** @param {Bound} b */
|
|
169
|
+
const raise = (b) => {
|
|
170
|
+
if (!lower || compare(b.version, lower.version) > 0) lower = b;
|
|
171
|
+
};
|
|
172
|
+
/** @param {Bound} b */
|
|
173
|
+
const cap = (b) => {
|
|
174
|
+
const c = upper ? compare(b.version, upper.version) : -1;
|
|
175
|
+
if (c < 0 || (c === 0 && !b.inclusive)) upper = b;
|
|
176
|
+
};
|
|
177
|
+
for (const token of text.replace(/(<=|>=|[<>=^~])\s+/g, "$1").split(/\s+/)) {
|
|
178
|
+
const m = /^(<=|>=|<|>|=|\^|~)?(.*)$/.exec(token);
|
|
179
|
+
const op = m?.[1] ?? "";
|
|
180
|
+
const v = partial(m?.[2] ?? "");
|
|
181
|
+
if (!v) return null;
|
|
182
|
+
if (!v.min) {
|
|
183
|
+
// `*`, `x`, `>=*`: no bound; `<*` admits nothing.
|
|
184
|
+
if (op === "<" || op === ">")
|
|
185
|
+
return { lower: null, upper: { version: "0.0.0", inclusive: false } };
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const at = /** @type {string} */ (v.min);
|
|
189
|
+
if (op === "^" || op === "~") {
|
|
190
|
+
const p = /** @type {{major: number, minor: number, patch: number}} */ (
|
|
191
|
+
parse(at)
|
|
192
|
+
);
|
|
193
|
+
raise({ version: at, inclusive: true });
|
|
194
|
+
const end =
|
|
195
|
+
op === "~"
|
|
196
|
+
? v.parts >= 2
|
|
197
|
+
? `${p.major}.${p.minor + 1}.0`
|
|
198
|
+
: `${p.major + 1}.0.0`
|
|
199
|
+
: p.major > 0 || v.parts === 1
|
|
200
|
+
? `${p.major + 1}.0.0`
|
|
201
|
+
: p.minor > 0 || v.parts === 2
|
|
202
|
+
? `0.${p.minor + 1}.0`
|
|
203
|
+
: `0.0.${p.patch + 1}`;
|
|
204
|
+
cap({ version: end, inclusive: false });
|
|
205
|
+
} else if (op === ">=") raise({ version: at, inclusive: true, text: token });
|
|
206
|
+
else if (op === ">")
|
|
207
|
+
raise(
|
|
208
|
+
v.full
|
|
209
|
+
? { version: at, inclusive: false, text: token }
|
|
210
|
+
: { version: /** @type {string} */ (v.end), inclusive: true, text: token },
|
|
211
|
+
);
|
|
212
|
+
else if (op === "<") cap({ version: at, inclusive: false, text: token });
|
|
213
|
+
else if (op === "<=")
|
|
214
|
+
cap(
|
|
215
|
+
v.full
|
|
216
|
+
? { version: at, inclusive: true, text: token }
|
|
217
|
+
: { version: /** @type {string} */ (v.end), inclusive: false, text: token },
|
|
218
|
+
);
|
|
219
|
+
else {
|
|
220
|
+
raise({ version: at, inclusive: true });
|
|
221
|
+
cap(
|
|
222
|
+
v.full
|
|
223
|
+
? { version: at, inclusive: true }
|
|
224
|
+
: { version: /** @type {string} */ (v.end), inclusive: false },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { lower, upper };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* `range` narrowed to the versions at or above `floor`, alternative by
|
|
233
|
+
* alternative: `>=18 <21 || ^22` at 19.2.0 is `>=19.2.0 <21 || ^22`. An
|
|
234
|
+
* alternative already above the floor keeps its text; one wholly below it is
|
|
235
|
+
* dropped. Null when nothing is left; `range` itself when it is not a range
|
|
236
|
+
* this reads.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} range
|
|
239
|
+
* @param {string} floor a full version
|
|
240
|
+
* @returns {string | null}
|
|
241
|
+
*/
|
|
242
|
+
export function narrowToFloor(range, floor) {
|
|
243
|
+
const f = parse(floor);
|
|
244
|
+
if (!f) throw new Error(`Not a version: ${floor}`);
|
|
245
|
+
const alternatives = range.split("||").map((a) => a.trim());
|
|
246
|
+
const parsed = alternatives.map(bounds);
|
|
247
|
+
if (parsed.some((b) => b == null)) return range;
|
|
248
|
+
/** @type {string[]} */
|
|
249
|
+
const kept = [];
|
|
250
|
+
for (const [i, b] of /** @type {NonNullable<ReturnType<typeof bounds>>[]} */ (
|
|
251
|
+
parsed
|
|
252
|
+
).entries()) {
|
|
253
|
+
const { lower, upper } = b;
|
|
254
|
+
if (lower && compare(lower.version, floor) >= 0) {
|
|
255
|
+
kept.push(alternatives[i]);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (upper) {
|
|
259
|
+
const c = compare(upper.version, floor);
|
|
260
|
+
if (c < 0 || (c === 0 && !upper.inclusive)) continue;
|
|
261
|
+
}
|
|
262
|
+
let next;
|
|
263
|
+
if (!upper) next = `>=${floor}`;
|
|
264
|
+
else if (b.hyphen != null) next = `${floor} - ${b.hyphen}`;
|
|
265
|
+
else if (!upper.inclusive && upper.version === `${f.major + 1}.0.0` && f.major > 0)
|
|
266
|
+
next = `^${floor}`;
|
|
267
|
+
else if (upper.inclusive && upper.version === floor) next = floor;
|
|
268
|
+
else
|
|
269
|
+
next = `>=${floor} ${upper.text ?? `${upper.inclusive ? "<=" : "<"}${upper.version}`}`;
|
|
270
|
+
kept.push(next);
|
|
271
|
+
}
|
|
272
|
+
const unique = [...new Set(kept)];
|
|
273
|
+
if (unique.length === 0) return null;
|
|
274
|
+
return unique.length === alternatives.length &&
|
|
275
|
+
unique.every((a, i) => a === alternatives[i])
|
|
276
|
+
? range
|
|
277
|
+
: unique.join(" || ");
|
|
278
|
+
}
|
package/cli/shadow.mjs
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Let Astryx's lookup commands find @astryxdesign/core in a project that only
|
|
3
|
+
* depends on @lablup/ui-common.
|
|
4
|
+
*
|
|
5
|
+
* `astryx component`, `search`, `docs`, `build` and friends locate core by
|
|
6
|
+
* walking up from the working directory for `node_modules/@astryxdesign/core`.
|
|
7
|
+
* Under pnpm's isolated layout a ui-common consumer has no such directory:
|
|
8
|
+
* core is ui-common's dependency, not the project's. So, for read-only
|
|
9
|
+
* commands only, the CLI runs in a throwaway "shadow" of the project: its
|
|
10
|
+
* package.json, config and lockfile, and a node_modules made of symlinks to
|
|
11
|
+
* the project's own packages plus the Astryx packages ui-common pins. The
|
|
12
|
+
* project's integrations (ui-common's included) load exactly as they would in
|
|
13
|
+
* place. Paths in the output are mapped back to the project.
|
|
14
|
+
*
|
|
15
|
+
* Commands that write files (`init`, `swizzle`, `template <name> <path>`,
|
|
16
|
+
* `theme add`, …) never run in a shadow.
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
mkdtempSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
rmSync,
|
|
24
|
+
symlinkSync,
|
|
25
|
+
} from "node:fs";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
|
|
29
|
+
import { dependencyDir, findProjectDir } from "./paths.mjs";
|
|
30
|
+
|
|
31
|
+
/** Astryx commands that only read. */
|
|
32
|
+
const READ_ONLY = new Set([
|
|
33
|
+
"component",
|
|
34
|
+
"search",
|
|
35
|
+
"docs",
|
|
36
|
+
"build",
|
|
37
|
+
"hook",
|
|
38
|
+
"discover",
|
|
39
|
+
"blog",
|
|
40
|
+
"layout",
|
|
41
|
+
"template",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const ASTRYX_PACKAGES = [
|
|
45
|
+
"@astryxdesign/core",
|
|
46
|
+
"@astryxdesign/theme-neutral",
|
|
47
|
+
"@astryxdesign/lab",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const PROJECT_FILES = [
|
|
51
|
+
"package.json",
|
|
52
|
+
"astryx.config.ts",
|
|
53
|
+
"astryx.config.mts",
|
|
54
|
+
"astryx.config.js",
|
|
55
|
+
"astryx.config.mjs",
|
|
56
|
+
"astryx.config.cjs",
|
|
57
|
+
"pnpm-lock.yaml",
|
|
58
|
+
"yarn.lock",
|
|
59
|
+
"package-lock.json",
|
|
60
|
+
"bun.lock",
|
|
61
|
+
"AGENTS.md",
|
|
62
|
+
"CLAUDE.md",
|
|
63
|
+
".claude",
|
|
64
|
+
".cursorrules",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the Astryx CLI finds core from `cwd` (its own findCoreDir walk:
|
|
69
|
+
* five levels of `packages/core` or `node_modules/@astryxdesign/core`).
|
|
70
|
+
*
|
|
71
|
+
* @param {string} cwd
|
|
72
|
+
*/
|
|
73
|
+
export function coreReachableFrom(cwd) {
|
|
74
|
+
let dir = cwd;
|
|
75
|
+
for (let i = 0; i < 5; i++) {
|
|
76
|
+
if (existsSync(join(dir, "packages", "core"))) return true;
|
|
77
|
+
if (existsSync(join(dir, "node_modules", "@astryxdesign", "core"))) return true;
|
|
78
|
+
const parent = dirname(dir);
|
|
79
|
+
if (parent === dir) break;
|
|
80
|
+
dir = parent;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether this command should run in a shadow of the project.
|
|
87
|
+
*
|
|
88
|
+
* @param {string[]} args
|
|
89
|
+
* @param {string} cwd
|
|
90
|
+
*/
|
|
91
|
+
export function needsShadow(args, cwd) {
|
|
92
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
93
|
+
const command = positionals[0];
|
|
94
|
+
if (!command || !READ_ONLY.has(command)) return false;
|
|
95
|
+
// `template <name> <path>` and `--cdn` write files.
|
|
96
|
+
if (
|
|
97
|
+
command === "template" &&
|
|
98
|
+
(positionals.length > 2 || args.some((a) => a.startsWith("--cdn")))
|
|
99
|
+
) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (!dependencyDir("@astryxdesign/core")) return false;
|
|
103
|
+
return !coreReachableFrom(cwd);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @param {string} target @param {string} link */
|
|
107
|
+
function link(target, link) {
|
|
108
|
+
symlinkSync(target, link, process.platform === "win32" ? "junction" : undefined);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} cwd
|
|
113
|
+
* @returns {{dir: string, projectDir: string, cleanup: () => void}}
|
|
114
|
+
*/
|
|
115
|
+
export function createShadow(cwd) {
|
|
116
|
+
const projectDir = findProjectDir(cwd) ?? cwd;
|
|
117
|
+
const dir = mkdtempSync(join(tmpdir(), "ui-common-astryx-"));
|
|
118
|
+
for (const file of PROJECT_FILES) {
|
|
119
|
+
const source = join(projectDir, file);
|
|
120
|
+
if (existsSync(source)) link(source, join(dir, file));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const modules = join(dir, "node_modules");
|
|
124
|
+
mkdirSync(modules);
|
|
125
|
+
// The project's installed packages, from the nearest node_modules.
|
|
126
|
+
let search = projectDir;
|
|
127
|
+
for (;;) {
|
|
128
|
+
const nm = join(search, "node_modules");
|
|
129
|
+
if (existsSync(nm)) {
|
|
130
|
+
for (const entry of readdirSync(nm)) {
|
|
131
|
+
if (entry.startsWith(".")) continue;
|
|
132
|
+
if (entry.startsWith("@")) {
|
|
133
|
+
mkdirSync(join(modules, entry), { recursive: true });
|
|
134
|
+
for (const scoped of readdirSync(join(nm, entry))) {
|
|
135
|
+
link(join(nm, entry, scoped), join(modules, entry, scoped));
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
link(join(nm, entry), join(modules, entry));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
const parent = dirname(search);
|
|
144
|
+
if (parent === search) break;
|
|
145
|
+
search = parent;
|
|
146
|
+
}
|
|
147
|
+
// The Astryx packages ui-common pins, where the project has none.
|
|
148
|
+
for (const name of ASTRYX_PACKAGES) {
|
|
149
|
+
const target = join(modules, ...name.split("/"));
|
|
150
|
+
const source = dependencyDir(name);
|
|
151
|
+
if (!source || existsSync(target)) continue;
|
|
152
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
153
|
+
link(source, target);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
dir,
|
|
158
|
+
projectDir,
|
|
159
|
+
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
|
160
|
+
};
|
|
161
|
+
}
|