@hirobius/design-system 0.17.0 → 0.18.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/README.md +16 -0
- package/codemods/patterns-subpath.mjs +254 -0
- package/codemods/patterns-subpath.names.json +26 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -23,6 +23,22 @@ pnpm add @hirobius/design-system
|
|
|
23
23
|
|
|
24
24
|
The counts are generated from source by `pnpm readme:counts`, which `pnpm tokens` also runs. `scripts/__tests__/front-door.test.mjs` fails if this README claims more than the source has.
|
|
25
25
|
|
|
26
|
+
## In use
|
|
27
|
+
|
|
28
|
+
<!-- auto:start:consumer-usage -->
|
|
29
|
+
|
|
30
|
+
The Ops dashboard (`hirobius/ops`) is the only verified component-level consumer: **35** of its source files import from `@hirobius/design-system`, using **25** distinct components. The two screenshots below are Ops pages rendered from its `main` (measured at commit `01fce2f`).
|
|
31
|
+
|
|
32
|
+
Other consumers: the split into product apps and token-level sites is not yet confirmed, so it is not stated here.
|
|
33
|
+
|
|
34
|
+
[](docs/images/ops-library.png)
|
|
35
|
+
|
|
36
|
+
[](docs/images/ops-fleet-audit.png)
|
|
37
|
+
|
|
38
|
+
<!-- auto:end:consumer-usage -->
|
|
39
|
+
|
|
40
|
+
The numbers come from `pnpm consumer:usage --root <ops checkout>`, which measures that checkout and records its commit in `docs/data/consumer-usage.json`; without a root it reuses the committed snapshot. `scripts/__tests__/consumer-usage.test.mjs` fails if this section drifts from that snapshot. The product-app and token-level-site split is a declared figure in that file and stays out of this section until `consumersConfirmed` there is set to true.
|
|
41
|
+
|
|
26
42
|
## Using the published package
|
|
27
43
|
|
|
28
44
|
Installing HDS in another project? It ships to the **public npm registry** as
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* patterns-subpath codemod (hds#316, follows hds#254)
|
|
4
|
+
*
|
|
5
|
+
* Rewrites `import { Page } from '@hirobius/design-system'` to
|
|
6
|
+
* `import { Page } from '@hirobius/design-system/patterns'` for every
|
|
7
|
+
* pattern-tier component whose root re-export is deprecated. Other named
|
|
8
|
+
* imports stay on the root. Aliases, `type` modifiers and multi-line layout
|
|
9
|
+
* are preserved; an existing `/patterns` import of the same kind is extended
|
|
10
|
+
* instead of duplicated.
|
|
11
|
+
*
|
|
12
|
+
* node codemods/patterns-subpath.mjs [--root <dir>] [--check] [--dry-run]
|
|
13
|
+
*
|
|
14
|
+
* --root <dir> directory to scan (default: current directory)
|
|
15
|
+
* --check write nothing; exit 1 when a rewrite is needed or a root namespace
|
|
16
|
+
* import (`import * as X`, `export *`) needs a manual look
|
|
17
|
+
* --dry-run write nothing; print each import line before (-) and after (+), exit 0
|
|
18
|
+
*
|
|
19
|
+
* The name list is codemods/patterns-subpath.names.json, generated from
|
|
20
|
+
* src/index.ts by `pnpm codemod:names`.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const ROOT_PKG = '@hirobius/design-system';
|
|
27
|
+
const SUBPATH = `${ROOT_PKG}/patterns`;
|
|
28
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
|
|
30
|
+
const SKIP_DIRS = new Set([
|
|
31
|
+
'node_modules',
|
|
32
|
+
'dist',
|
|
33
|
+
'build',
|
|
34
|
+
'.git',
|
|
35
|
+
'.next',
|
|
36
|
+
'.vercel',
|
|
37
|
+
'coverage',
|
|
38
|
+
'storybook-static',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export function loadPatternNames() {
|
|
42
|
+
const { names } = JSON.parse(readFileSync(join(HERE, 'patterns-subpath.names.json'), 'utf8'));
|
|
43
|
+
return new Set(names);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const esc = (pkg) => pkg.replace(/[/@]/g, '\\$&');
|
|
47
|
+
// indent, import|export, type?, default?, { body }, quote, semicolon
|
|
48
|
+
const namedRe = (pkg) =>
|
|
49
|
+
new RegExp(
|
|
50
|
+
`^([ \\t]*)(import|export)(\\s+type)?\\s*(?:([\\w$]+)\\s*,\\s*)?\\{([^}]*)\\}\\s*from\\s*(['"])${esc(pkg)}\\6(;?)`,
|
|
51
|
+
'gm',
|
|
52
|
+
);
|
|
53
|
+
// `import * as X`, `import D, * as X`, `export * from`, `export * as X from` on the root
|
|
54
|
+
const namespaceRe = new RegExp(
|
|
55
|
+
`^[ \\t]*(?:import\\s+(?:[\\w$]+\\s*,\\s*)?\\*\\s*as\\s+[\\w$]+|export\\s+\\*(?:\\s*as\\s+[\\w$]+)?)\\s*from\\s*['"]${esc(ROOT_PKG)}['"]`,
|
|
56
|
+
'gm',
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const specName = (spec) =>
|
|
60
|
+
spec
|
|
61
|
+
.replace(/^type\s+/, '')
|
|
62
|
+
.split(/\s+as\s+/)[0]
|
|
63
|
+
.trim();
|
|
64
|
+
const splitSpecs = (body) =>
|
|
65
|
+
body
|
|
66
|
+
.split(',')
|
|
67
|
+
.map((s) => s.trim())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
|
|
70
|
+
/** Namespace imports and star re-exports of the root: names are not visible, so no rewrite. */
|
|
71
|
+
export function findUnrewritable(source) {
|
|
72
|
+
return [...source.matchAll(namespaceRe)].map((m) => m[0].trim());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Renders one import/export statement. */
|
|
76
|
+
function render({ indent, kw, typeKw, def, list, pkg, multiline, quote, semi }) {
|
|
77
|
+
const head = `${indent}${kw}${typeKw ? ' type' : ''} `;
|
|
78
|
+
const tail = ` from ${quote}${pkg}${quote}${semi}`;
|
|
79
|
+
if (list.length === 0) return `${head}${def}${tail}`;
|
|
80
|
+
const d = def ? `${def}, ` : '';
|
|
81
|
+
if (multiline)
|
|
82
|
+
return `${head}${d}{\n${list.map((s) => `${indent} ${s},`).join('\n')}\n${indent}}${tail}`;
|
|
83
|
+
return `${head}${d}{ ${list.join(', ')} }${tail}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Pure transform of one file's source.
|
|
88
|
+
* @returns {{ source: string, changed: boolean, sites: number, moved: string[],
|
|
89
|
+
* edits: {before: string, after: string}[] }}
|
|
90
|
+
*/
|
|
91
|
+
export function transformSource(source, names) {
|
|
92
|
+
const moved = [];
|
|
93
|
+
const edits = [];
|
|
94
|
+
const isType = (m) => !!m[3];
|
|
95
|
+
const parse = (m) => ({
|
|
96
|
+
indent: m[1],
|
|
97
|
+
kw: m[2],
|
|
98
|
+
typeKw: m[3],
|
|
99
|
+
def: m[4],
|
|
100
|
+
body: m[5],
|
|
101
|
+
quote: m[6],
|
|
102
|
+
semi: m[7],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// An existing `import { … } from '/patterns'` of each kind absorbs moved names.
|
|
106
|
+
const targets = {};
|
|
107
|
+
for (const m of source.matchAll(namedRe(SUBPATH))) {
|
|
108
|
+
if (m[2] !== 'import' || m[4]) continue;
|
|
109
|
+
targets[isType(m) ? 'type' : 'value'] ??= { m, add: [] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const replacements = [];
|
|
113
|
+
for (const m of source.matchAll(namedRe(ROOT_PKG))) {
|
|
114
|
+
const p = parse(m);
|
|
115
|
+
const specs = splitSpecs(p.body);
|
|
116
|
+
const go = specs.filter((s) => names.has(specName(s)));
|
|
117
|
+
if (go.length === 0) continue;
|
|
118
|
+
moved.push(...go.map(specName));
|
|
119
|
+
const keep = specs.filter((s) => !go.includes(s));
|
|
120
|
+
const multiline = p.body.includes('\n');
|
|
121
|
+
const lines = [];
|
|
122
|
+
if (keep.length > 0 || p.def)
|
|
123
|
+
lines.push(render({ ...p, list: keep, pkg: ROOT_PKG, multiline }));
|
|
124
|
+
const target = p.kw === 'import' && targets[p.typeKw ? 'type' : 'value'];
|
|
125
|
+
if (target) target.add.push(...go);
|
|
126
|
+
else lines.push(render({ ...p, def: undefined, list: go, pkg: SUBPATH, multiline }));
|
|
127
|
+
replacements.push({ start: m.index, end: m.index + m[0].length, before: m[0], lines });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (replacements.length === 0) return { source, changed: false, sites: 0, moved: [], edits: [] };
|
|
131
|
+
|
|
132
|
+
const sites = replacements.length;
|
|
133
|
+
for (const { m, add } of Object.values(targets)) {
|
|
134
|
+
if (add.length === 0) continue;
|
|
135
|
+
const p = parse(m);
|
|
136
|
+
const have = splitSpecs(p.body);
|
|
137
|
+
const merged = [...have, ...add.filter((n) => !have.includes(n))];
|
|
138
|
+
replacements.push({
|
|
139
|
+
start: m.index,
|
|
140
|
+
end: m.index + m[0].length,
|
|
141
|
+
before: m[0],
|
|
142
|
+
lines: [
|
|
143
|
+
render({
|
|
144
|
+
...p,
|
|
145
|
+
def: undefined,
|
|
146
|
+
list: merged,
|
|
147
|
+
pkg: SUBPATH,
|
|
148
|
+
multiline: p.body.includes('\n'),
|
|
149
|
+
}),
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let out = source;
|
|
155
|
+
for (const r of replacements.sort((x, y) => y.start - x.start)) {
|
|
156
|
+
const after = r.lines.join('\n');
|
|
157
|
+
// A statement folded entirely into an existing import leaves nothing behind, not a blank line.
|
|
158
|
+
const end = after === '' && out[r.end] === '\n' ? r.end + 1 : r.end;
|
|
159
|
+
out = out.slice(0, r.start) + after + out.slice(end);
|
|
160
|
+
edits.unshift({ before: r.before, after });
|
|
161
|
+
}
|
|
162
|
+
return { source: out, changed: out !== source, sites, moved, edits };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function* walk(dir) {
|
|
166
|
+
for (const entry of readdirSync(dir)) {
|
|
167
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
168
|
+
const full = join(dir, entry);
|
|
169
|
+
const st = statSync(full);
|
|
170
|
+
if (st.isDirectory()) yield* walk(full);
|
|
171
|
+
else if (EXTS.has(full.slice(full.lastIndexOf('.')))) yield full;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Scan a directory. Writes only when `write` is true. */
|
|
176
|
+
export function runCodemod({ root, write = false, names = loadPatternNames() }) {
|
|
177
|
+
const files = [];
|
|
178
|
+
const manual = [];
|
|
179
|
+
const moved = new Set();
|
|
180
|
+
let sites = 0;
|
|
181
|
+
for (const file of walk(root)) {
|
|
182
|
+
const src = readFileSync(file, 'utf8');
|
|
183
|
+
if (!src.includes(ROOT_PKG)) continue;
|
|
184
|
+
for (const stmt of findUnrewritable(src)) manual.push({ file: relative(root, file), stmt });
|
|
185
|
+
const r = transformSource(src, names);
|
|
186
|
+
if (!r.changed) continue;
|
|
187
|
+
files.push({ file: relative(root, file), sites: r.sites, moved: r.moved, edits: r.edits });
|
|
188
|
+
r.moved.forEach((n) => moved.add(n));
|
|
189
|
+
sites += r.sites;
|
|
190
|
+
if (write) writeFileSync(file, r.source);
|
|
191
|
+
}
|
|
192
|
+
return { files, sites, manual, names: [...moved].sort() };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function main(argv) {
|
|
196
|
+
const args = { root: process.cwd(), check: false, dryRun: false };
|
|
197
|
+
for (let i = 0; i < argv.length; i++) {
|
|
198
|
+
if (argv[i] === '--root') args.root = resolve(argv[++i] ?? '');
|
|
199
|
+
else if (argv[i] === '--check') args.check = true;
|
|
200
|
+
else if (argv[i] === '--dry-run') args.dryRun = true;
|
|
201
|
+
else {
|
|
202
|
+
console.error(
|
|
203
|
+
`unknown argument: ${argv[i]}\nusage: patterns-subpath [--root <dir>] [--check] [--dry-run]`,
|
|
204
|
+
);
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const write = !args.check && !args.dryRun;
|
|
209
|
+
const res = runCodemod({ root: args.root, write });
|
|
210
|
+
const summary = `${res.files.length} files, ${res.sites} import sites, ${res.names.length} names (${res.names.join(', ') || 'none'})`;
|
|
211
|
+
const manualLines = res.manual.map(
|
|
212
|
+
(m) => ` ${m.file}: ${m.stmt} (namespace import: move pattern names to '${SUBPATH}' by hand)`,
|
|
213
|
+
);
|
|
214
|
+
if (args.check) {
|
|
215
|
+
if (res.sites > 0 || res.manual.length > 0) {
|
|
216
|
+
console.error(`patterns-subpath: rewrite needed: ${summary}`);
|
|
217
|
+
for (const f of res.files) console.error(` ${f.file}: ${f.moved.join(', ')}`);
|
|
218
|
+
for (const l of manualLines) console.error(l);
|
|
219
|
+
return 1;
|
|
220
|
+
}
|
|
221
|
+
console.log('patterns-subpath: nothing to rewrite');
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
console.log(`patterns-subpath: ${write ? 'rewrote' : 'would rewrite'} ${summary}`);
|
|
225
|
+
for (const f of res.files) {
|
|
226
|
+
console.log(` ${f.file}: ${f.moved.join(', ')}`);
|
|
227
|
+
if (args.dryRun)
|
|
228
|
+
for (const e of f.edits) {
|
|
229
|
+
for (const l of e.before.split('\n')) console.log(`- ${l}`);
|
|
230
|
+
for (const l of e.after.split('\n')) console.log(`+ ${l}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (res.manual.length > 0) {
|
|
234
|
+
console.warn('patterns-subpath: cannot rewrite these, do them by hand:');
|
|
235
|
+
for (const l of manualLines) console.warn(l);
|
|
236
|
+
}
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Compare real paths: npm/yarn link the bin and pnpm links the package directory, so
|
|
241
|
+
// argv[1] is a symlink while import.meta.url is already resolved.
|
|
242
|
+
const isEntry = (() => {
|
|
243
|
+
try {
|
|
244
|
+
return (
|
|
245
|
+
!!process.argv[1] &&
|
|
246
|
+
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
|
|
247
|
+
);
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
if (isEntry) {
|
|
253
|
+
process.exit(main(process.argv.slice(2)));
|
|
254
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"subpath": "@hirobius/design-system/patterns",
|
|
3
|
+
"names": [
|
|
4
|
+
"ActivityFeed",
|
|
5
|
+
"AppShell",
|
|
6
|
+
"AssetImg",
|
|
7
|
+
"Calendar",
|
|
8
|
+
"Carousel",
|
|
9
|
+
"CodeBlock",
|
|
10
|
+
"CommandPalette",
|
|
11
|
+
"DocLinkCard",
|
|
12
|
+
"ErrorPattern",
|
|
13
|
+
"FileInput",
|
|
14
|
+
"Form",
|
|
15
|
+
"Lightbox",
|
|
16
|
+
"NavItem",
|
|
17
|
+
"OverflowList",
|
|
18
|
+
"Page",
|
|
19
|
+
"Reveal",
|
|
20
|
+
"SideNav",
|
|
21
|
+
"Stepper",
|
|
22
|
+
"Toolbar",
|
|
23
|
+
"TopNav",
|
|
24
|
+
"TreeList"
|
|
25
|
+
]
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"author": "Adrian Milsap",
|
|
21
21
|
"license": "SEE LICENSE IN NOTICE.md",
|
|
22
22
|
"private": false,
|
|
23
|
-
"version": "0.
|
|
23
|
+
"version": "0.18.0",
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=20",
|
|
26
26
|
"pnpm": ">=8"
|
|
@@ -97,7 +97,9 @@
|
|
|
97
97
|
"dist",
|
|
98
98
|
"NOTICE.md",
|
|
99
99
|
"hirobius.tokens.json",
|
|
100
|
-
"public/hds-manifest.json"
|
|
100
|
+
"public/hds-manifest.json",
|
|
101
|
+
"codemods/patterns-subpath.mjs",
|
|
102
|
+
"codemods/patterns-subpath.names.json"
|
|
101
103
|
],
|
|
102
104
|
"publishConfig": {
|
|
103
105
|
"registry": "https://registry.npmjs.org",
|
|
@@ -232,6 +234,9 @@
|
|
|
232
234
|
"lint-staged": {
|
|
233
235
|
"*.{ts,tsx,js,jsx,mjs,cjs,json,md,mdx,css,scss,html,yaml,yml}": "prettier --write"
|
|
234
236
|
},
|
|
237
|
+
"bin": {
|
|
238
|
+
"hds-patterns-subpath": "codemods/patterns-subpath.mjs"
|
|
239
|
+
},
|
|
235
240
|
"scripts": {
|
|
236
241
|
"_comment:a11y-high-value": "The following check:* validators are high-value accessibility checks that catch real violations (check:focus found 1 missing focus class). Kept dormant/manual-only to avoid pre-commit performance regression. Candidates to wire into pretest after CI stability review.",
|
|
237
242
|
"_comment:editorconfig-version": "check:mojibake routes through scripts/lib/mojibake.mjs, which pins EC_VERSION=v3.7.0. The editorconfig-checker@6.1.x npm wrapper expects binary v3.7.0 but downloads the latest Go release (v3.8.0, 2026-06-25), so a fresh install fails with a version mismatch (binary != config). Pinning the download to the wrapper's expected version keeps CI + pre-commit green. Bump both together when upgrading the npm package. The wrapper also SKIPS gracefully (like check:secrets) when the binary download is network-blocked (remote agent sessions), so hooks no longer need --no-verify (#18/#52); the gate still enforces wherever the binary is present.",
|
|
@@ -386,6 +391,8 @@
|
|
|
386
391
|
"validate:manifest": "node scripts/validate-manifest.mjs",
|
|
387
392
|
"visual-ingest": "node scripts/visual-ingest.mjs",
|
|
388
393
|
"watch:guardrails": "nodemon --watch CLAUDE.md --watch .cursorrules --watch eslint.config.js --watch .github/workflows -e md,json,js,yml,yaml --exec \"node scripts/audit-guardrails.js\"",
|
|
389
|
-
"watch:metrics": "nodemon --watch src/app/components --watch src/app/pages --watch src/app/design-system --watch src/app/styles -e tsx,ts,css --exec \"node scripts/audit-tokens.mjs --full && node scripts/generate-manifest.mjs\""
|
|
394
|
+
"watch:metrics": "nodemon --watch src/app/components --watch src/app/pages --watch src/app/design-system --watch src/app/styles -e tsx,ts,css --exec \"node scripts/audit-tokens.mjs --full && node scripts/generate-manifest.mjs\"",
|
|
395
|
+
"consumer:usage": "node scripts/count-consumer-usage.mjs",
|
|
396
|
+
"codemod:names": "node scripts/build-codemod-pattern-names.mjs"
|
|
390
397
|
}
|
|
391
398
|
}
|