@hirobius/design-system 0.21.0 → 0.22.0
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/AGENTS.md +2 -0
- package/CHANGELOG.md +969 -0
- package/CONSUMING.md +23 -9
- package/MIGRATIONS.md +603 -0
- package/README.md +2 -0
- package/UPGRADING.md +222 -0
- package/codemods/hds-prefix.mjs +28 -13
- package/codemods/jsx-fold.mjs +28 -13
- package/codemods/lib/atomic-write.mjs +28 -0
- package/codemods/lib/installed-version.mjs +354 -0
- package/codemods/lib/project.mjs +395 -0
- package/codemods/lib/record.mjs +110 -0
- package/codemods/lib/scan.mjs +323 -0
- package/codemods/lib/semver.mjs +57 -0
- package/codemods/not-found-pattern.mjs +3 -3
- package/codemods/patterns-subpath.mjs +28 -12
- package/codemods/registry.mjs +67 -0
- package/codemods/tile-grid.mjs +3 -3
- package/codemods/upgrade.mjs +909 -0
- package/dist/hirobius-ui.js +1 -1
- package/dist/manifest.js +3 -3
- package/dist/styles.css +1 -1
- package/dist/tokens.css +1 -1
- package/dist/types/src/app/components/alert-dialog.d.ts +1 -1
- package/dist/types/src/app/components/alert.d.ts +1 -1
- package/dist/types/src/app/components/asset-img.d.ts +1 -1
- package/dist/types/src/app/components/avatar-group.d.ts +1 -1
- package/dist/types/src/app/components/avatar.d.ts +1 -1
- package/dist/types/src/app/components/badge.d.ts +1 -1
- package/dist/types/src/app/components/blockquote.d.ts +1 -1
- package/dist/types/src/app/components/breadcrumb.d.ts +1 -1
- package/dist/types/src/app/components/button.d.ts +1 -1
- package/dist/types/src/app/components/callout.d.ts +1 -1
- package/dist/types/src/app/components/card.d.ts +1 -1
- package/dist/types/src/app/components/checkbox.d.ts +1 -1
- package/dist/types/src/app/components/code-block.d.ts +1 -1
- package/dist/types/src/app/components/combobox.d.ts +1 -1
- package/dist/types/src/app/components/data-table-section.d.ts +1 -1
- package/dist/types/src/app/components/destructive-section.d.ts +1 -1
- package/dist/types/src/app/components/dialog.d.ts +1 -1
- package/dist/types/src/app/components/disclosure.d.ts +1 -1
- package/dist/types/src/app/components/divider.d.ts +1 -1
- package/dist/types/src/app/components/empty-state.d.ts +1 -1
- package/dist/types/src/app/components/field.d.ts +1 -1
- package/dist/types/src/app/components/form-actions.d.ts +1 -1
- package/dist/types/src/app/components/form.d.ts +1 -1
- package/dist/types/src/app/components/hds-tooltip.d.ts +1 -1
- package/dist/types/src/app/components/icon.d.ts +1 -1
- package/dist/types/src/app/components/inline-link.d.ts +1 -1
- package/dist/types/src/app/components/input.d.ts +1 -1
- package/dist/types/src/app/components/kbd.d.ts +1 -1
- package/dist/types/src/app/components/menu.d.ts +1 -1
- package/dist/types/src/app/components/metadata-list.d.ts +1 -1
- package/dist/types/src/app/components/metric-tiles.d.ts +1 -1
- package/dist/types/src/app/components/pagination.d.ts +1 -1
- package/dist/types/src/app/components/popover.d.ts +1 -1
- package/dist/types/src/app/components/progress.d.ts +1 -1
- package/dist/types/src/app/components/radio.d.ts +1 -1
- package/dist/types/src/app/components/select.d.ts +1 -1
- package/dist/types/src/app/components/skeleton.d.ts +1 -1
- package/dist/types/src/app/components/slider.d.ts +1 -1
- package/dist/types/src/app/components/spinner.d.ts +1 -1
- package/dist/types/src/app/components/stat.d.ts +1 -1
- package/dist/types/src/app/components/status-list-item.d.ts +1 -1
- package/dist/types/src/app/components/status-tile.d.ts +1 -1
- package/dist/types/src/app/components/table.d.ts +1 -1
- package/dist/types/src/app/components/tabs.d.ts +1 -1
- package/dist/types/src/app/components/tag.d.ts +1 -1
- package/dist/types/src/app/components/text.d.ts +1 -1
- package/dist/types/src/app/components/textarea.d.ts +1 -1
- package/dist/types/src/app/components/timestamp.d.ts +1 -1
- package/dist/types/src/app/components/toast.d.ts +1 -1
- package/dist/types/src/app/components/toggle.d.ts +1 -1
- package/dist/types/src/index.d.ts +79 -32
- package/hirobius.tokens.json +86 -39
- package/mcp/catalog.mjs +10 -7
- package/mcp/core-set.mjs +66 -0
- package/mcp/guide.mjs +9 -52
- package/package.json +23 -7
- package/public/hds-manifest.json +109 -109
- package/src/app/data/component-api.json +54 -54
- package/upgrade/index.json +208 -0
- package/upgrade/releases/0.17.0.json +962 -0
- package/upgrade/releases/0.18.0.json +8 -0
- package/upgrade/releases/0.19.0.json +233 -0
- package/upgrade/releases/0.19.1.json +8 -0
- package/upgrade/releases/0.20.0.json +5108 -0
- package/upgrade/releases/0.21.0.json +538 -0
- package/upgrade/releases/0.22.0.json +94 -0
- package/upgrade/schema.json +989 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* project — what a consumer repo looks like to the upgrade command (hds#452):
|
|
3
|
+
* its package manager, its lockfile, every importer that declares
|
|
4
|
+
* @hirobius/design-system, and the version each one has installed.
|
|
5
|
+
*
|
|
6
|
+
* Node builtins only. An importer is the root package or a workspace package
|
|
7
|
+
* (pnpm-workspace.yaml, or package.json `workspaces` for npm, yarn and bun).
|
|
8
|
+
* The installed version comes from, in order: the lockfile, the importer's
|
|
9
|
+
* node_modules copy, `--from`. A lockfile at git HEAD or at the merge-base with
|
|
10
|
+
* the default branch that holds an older version wins (the dependency was
|
|
11
|
+
* bumped by hand and not yet committed or merged); when the lockfile already
|
|
12
|
+
* says the target, the lockfile's own first-parent history is read too. An
|
|
13
|
+
* explicit `--from` skips git. A version read from history below the floor
|
|
14
|
+
* becomes the floor, with `belowFloor` set: only a version actually installed
|
|
15
|
+
* is refused for it.
|
|
16
|
+
*/
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
20
|
+
import { HDS_PACKAGE, lockfileKind, pickLockfile, readLockfile } from './installed-version.mjs';
|
|
21
|
+
import { compareVersions, isVersion } from './semver.mjs';
|
|
22
|
+
|
|
23
|
+
const DEP_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
|
|
24
|
+
const SKIP = new Set(['node_modules', '.git']);
|
|
25
|
+
|
|
26
|
+
function readJsonFile(file) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The range an importer's package.json declares for HDS, from its first field that has one. */
|
|
35
|
+
export function declaredRange(pkg) {
|
|
36
|
+
for (const field of DEP_FIELDS) {
|
|
37
|
+
const range = pkg?.[field]?.[HDS_PACKAGE];
|
|
38
|
+
if (typeof range === 'string') return range;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One YAML scalar: its trailing ` # comment` dropped, then unquoted. */
|
|
44
|
+
function yamlScalar(raw) {
|
|
45
|
+
const text = raw.trim();
|
|
46
|
+
const quoted = /^(['"])(.*?)\1/.exec(text);
|
|
47
|
+
if (quoted) return quoted[2];
|
|
48
|
+
return text.replace(/\s+#.*$/, '').trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `packages:` of pnpm-workspace.yaml, as a block list or a flow list (`[a, b]`), unquoted. */
|
|
52
|
+
function pnpmWorkspaceGlobs(text) {
|
|
53
|
+
const globs = [];
|
|
54
|
+
let inPackages = false;
|
|
55
|
+
for (const line of text.split(/\r?\n/)) {
|
|
56
|
+
const head = /^packages\s*:\s*(.*)$/.exec(line);
|
|
57
|
+
if (head) {
|
|
58
|
+
const rest = head[1].replace(/^#.*$/, '').trim();
|
|
59
|
+
const flow = /^\[(.*)\]/.exec(rest);
|
|
60
|
+
if (flow) {
|
|
61
|
+
for (const item of flow[1].split(',')) {
|
|
62
|
+
const glob = yamlScalar(item);
|
|
63
|
+
if (glob) globs.push(glob);
|
|
64
|
+
}
|
|
65
|
+
return globs;
|
|
66
|
+
}
|
|
67
|
+
inPackages = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!inPackages) continue;
|
|
71
|
+
if (/^[^\s#-]/.test(line)) break;
|
|
72
|
+
const item = /^\s*-\s*(.+?)\s*$/.exec(line);
|
|
73
|
+
if (item) {
|
|
74
|
+
const glob = yamlScalar(item[1]);
|
|
75
|
+
if (glob) globs.push(glob);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return globs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Directories (relative, posix) under `root` that a workspace glob names. */
|
|
82
|
+
function expandGlob(root, glob) {
|
|
83
|
+
const segments = glob
|
|
84
|
+
.replace(/\/+$/, '')
|
|
85
|
+
.split('/')
|
|
86
|
+
.filter((s) => s && s !== '.');
|
|
87
|
+
const out = new Set();
|
|
88
|
+
const subdirs = (dir) => {
|
|
89
|
+
try {
|
|
90
|
+
return readdirSync(join(root, dir), { withFileTypes: true })
|
|
91
|
+
.filter((e) => e.isDirectory() && !SKIP.has(e.name))
|
|
92
|
+
.map((e) => (dir ? `${dir}/${e.name}` : e.name));
|
|
93
|
+
} catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const step = (dir, i) => {
|
|
98
|
+
if (i === segments.length) {
|
|
99
|
+
out.add(dir);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const seg = segments[i];
|
|
103
|
+
if (seg === '**') {
|
|
104
|
+
step(dir, i + 1);
|
|
105
|
+
for (const sub of subdirs(dir)) step(sub, i);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!/[*?]/.test(seg)) {
|
|
109
|
+
const next = dir ? `${dir}/${seg}` : seg;
|
|
110
|
+
if (existsSync(join(root, next))) step(next, i + 1);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const re = new RegExp(
|
|
114
|
+
`^${seg
|
|
115
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
116
|
+
.replace(/\*/g, '[^/]*')
|
|
117
|
+
.replace(/\?/g, '[^/]')}$`,
|
|
118
|
+
);
|
|
119
|
+
for (const sub of subdirs(dir))
|
|
120
|
+
if (re.test(sub.slice(sub.lastIndexOf('/') + 1))) step(sub, i + 1);
|
|
121
|
+
};
|
|
122
|
+
step('', 0);
|
|
123
|
+
return [...out];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const hasPackageJson = (root, dir) => existsSync(join(root, ...dir.split('/'), 'package.json'));
|
|
127
|
+
|
|
128
|
+
/** The workspace globs a root declares, whichever manager wrote them. */
|
|
129
|
+
function workspaceGlobs(root, rootPkg) {
|
|
130
|
+
const yaml = join(root, 'pnpm-workspace.yaml');
|
|
131
|
+
if (existsSync(yaml)) return pnpmWorkspaceGlobs(readFileSync(yaml, 'utf8'));
|
|
132
|
+
const ws = rootPkg?.workspaces;
|
|
133
|
+
if (Array.isArray(ws)) return ws;
|
|
134
|
+
if (Array.isArray(ws?.packages)) return ws.packages;
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Every importer under `root` that declares HDS, root first, then by directory.
|
|
140
|
+
* A workspace package whose package.json cannot be read is never skipped in
|
|
141
|
+
* silence: its path and the reason go on `unreadable`, for the caller to refuse.
|
|
142
|
+
* @param {string} root
|
|
143
|
+
* @param {{ file: string, reason: string }[]} [unreadable]
|
|
144
|
+
* @returns {{ dir: string, abs: string, pkg: any, range: string, nested: string[] }[]}
|
|
145
|
+
*/
|
|
146
|
+
export function findImporters(root, unreadable = []) {
|
|
147
|
+
const rootPkg = readJsonFile(join(root, 'package.json'));
|
|
148
|
+
const globs = workspaceGlobs(root, rootPkg);
|
|
149
|
+
const include = new Set();
|
|
150
|
+
const exclude = new Set();
|
|
151
|
+
for (const glob of globs) {
|
|
152
|
+
const negated = glob.startsWith('!');
|
|
153
|
+
for (const dir of expandGlob(root, negated ? glob.slice(1) : glob)) {
|
|
154
|
+
(negated ? exclude : include).add(dir);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const dirs = [
|
|
158
|
+
...new Set([
|
|
159
|
+
'.',
|
|
160
|
+
...[...include].filter((d) => d && !exclude.has(d) && hasPackageJson(root, d)).sort(),
|
|
161
|
+
]),
|
|
162
|
+
];
|
|
163
|
+
const absOf = (dir) => (dir === '.' ? root : join(root, ...dir.split('/')));
|
|
164
|
+
const out = [];
|
|
165
|
+
for (const dir of dirs) {
|
|
166
|
+
const abs = absOf(dir);
|
|
167
|
+
let pkg = rootPkg;
|
|
168
|
+
if (dir !== '.') {
|
|
169
|
+
const file = join(abs, 'package.json');
|
|
170
|
+
try {
|
|
171
|
+
pkg = JSON.parse(readFileSync(file, 'utf8'));
|
|
172
|
+
} catch (error) {
|
|
173
|
+
unreadable.push({ file, reason: error.message });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const range = declaredRange(pkg);
|
|
178
|
+
if (range === null) continue;
|
|
179
|
+
// Workspace packages inside this one are importers of their own: never scan them twice.
|
|
180
|
+
const nested = dirs
|
|
181
|
+
.filter((d) => d !== dir && (dir === '.' || d.startsWith(`${dir}/`)))
|
|
182
|
+
.map(absOf);
|
|
183
|
+
out.push({ dir, abs, pkg, range, nested });
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The workspace that `root` is a package of, when a parent directory declares
|
|
190
|
+
* one (pnpm-workspace.yaml, or package.json `workspaces`) whose globs name it:
|
|
191
|
+
* `{ dir, rel }`, the workspace root and root's path in it (posix), or null.
|
|
192
|
+
* Only the nearest parent that declares a workspace is asked.
|
|
193
|
+
*/
|
|
194
|
+
export function enclosingWorkspace(root) {
|
|
195
|
+
const abs = resolve(root);
|
|
196
|
+
for (let dir = dirname(abs); ; dir = dirname(dir)) {
|
|
197
|
+
const pkg = readJsonFile(join(dir, 'package.json'));
|
|
198
|
+
const globs = workspaceGlobs(dir, pkg);
|
|
199
|
+
if (globs.length > 0 || existsSync(join(dir, 'pnpm-workspace.yaml'))) {
|
|
200
|
+
const rel = relative(dir, abs).split(sep).join('/');
|
|
201
|
+
const names = (negated) =>
|
|
202
|
+
new Set(
|
|
203
|
+
globs
|
|
204
|
+
.filter((g) => g.startsWith('!') === negated)
|
|
205
|
+
.flatMap((g) => expandGlob(dir, negated ? g.slice(1) : g)),
|
|
206
|
+
);
|
|
207
|
+
return names(false).has(rel) && !names(true).has(rel) ? { dir, rel } : null;
|
|
208
|
+
}
|
|
209
|
+
if (dirname(dir) === dir) return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The package manager and lockfile at a root (lockfile null when there is none to read). */
|
|
214
|
+
export function detectManager(root, rootPkg) {
|
|
215
|
+
let names = [];
|
|
216
|
+
try {
|
|
217
|
+
names = readdirSync(root);
|
|
218
|
+
} catch {
|
|
219
|
+
// an unreadable root has no lockfile
|
|
220
|
+
}
|
|
221
|
+
const lockfile = pickLockfile(names, rootPkg?.packageManager);
|
|
222
|
+
const declared = String(rootPkg?.packageManager ?? '').split('@')[0];
|
|
223
|
+
const manager =
|
|
224
|
+
(lockfile && lockfileKind(lockfile)) ||
|
|
225
|
+
(['pnpm', 'npm', 'yarn', 'bun'].includes(declared) ? declared : null) ||
|
|
226
|
+
(names.includes('bun.lockb') ? 'bun' : null);
|
|
227
|
+
return { manager, lockfile };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Read the lockfile at a root, or null; `ranges` feeds yarn v1 (importer dir → declared range). */
|
|
231
|
+
export function readRootLockfile(root, lockfile, importers) {
|
|
232
|
+
if (!lockfile) return null;
|
|
233
|
+
try {
|
|
234
|
+
const ranges = Object.fromEntries(importers.map((i) => [i.dir, i.range]));
|
|
235
|
+
return readLockfile(lockfile, readFileSync(join(root, lockfile), 'utf8'), { ranges });
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The version an importer's node_modules (or the hoisted root copy) holds, or null. */
|
|
242
|
+
export function nodeModulesVersion(root, importer) {
|
|
243
|
+
for (const base of [importer.abs, root]) {
|
|
244
|
+
const pkg = readJsonFile(join(base, 'node_modules', ...HDS_PACKAGE.split('/'), 'package.json'));
|
|
245
|
+
if (isVersion(pkg?.version)) return pkg.version;
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── git ──────────────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
const GIT_MAX_BUFFER = 512 * 1024 * 1024;
|
|
253
|
+
/** How many commits that touched the lockfile to read back, newest first. */
|
|
254
|
+
const LOG_DEPTH = 200;
|
|
255
|
+
|
|
256
|
+
/** git with GIT_* stripped (a hook's GIT_DIR must not point it elsewhere) and cwd = root. */
|
|
257
|
+
function git(root, args) {
|
|
258
|
+
const env = { ...process.env };
|
|
259
|
+
for (const key of Object.keys(env)) if (key.startsWith('GIT_')) delete env[key];
|
|
260
|
+
try {
|
|
261
|
+
// A real lockfile is often several MiB: the default 1 MiB maxBuffer would
|
|
262
|
+
// kill `git show` and lose the old version without a word.
|
|
263
|
+
const res = spawnSync('git', args, {
|
|
264
|
+
cwd: root,
|
|
265
|
+
env,
|
|
266
|
+
encoding: 'utf8',
|
|
267
|
+
timeout: 60_000,
|
|
268
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
269
|
+
});
|
|
270
|
+
if (res.error) return null;
|
|
271
|
+
return res.status === 0 ? res.stdout : null;
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The default branch to take a merge-base with: origin's HEAD, else main or master. */
|
|
278
|
+
function defaultBranch(root) {
|
|
279
|
+
const head = git(root, ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD']);
|
|
280
|
+
if (head) return head.trim().replace(/^refs\/remotes\//, '');
|
|
281
|
+
for (const name of ['origin/main', 'origin/master', 'main', 'master']) {
|
|
282
|
+
if (git(root, ['rev-parse', '--verify', '--quiet', `${name}^{commit}`])) return name;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The git revisions to recover an old lockfile from: HEAD, then the
|
|
289
|
+
* merge-base with the default branch (when it differs). `null` when root is
|
|
290
|
+
* not in a git work tree or has no commit.
|
|
291
|
+
* @returns {{ rev: string, source: 'git-head' | 'git-merge-base' }[] | null}
|
|
292
|
+
*/
|
|
293
|
+
export function historyRevisions(root) {
|
|
294
|
+
const head = git(root, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'])?.trim();
|
|
295
|
+
if (!head) return null;
|
|
296
|
+
const revs = [{ rev: head, source: 'git-head' }];
|
|
297
|
+
const branch = defaultBranch(root);
|
|
298
|
+
const base = branch ? git(root, ['merge-base', 'HEAD', branch])?.trim() : null;
|
|
299
|
+
if (base && base !== head) revs.push({ rev: base, source: 'git-merge-base' });
|
|
300
|
+
return revs;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The commits that changed the lockfile, newest first along HEAD's first
|
|
305
|
+
* parents: where the old version is when the bump is already committed on the
|
|
306
|
+
* default branch, so HEAD and the merge-base both say the target.
|
|
307
|
+
*/
|
|
308
|
+
function lockfileCommits(root, lockfile) {
|
|
309
|
+
const out = git(root, [
|
|
310
|
+
'log',
|
|
311
|
+
'--first-parent',
|
|
312
|
+
`--max-count=${LOG_DEPTH}`,
|
|
313
|
+
'--format=%H',
|
|
314
|
+
'HEAD',
|
|
315
|
+
'--',
|
|
316
|
+
lockfile,
|
|
317
|
+
]);
|
|
318
|
+
return out ? out.split('\n').filter(Boolean) : [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** An importer's HDS version in the lockfile as it was at `rev`, or null. */
|
|
322
|
+
export function versionAtRevision(root, rev, lockfile, importer) {
|
|
323
|
+
const text = git(root, ['show', `${rev}:./${lockfile}`]);
|
|
324
|
+
if (text === null) return null;
|
|
325
|
+
let ranges = { [importer.dir]: importer.range };
|
|
326
|
+
if (lockfile === 'yarn.lock') {
|
|
327
|
+
const path = importer.dir === '.' ? 'package.json' : `${importer.dir}/package.json`;
|
|
328
|
+
const old = git(root, ['show', `${rev}:./${path}`]);
|
|
329
|
+
try {
|
|
330
|
+
ranges = { [importer.dir]: declaredRange(JSON.parse(old)) };
|
|
331
|
+
} catch {
|
|
332
|
+
// keep today's range
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const v = readLockfile(lockfile, text, { ranges }).importers[importer.dir];
|
|
337
|
+
return isVersion(v) ? v : null;
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The version each importer upgrades from, and where it was read.
|
|
345
|
+
* `noHistory` is set when the version is already the target and nothing says
|
|
346
|
+
* which one it was upgraded from: 'none' (no git, or nothing in it to read) or
|
|
347
|
+
* 'at-target' (every commit of the lockfile says the target). `belowFloor`
|
|
348
|
+
* holds a version read from history older than `floor`, which `from` replaces.
|
|
349
|
+
* @param {{ root: string, lockfile: string|null, lock: any, importers: any[], target: string, floor?: string, from?: string }} ctx
|
|
350
|
+
* @returns {{ from: string|null, source: string, noHistory?: 'none'|'at-target', belowFloor?: string }[]}
|
|
351
|
+
*/
|
|
352
|
+
export function installedVersions({ root, lockfile, lock, importers, target, floor, from }) {
|
|
353
|
+
let revisions;
|
|
354
|
+
let touched;
|
|
355
|
+
const history = () =>
|
|
356
|
+
revisions === undefined ? (revisions = historyRevisions(root)) : revisions;
|
|
357
|
+
const commits = () =>
|
|
358
|
+
touched === undefined ? (touched = lockfileCommits(root, lockfile)) : touched;
|
|
359
|
+
const recovered = (old, source) =>
|
|
360
|
+
floor && compareVersions(old, floor) < 0
|
|
361
|
+
? { from: floor, source, belowFloor: old }
|
|
362
|
+
: { from: old, source };
|
|
363
|
+
return importers.map((importer) => {
|
|
364
|
+
const locked = lock?.importers?.[importer.dir];
|
|
365
|
+
let found = null;
|
|
366
|
+
if (isVersion(locked)) found = { from: locked, source: 'lockfile' };
|
|
367
|
+
else {
|
|
368
|
+
const nm = nodeModulesVersion(root, importer);
|
|
369
|
+
if (nm) found = { from: nm, source: 'node_modules' };
|
|
370
|
+
}
|
|
371
|
+
if (!found)
|
|
372
|
+
return isVersion(from) ? { from, source: 'flag' } : { from: null, source: 'unknown' };
|
|
373
|
+
const atTarget = compareVersions(found.from, target) === 0;
|
|
374
|
+
// An explicit --from beats anything inferred from history.
|
|
375
|
+
if (isVersion(from)) return atTarget ? { from, source: 'flag' } : found;
|
|
376
|
+
const revs = lockfile && found.source === 'lockfile' ? history() : null;
|
|
377
|
+
if (revs) {
|
|
378
|
+
// An older lockfile at HEAD or the merge-base: bumped by hand, not yet committed or merged.
|
|
379
|
+
for (const { rev, source } of revs) {
|
|
380
|
+
const old = versionAtRevision(root, rev, lockfile, importer);
|
|
381
|
+
if (old && compareVersions(old, found.from) < 0) return recovered(old, source);
|
|
382
|
+
}
|
|
383
|
+
if (atTarget) {
|
|
384
|
+
for (const rev of commits()) {
|
|
385
|
+
const old = versionAtRevision(root, rev, lockfile, importer);
|
|
386
|
+
if (old && compareVersions(old, target) < 0) return recovered(old, 'git-log');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!atTarget) return found;
|
|
391
|
+
// No history below the target (or none readable): say so, never a silent from = target.
|
|
392
|
+
const readable = revs !== null && revs !== undefined && commits().length > 0;
|
|
393
|
+
return { ...found, noHistory: readable ? 'at-target' : 'none' };
|
|
394
|
+
});
|
|
395
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* record — the upgrade record a copy of @hirobius/design-system ships
|
|
3
|
+
* (upgrade/index.json and upgrade/releases/<version>.json, hds#451), read for
|
|
4
|
+
* the upgrade command (hds#452). Node builtins only.
|
|
5
|
+
*
|
|
6
|
+
* The command reads the record of the package it runs from, never the
|
|
7
|
+
* consumer's installed copy, so `npx @hirobius/design-system@latest upgrade`
|
|
8
|
+
* always brings the newest steps.
|
|
9
|
+
*
|
|
10
|
+
* listOf and the deprecation bookkeeping mirror scripts/upgrade/compile.mjs,
|
|
11
|
+
* which writes UPGRADING.md from the same ledgers; a test holds them equal.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { compareVersions } from './semver.mjs';
|
|
16
|
+
|
|
17
|
+
const readJson = (file) => JSON.parse(readFileSync(file, 'utf8'));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} pkgDir the package root (the directory holding package.json)
|
|
21
|
+
* @returns {{ version: string, index: any, ledgers: any[] }}
|
|
22
|
+
*/
|
|
23
|
+
export function loadRecord(pkgDir) {
|
|
24
|
+
const { version } = readJson(join(pkgDir, 'package.json'));
|
|
25
|
+
const index = readJson(join(pkgDir, 'upgrade/index.json'));
|
|
26
|
+
const ledgers = index.versions
|
|
27
|
+
.map((v) => join(pkgDir, 'upgrade/releases', `${v.version}.json`))
|
|
28
|
+
.filter((file) => existsSync(file))
|
|
29
|
+
.map(readJson)
|
|
30
|
+
.sort((a, b) => compareVersions(a.version, b.version));
|
|
31
|
+
return { version, index, ledgers };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Step kinds that take something public away. */
|
|
35
|
+
const REMOVING = new Set(['removed', 'moved', 'renamed', 'folded']);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The list a step belongs to, as UPGRADING.md prints it
|
|
39
|
+
* (scripts/upgrade/compile.mjs listOf).
|
|
40
|
+
* @returns {'fixedForYou' | 'looksDifferent' | 'comingNext' | 'doByHand'}
|
|
41
|
+
*/
|
|
42
|
+
export function listOf(step) {
|
|
43
|
+
if (step.kind === 'deprecated') return 'comingNext';
|
|
44
|
+
if (step.auto) return 'fixedForYou';
|
|
45
|
+
if (step.impact === 'look' && !REMOVING.has(step.kind) && step.kind !== 'manual') {
|
|
46
|
+
return 'looksDifferent';
|
|
47
|
+
}
|
|
48
|
+
return 'doByHand';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const subject = (step) => step.id.split('/').slice(2).join('/');
|
|
52
|
+
|
|
53
|
+
const namedUses = (step) => [
|
|
54
|
+
...(step.detect?.imports ?? []).flatMap((i) => i.names.map((name) => `import:${i.from}:${name}`)),
|
|
55
|
+
...(step.detect?.cssVars ?? []).map((name) => `cssVar:${name}`),
|
|
56
|
+
...(step.detect?.classes ?? []).map((name) => `class:${name}`),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The deprecation steps still deprecated at `target`: announced at or before
|
|
61
|
+
* it, not removed by it (removeIn after it), and with some use no later
|
|
62
|
+
* release up to `target` took away.
|
|
63
|
+
* @param {{ version: string, steps: any[] }[]} ledgers oldest first
|
|
64
|
+
* @param {string} target
|
|
65
|
+
*/
|
|
66
|
+
export function comingNextSteps(ledgers, target) {
|
|
67
|
+
const upTo = ledgers.filter((l) => compareVersions(l.version, target) <= 0);
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const ledger of upTo) {
|
|
70
|
+
const later = upTo.filter((l) => compareVersions(l.version, ledger.version) > 0);
|
|
71
|
+
for (const step of ledger.steps) {
|
|
72
|
+
if (step.kind !== 'deprecated') continue;
|
|
73
|
+
if (step.removeIn && compareVersions(step.removeIn, target) <= 0) continue;
|
|
74
|
+
const named = namedUses(step);
|
|
75
|
+
const uses = named.length > 0 ? named : [`subject:${subject(step)}`];
|
|
76
|
+
const taken = new Set();
|
|
77
|
+
for (const taker of later.flatMap((l) => l.steps).filter((s) => REMOVING.has(s.kind))) {
|
|
78
|
+
const own = namedUses(taker);
|
|
79
|
+
for (const use of uses) {
|
|
80
|
+
if (own.length === 0 && subject(taker) === subject(step)) taken.add(use);
|
|
81
|
+
else if (own.includes(use) || use === `subject:${subject(taker)}`) taken.add(use);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (taken.size === uses.length) continue;
|
|
85
|
+
if (taken.size === 0 || !step.detect) {
|
|
86
|
+
out.push(step);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Look only for the uses still deprecated: a name a later release took
|
|
90
|
+
// away is a Do by hand step of that release, not Coming next.
|
|
91
|
+
const { imports, cssVars, classes, ...rest } = step.detect;
|
|
92
|
+
const detect = {
|
|
93
|
+
...rest,
|
|
94
|
+
imports: imports
|
|
95
|
+
?.map((i) => ({
|
|
96
|
+
...i,
|
|
97
|
+
names: i.names.filter((n) => !taken.has(`import:${i.from}:${n}`)),
|
|
98
|
+
}))
|
|
99
|
+
.filter((i) => i.names.length > 0),
|
|
100
|
+
cssVars: cssVars?.filter((v) => !taken.has(`cssVar:${v}`)),
|
|
101
|
+
classes: classes?.filter((c) => !taken.has(`class:${c}`)),
|
|
102
|
+
};
|
|
103
|
+
for (const key of ['imports', 'cssVars', 'classes']) {
|
|
104
|
+
if (!detect[key]?.length) delete detect[key];
|
|
105
|
+
}
|
|
106
|
+
out.push({ ...step, detect });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|