@flashdsm/genome 0.1.0 → 0.2.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 +175 -19
- package/dist/{chunk-6OCQZNS7.js → chunk-PHEWKKKF.js} +14 -357
- package/dist/index.d.ts +70 -6
- package/dist/index.js +4008 -357
- package/dist/smoke.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,18 +13,28 @@ your project is enough.
|
|
|
13
13
|
|
|
14
14
|
## Connect it to a Flash design system
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Sign in once. Genome opens Flash in the browser, the same OAuth flow Cursor uses, and stores the session under `~/.config/flash-dsm/`. Nothing is printed and nothing goes in the repo.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
19
|
+
npx @flashdsm/genome login
|
|
20
20
|
npx @flashdsm/genome link
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`link` asks the server which system
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
`link` asks the server which system you signed in to and writes `genome.config.json` next to your code.
|
|
24
|
+
|
|
25
|
+
A script that cannot open a browser can still set `FLASH_GENOME_TOKEN` to a `flash_mcp_…` value. That is the same kind of token OAuth issues.
|
|
26
|
+
|
|
27
|
+
## No design system yet? Start from your code
|
|
28
|
+
|
|
29
|
+
In your app's folder, in a terminal or in your AI editor's chat:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @flashdsm/genome@latest connect
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`connect` scans the folder (stylesheets, CSS inside JS/TS, Tailwind config, theme modules, tokens files; nothing else is read) and opens Flash in your browser, showing what it found and a short code that matches the one in your terminal. Click **Connect** there: Flash creates the system in your account, and the command links the repo (`genome.config.json`), writes the system into it (`pull --confirm`) and stores a token bound to that system under `~/.config/flash-dsm/`. Inside Claude Code it also adds Genome as the project's MCP server; elsewhere it prints the line to add. `--name` overrides the name, which otherwise comes from `package.json`; `--no-open` prints the link instead of opening it.
|
|
36
|
+
|
|
37
|
+
The link works once and expires after 15 minutes. `init --code` does the same with a setup code instead of the browser.
|
|
28
38
|
|
|
29
39
|
## Quick start
|
|
30
40
|
|
|
@@ -50,6 +60,38 @@ With no flags it looks for `./genome.config.json`, then the older
|
|
|
50
60
|
`./components.json`, and finally falls back to a small bundled fixture so the
|
|
51
61
|
server always boots. It logs which file it resolved to stderr.
|
|
52
62
|
|
|
63
|
+
## Project shell preview
|
|
64
|
+
|
|
65
|
+
After `scan_workspace` creates a design system from local tokens, capture a
|
|
66
|
+
running preview so Flash can show **Your project** in the editor:
|
|
67
|
+
|
|
68
|
+
| Tool | What it does |
|
|
69
|
+
| ----------------------- | ------------------------------------------------------------------------- |
|
|
70
|
+
| `capture_project_shell` | Headless capture of a preview URL → upload + attach as `source: project` |
|
|
71
|
+
| `scan_usage` | Where the repo's own components are used → the editor's "In your product" |
|
|
72
|
+
|
|
73
|
+
**Localhost vs public URL:** Flash’s API cannot open `localhost` on your
|
|
74
|
+
laptop, so Genome always captures on your machine (Playwright). Pass
|
|
75
|
+
`http://localhost:6006/` (or any public `https://…` preview you can reach
|
|
76
|
+
locally). The API only stores the sanitised HTML+CSS — it never fetches the
|
|
77
|
+
URL for you. Full teammate loop: [docs/workflows/README.md](../../docs/workflows/README.md)
|
|
78
|
+
§ E.
|
|
79
|
+
|
|
80
|
+
Set an optional default URL in `genome.config.json`:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"version": 2,
|
|
85
|
+
"genomeId": 42,
|
|
86
|
+
"name": "Acme Product",
|
|
87
|
+
"previewUrl": "http://localhost:6006/"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then call `capture_project_shell` with no `url`, or pass `url` and
|
|
92
|
+
`designSystemId` explicitly. Sign in with `flash-genome login` or
|
|
93
|
+
`FLASH_GENOME_TOKEN` (same credential as other hosted tools — never commit it).
|
|
94
|
+
|
|
53
95
|
## Keeping a repo in sync
|
|
54
96
|
|
|
55
97
|
Once linked:
|
|
@@ -83,6 +125,34 @@ is ahead of `genome/genome.lock`, because a value left over from the last
|
|
|
83
125
|
pull would otherwise look like an edit and undo someone's change. Pull,
|
|
84
126
|
re-apply your edits, then push.
|
|
85
127
|
|
|
128
|
+
### Tokens that change with the viewport
|
|
129
|
+
|
|
130
|
+
A system with breakpoints writes each size that moves at its widest step,
|
|
131
|
+
and lists every step beside it:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
"type-h1": {
|
|
135
|
+
"$value": "48px",
|
|
136
|
+
"$type": "font",
|
|
137
|
+
"$extensions": {
|
|
138
|
+
"com.flashdsm.breakpoints": [
|
|
139
|
+
{ "name": "phone", "minWidth": 0, "value": "36px" },
|
|
140
|
+
{ "name": "tablet", "minWidth": 768, "value": "42px" },
|
|
141
|
+
{ "name": "desktop", "minWidth": 1280, "value": "48px" }
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`tokens.css` carries the same steps as media queries, so `var(--type-h1)`
|
|
148
|
+
is already responsive.
|
|
149
|
+
|
|
150
|
+
A push carries the narrowest step only, because that is the one value Flash
|
|
151
|
+
stores for the token. Change `phone` in the list and it is proposed like any
|
|
152
|
+
other edit. Change `$value` or a wider step and it is flagged: set those
|
|
153
|
+
sizes in Flash, then pull. A repo that pulled before Genome wrote steps is
|
|
154
|
+
told to pull again.
|
|
155
|
+
|
|
86
156
|
`AGENTS.md` is written once and then left alone however far it drifts: it is
|
|
87
157
|
where a team puts its own instructions. The Cursor rule is Genome's to keep
|
|
88
158
|
current, because a stale rule file is worse than none.
|
|
@@ -102,18 +172,20 @@ name. No linked repo needed.
|
|
|
102
172
|
|
|
103
173
|
## Tools
|
|
104
174
|
|
|
105
|
-
| Tool | Args
|
|
106
|
-
| --------------------------- |
|
|
107
|
-
| `get_tokens` | `category?`
|
|
108
|
-
| `list_components` | `category?`
|
|
109
|
-
| `get_component` | `name`
|
|
110
|
-
| `get_brand_dna` | -
|
|
111
|
-
| `suggest_token` | `context`
|
|
112
|
-
| `apply_token_patch` | `patch`, `confirm?`
|
|
113
|
-
| `generate_component_prompt` | `name`
|
|
114
|
-
| `check_violations` | `file`
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
175
|
+
| Tool | Args | Description |
|
|
176
|
+
| --------------------------- | ------------------------------------------- | ---------------------------------------------------------------- |
|
|
177
|
+
| `get_tokens` | `category?` | Read tokens from the local token file. Read-only |
|
|
178
|
+
| `list_components` | `category?` | List components from `components.json` |
|
|
179
|
+
| `get_component` | `name` | One component spec |
|
|
180
|
+
| `get_brand_dna` | - | Brand DNA from a sibling file or `tokens['dsm-brand-dna']` |
|
|
181
|
+
| `suggest_token` | `context` | Nearest-token suggestions for hex/px in a snippet. Deterministic |
|
|
182
|
+
| `apply_token_patch` | `patch`, `confirm?` | Upsert/remove tokens. Dry-run unless `confirm: true` |
|
|
183
|
+
| `generate_component_prompt` | `name` | Handoff prompt for a component plus its tokens |
|
|
184
|
+
| `check_violations` | `file` | Lint a source file for hardcoded colors / arbitrary Tailwind |
|
|
185
|
+
| `capture_project_shell` | `url?`, `designSystemId?`, `viewportWidth?` | Local preview capture → upload + attach (localhost OK) |
|
|
186
|
+
| `scan_usage` | `designSystemId?`, `countsOnly?`, `dryRun?` | Count where components are used; `dryRun` sends nothing |
|
|
187
|
+
| `api_health` | - | `GET {api}/health` against a Flash DSM backend |
|
|
188
|
+
| `list_design_systems` | - | List your systems from a Flash DSM backend (needs a token) |
|
|
117
189
|
|
|
118
190
|
Writes are guarded: `apply_token_patch` is a dry-run unless you pass
|
|
119
191
|
`confirm: true`, so a curious agent cannot rewrite your tokens by accident.
|
|
@@ -140,6 +212,90 @@ flash-genome --tokens ./design-tokens.json --components ./components.json
|
|
|
140
212
|
Against a backend on your own machine, pass
|
|
141
213
|
`--api-url http://localhost:8008`.
|
|
142
214
|
|
|
215
|
+
### Sync commands: `pull`, `push`, `drift`
|
|
216
|
+
|
|
217
|
+
Scripts and CI need no MCP client. These run the same code as the MCP tools
|
|
218
|
+
`pull_from_web`, `push_to_web` and `detect_drift`, in the repo that holds
|
|
219
|
+
`genome.config.json` (run `link` first).
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
flash-genome pull # print what would be written; writes nothing
|
|
223
|
+
flash-genome pull --confirm # write the token files, the mirror and genome.lock
|
|
224
|
+
flash-genome push # print the token edits that would be proposed
|
|
225
|
+
flash-genome push --confirm # open the change set for review in Flash
|
|
226
|
+
flash-genome drift # has the repo moved, has Flash, which tokens
|
|
227
|
+
flash-genome drift --json # the same, as one JSON object (for watchers)
|
|
228
|
+
flash-genome pull --cwd ../web # run against another repo root
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Flag | Applies to | Meaning |
|
|
232
|
+
| ---------------------------------- | ---------- | -------------------------------------------------------------------------- |
|
|
233
|
+
| `--confirm` | pull, push | Write / propose. Without it the command is a dry run |
|
|
234
|
+
| `--json` | all three | Print one JSON object on stdout instead of the plan (errors too) |
|
|
235
|
+
| `--cwd` | all three | Repo root (default: the current directory); `--config` resolves against it |
|
|
236
|
+
| `--config`, `--api-url`, `--token` | all three | As in the table above |
|
|
237
|
+
|
|
238
|
+
The human output is the plan: `+ create`, `~ update`, `! flag` lines (a flag
|
|
239
|
+
is something a person must look at), and a count of the unchanged items.
|
|
240
|
+
|
|
241
|
+
**Exit codes**
|
|
242
|
+
|
|
243
|
+
| Code | Meaning |
|
|
244
|
+
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
245
|
+
| `0` | Done. A dry run that printed its plan is done. `drift` exits 0 whether or not it found drift: read the output |
|
|
246
|
+
| `1` | Refused or failed: `push` while Flash is ahead of the last pull, `push` or `drift` in a repo that never pulled, a `pull` where a file was flagged (refused or failed to write), or a network/server error |
|
|
247
|
+
| `2` | Auth or config: no token, a token Flash rejects (401/403), no `genome.config.json`, or a token for a different design system than the config names |
|
|
248
|
+
|
|
249
|
+
**`--json` output.** Always one object on stdout, always with `ok`, `command`,
|
|
250
|
+
`exitCode` (equal to the process exit code) and `schemaVersion` (currently
|
|
251
|
+
`1`; it moves only if a field is removed or changes meaning, fields are
|
|
252
|
+
otherwise only added).
|
|
253
|
+
|
|
254
|
+
- `pull` / `push`: `{ ok, command, exitCode, schemaVersion, result }`, where
|
|
255
|
+
`result` is the tool's own result (the change plan: `dryRun`, `entries`,
|
|
256
|
+
`counts`, plus `written` for pull, `status` / `changeSetId` for push).
|
|
257
|
+
- Any failure: `{ ok: false, command, exitCode, schemaVersion, error: { kind, message } }`
|
|
258
|
+
with `kind` one of `auth`, `config`, `refused`, `error`.
|
|
259
|
+
- `drift`:
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"ok": true,
|
|
264
|
+
"command": "drift",
|
|
265
|
+
"exitCode": 0,
|
|
266
|
+
"schemaVersion": 1,
|
|
267
|
+
"genomeId": 42,
|
|
268
|
+
"state": "repo-dirty",
|
|
269
|
+
"neverPulled": false,
|
|
270
|
+
"flashAhead": false,
|
|
271
|
+
"dirty": true,
|
|
272
|
+
"pushable": 2,
|
|
273
|
+
"changedFiles": ["design-tokens.json"],
|
|
274
|
+
"missingFiles": [],
|
|
275
|
+
"changedTokens": [
|
|
276
|
+
{
|
|
277
|
+
"path": "color.brand-500 (dark)",
|
|
278
|
+
"before": "#2563eb",
|
|
279
|
+
"after": "#ff0000",
|
|
280
|
+
"code": "repo-edit"
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
"pulledPublishedVersionId": 100,
|
|
284
|
+
"publishedVersionId": 100,
|
|
285
|
+
"pulledRevision": 7,
|
|
286
|
+
"revision": 7,
|
|
287
|
+
"summary": "The repo changed 2 token values. Run push_to_web to propose them for review in Flash."
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`state` is one of `in-step`, `flash-ahead` (Flash published since the pull),
|
|
292
|
+
`repo-dirty` (a pulled file changed or is missing), `both`, `never-pulled`.
|
|
293
|
+
`flashAhead` and `dirty` are the booleans behind it; `changedTokens` lists
|
|
294
|
+
the token values the repo changed (`code: "repo-edit"`, what `push` would
|
|
295
|
+
propose) and anything else a push does not carry (any other `code`).
|
|
296
|
+
`never-pulled` is the one `drift` result with exit code `1` and `ok: false`;
|
|
297
|
+
the object is still printed in full.
|
|
298
|
+
|
|
143
299
|
## Hosted alternative
|
|
144
300
|
|
|
145
301
|
If your design system lives in Flash DSM, you may not need this package at all:
|
|
@@ -29,6 +29,7 @@ var TokenCategorySchema = z.enum([
|
|
|
29
29
|
"icons"
|
|
30
30
|
]);
|
|
31
31
|
var TokenStatusSchema = z.enum(["active", "deprecated", "legacy"]);
|
|
32
|
+
var ColorGroupTypeSchema = z.enum(["solid", "gradient"]);
|
|
32
33
|
var TokenSchema = z.object({
|
|
33
34
|
name: z.string(),
|
|
34
35
|
value: z.string(),
|
|
@@ -43,6 +44,7 @@ var TokenSchema = z.object({
|
|
|
43
44
|
foundationDetachedRowIds: z.array(z.string()).optional(),
|
|
44
45
|
foundationServingRowIds: z.array(z.string()).optional(),
|
|
45
46
|
metadata: z.record(z.any()).optional(),
|
|
47
|
+
responsive: z.record(z.string()).optional(),
|
|
46
48
|
documentation: z.string().optional(),
|
|
47
49
|
lastModified: z.number().optional(),
|
|
48
50
|
lastSynced: z.number().optional(),
|
|
@@ -65,6 +67,7 @@ var TokenSchema = z.object({
|
|
|
65
67
|
"focus"
|
|
66
68
|
]).optional(),
|
|
67
69
|
referencedTokenId: z.string().optional(),
|
|
70
|
+
inheritsLight: z.boolean().optional(),
|
|
68
71
|
tokenId: z.number().int().optional()
|
|
69
72
|
});
|
|
70
73
|
var DesignTokenSchema = z.object({
|
|
@@ -201,7 +204,10 @@ var AtomicBindingSchema = z2.object({
|
|
|
201
204
|
variantId: z2.string().optional(),
|
|
202
205
|
stateId: ComponentPreviewStateSchema.optional(),
|
|
203
206
|
slot: BindingSlotSchema.or(z2.string().min(1)),
|
|
204
|
-
tokenId: z2.string().min(1)
|
|
207
|
+
tokenId: z2.string().min(1),
|
|
208
|
+
/** The Dark side, when it differs from Light (`tokenId`); missing, Dark
|
|
209
|
+
* reads `tokenId`. */
|
|
210
|
+
darkTokenId: z2.string().min(1).optional()
|
|
205
211
|
});
|
|
206
212
|
var AtomicComponentProvenanceSchema = z2.object({
|
|
207
213
|
figmaNodeId: z2.string().optional(),
|
|
@@ -339,357 +345,14 @@ var GenomeConfigSchema = z3.object({
|
|
|
339
345
|
outputs: GenomeOutputsSchema.default({}),
|
|
340
346
|
framework: z3.string().optional(),
|
|
341
347
|
/** Absent only in configs written before codebase identity existed. */
|
|
342
|
-
codebase: GenomeCodebaseSchema.optional()
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
"file",
|
|
350
|
-
"token",
|
|
351
|
-
"variable",
|
|
352
|
-
"style",
|
|
353
|
-
"component"
|
|
354
|
-
]);
|
|
355
|
-
var ChangePlanEntrySchema = z4.object({
|
|
356
|
-
action: ChangeActionSchema,
|
|
357
|
-
targetKind: ChangeTargetKindSchema,
|
|
358
|
-
/** Repo path, token name, variable path, style name, component id. */
|
|
359
|
-
path: z4.string().min(1),
|
|
360
|
-
/** The value on the target before; null when it did not exist. */
|
|
361
|
-
before: z4.string().nullable().optional(),
|
|
362
|
-
/** The value after; null when the entry removes the target. */
|
|
363
|
-
after: z4.string().nullable().optional(),
|
|
364
|
-
/** Why an entry is flagged or skipped, in a sentence a person can act on. */
|
|
365
|
-
reason: z4.string().optional(),
|
|
366
|
-
/** Machine code for the reason: `alias-missing`, `path-collision`, … */
|
|
367
|
-
code: z4.string().optional()
|
|
368
|
-
});
|
|
369
|
-
var ChangePlanCountsSchema = z4.object({
|
|
370
|
-
create: z4.number().int().nonnegative(),
|
|
371
|
-
update: z4.number().int().nonnegative(),
|
|
372
|
-
skip: z4.number().int().nonnegative(),
|
|
373
|
-
flag: z4.number().int().nonnegative()
|
|
374
|
-
});
|
|
375
|
-
var ChangePlanSchema = z4.object({
|
|
376
|
-
/** True when nothing was written; the entries say what would be. */
|
|
377
|
-
dryRun: z4.boolean(),
|
|
378
|
-
entries: z4.array(ChangePlanEntrySchema),
|
|
379
|
-
counts: ChangePlanCountsSchema
|
|
348
|
+
codebase: GenomeCodebaseSchema.optional(),
|
|
349
|
+
/**
|
|
350
|
+
* Local or public preview URL for `capture_project_shell` (Storybook,
|
|
351
|
+
* Vite preview, etc.). Capture runs on the developer machine, so
|
|
352
|
+
* localhost is fine — Flash’s servers never fetch this URL.
|
|
353
|
+
*/
|
|
354
|
+
previewUrl: z3.string().url().optional()
|
|
380
355
|
});
|
|
381
|
-
function countChangePlan(entries) {
|
|
382
|
-
const counts = { create: 0, update: 0, skip: 0, flag: 0 };
|
|
383
|
-
for (const entry of entries)
|
|
384
|
-
counts[entry.action] += 1;
|
|
385
|
-
return counts;
|
|
386
|
-
}
|
|
387
|
-
function makeChangePlan(entries, dryRun) {
|
|
388
|
-
return { dryRun, entries: [...entries], counts: countChangePlan(entries) };
|
|
389
|
-
}
|
|
390
|
-
function tokenChangeToPlanEntry(change) {
|
|
391
|
-
const base = {
|
|
392
|
-
targetKind: "token",
|
|
393
|
-
path: change.name,
|
|
394
|
-
before: change.before ?? null,
|
|
395
|
-
after: change.after ?? null
|
|
396
|
-
};
|
|
397
|
-
switch (change.kind) {
|
|
398
|
-
case "added":
|
|
399
|
-
return { ...base, action: "create" };
|
|
400
|
-
case "changed":
|
|
401
|
-
return { ...base, action: "update" };
|
|
402
|
-
case "removed":
|
|
403
|
-
return {
|
|
404
|
-
...base,
|
|
405
|
-
action: "flag",
|
|
406
|
-
code: "removed",
|
|
407
|
-
reason: "Not in the incoming set; applying removes it from the system."
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// ../core/dist/graph.js
|
|
413
|
-
var FOUNDATION_ORDER = [
|
|
414
|
-
"colors",
|
|
415
|
-
"typography",
|
|
416
|
-
"spacing",
|
|
417
|
-
"radius",
|
|
418
|
-
"shadows",
|
|
419
|
-
"border",
|
|
420
|
-
"motion",
|
|
421
|
-
"effects",
|
|
422
|
-
"icons"
|
|
423
|
-
];
|
|
424
|
-
var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
425
|
-
var str = (v) => typeof v === "string" && v ? v : void 0;
|
|
426
|
-
var list = (v) => Array.isArray(v) ? v : [];
|
|
427
|
-
var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
428
|
-
function plural(n, one, many) {
|
|
429
|
-
return `${n} ${n === 1 ? one : many}`;
|
|
430
|
-
}
|
|
431
|
-
function tokensOf(doc) {
|
|
432
|
-
const out = /* @__PURE__ */ new Map();
|
|
433
|
-
const foundations = isObject(doc.foundations) ? doc.foundations : {};
|
|
434
|
-
const lists = [
|
|
435
|
-
...FOUNDATION_ORDER.map((key) => list(foundations[key])),
|
|
436
|
-
list(doc.semantics)
|
|
437
|
-
];
|
|
438
|
-
for (const tokens of lists) {
|
|
439
|
-
for (const token of tokens) {
|
|
440
|
-
if (!isObject(token))
|
|
441
|
-
continue;
|
|
442
|
-
const key = str(token.id) ?? str(token.name);
|
|
443
|
-
if (key && !out.has(key))
|
|
444
|
-
out.set(key, token);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return out;
|
|
448
|
-
}
|
|
449
|
-
function resolver(tokens) {
|
|
450
|
-
const byName = /* @__PURE__ */ new Map();
|
|
451
|
-
const byLower = /* @__PURE__ */ new Map();
|
|
452
|
-
for (const [key, token] of tokens) {
|
|
453
|
-
const name = str(token.name);
|
|
454
|
-
if (name && !byName.has(name))
|
|
455
|
-
byName.set(name, key);
|
|
456
|
-
for (const candidate of [key, name]) {
|
|
457
|
-
if (candidate && !byLower.has(candidate.toLowerCase())) {
|
|
458
|
-
byLower.set(candidate.toLowerCase(), key);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return (ref) => {
|
|
463
|
-
const wanted = str(ref);
|
|
464
|
-
if (!wanted)
|
|
465
|
-
return void 0;
|
|
466
|
-
if (tokens.has(wanted))
|
|
467
|
-
return wanted;
|
|
468
|
-
return byName.get(wanted) ?? byLower.get(wanted.toLowerCase());
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
function edgesOf(doc, tokens, resolve2) {
|
|
472
|
-
const seen = /* @__PURE__ */ new Set();
|
|
473
|
-
const edges = [];
|
|
474
|
-
const add = (source, target, type = "alias_of") => {
|
|
475
|
-
if (!source || !target || source === target)
|
|
476
|
-
return;
|
|
477
|
-
const key = `${source}\0${target}`;
|
|
478
|
-
if (seen.has(key))
|
|
479
|
-
return;
|
|
480
|
-
seen.add(key);
|
|
481
|
-
edges.push({ source, target, type });
|
|
482
|
-
};
|
|
483
|
-
for (const relation of list(doc.relations?.tokens)) {
|
|
484
|
-
if (!isObject(relation))
|
|
485
|
-
continue;
|
|
486
|
-
add(resolve2(relation.source), resolve2(relation.target), str(relation.type) ?? "alias_of");
|
|
487
|
-
}
|
|
488
|
-
for (const [key, token] of tokens) {
|
|
489
|
-
add(key, resolve2(token.referencedTokenId));
|
|
490
|
-
}
|
|
491
|
-
return edges.sort((a, b) => a.source !== b.source ? a.source < b.source ? -1 : 1 : a.target < b.target ? -1 : a.target > b.target ? 1 : 0);
|
|
492
|
-
}
|
|
493
|
-
function bindingsOf(owner) {
|
|
494
|
-
const out = [];
|
|
495
|
-
for (const binding of list(owner.bindings)) {
|
|
496
|
-
if (!isObject(binding))
|
|
497
|
-
continue;
|
|
498
|
-
const slot = str(binding.slot);
|
|
499
|
-
const tokenId = str(binding.tokenId);
|
|
500
|
-
if (!slot || !tokenId)
|
|
501
|
-
continue;
|
|
502
|
-
const entry = { slot, tokenId };
|
|
503
|
-
const variantId = str(binding.variantId);
|
|
504
|
-
const stateId = str(binding.stateId);
|
|
505
|
-
if (variantId)
|
|
506
|
-
entry.variantId = variantId;
|
|
507
|
-
if (stateId)
|
|
508
|
-
entry.stateId = stateId;
|
|
509
|
-
out.push(entry);
|
|
510
|
-
}
|
|
511
|
-
return out;
|
|
512
|
-
}
|
|
513
|
-
function ownersOf(items) {
|
|
514
|
-
return items.filter((item) => isObject(item) && !!str(item.id) && !!str(item.name));
|
|
515
|
-
}
|
|
516
|
-
function tokenSummary(key, token) {
|
|
517
|
-
const out = { id: key, name: str(token.name) ?? key };
|
|
518
|
-
const layer = str(token.layer);
|
|
519
|
-
const category = str(token.category);
|
|
520
|
-
if (layer)
|
|
521
|
-
out.layer = layer;
|
|
522
|
-
if (category)
|
|
523
|
-
out.category = category;
|
|
524
|
-
return out;
|
|
525
|
-
}
|
|
526
|
-
function listModules(doc) {
|
|
527
|
-
const components = new Map(ownersOf(list(doc.components)).map((c) => [str(c.id), str(c.name)]));
|
|
528
|
-
return ownersOf(list(doc.modules)).map((module) => {
|
|
529
|
-
const used = [];
|
|
530
|
-
const seen = /* @__PURE__ */ new Set();
|
|
531
|
-
const parts = list(module.parts).filter(isObject);
|
|
532
|
-
for (const part of parts) {
|
|
533
|
-
const id = str(part.componentId);
|
|
534
|
-
if (!id || seen.has(id))
|
|
535
|
-
continue;
|
|
536
|
-
seen.add(id);
|
|
537
|
-
used.push({ id, name: components.get(id) ?? null });
|
|
538
|
-
}
|
|
539
|
-
return {
|
|
540
|
-
id: str(module.id),
|
|
541
|
-
name: str(module.name),
|
|
542
|
-
parts: parts.length,
|
|
543
|
-
components: used,
|
|
544
|
-
bindings: bindingsOf(module).length
|
|
545
|
-
};
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
function getModule(doc, ref) {
|
|
549
|
-
const modules = ownersOf(list(doc.modules));
|
|
550
|
-
const wanted = ref.trim().toLowerCase();
|
|
551
|
-
const module = modules.find((m) => str(m.id) === ref) ?? modules.find((m) => str(m.name) === ref) ?? modules.find((m) => str(m.id).toLowerCase() === wanted || str(m.name).toLowerCase() === wanted);
|
|
552
|
-
if (!module) {
|
|
553
|
-
return {
|
|
554
|
-
found: false,
|
|
555
|
-
available: modules.map((m) => str(m.name))
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
const components = new Map(ownersOf(list(doc.components)).map((c) => [str(c.id), str(c.name)]));
|
|
559
|
-
const parts = list(module.parts).filter(isObject).flatMap((part) => {
|
|
560
|
-
const slotId = str(part.slotId);
|
|
561
|
-
const componentId = str(part.componentId);
|
|
562
|
-
if (!slotId || !componentId)
|
|
563
|
-
return [];
|
|
564
|
-
const entry = {
|
|
565
|
-
slotId,
|
|
566
|
-
componentId,
|
|
567
|
-
componentName: components.get(componentId) ?? null
|
|
568
|
-
};
|
|
569
|
-
const variantId = str(part.variantId);
|
|
570
|
-
if (variantId)
|
|
571
|
-
entry.variantId = variantId;
|
|
572
|
-
return [entry];
|
|
573
|
-
});
|
|
574
|
-
return {
|
|
575
|
-
found: true,
|
|
576
|
-
module: {
|
|
577
|
-
id: str(module.id),
|
|
578
|
-
name: str(module.name),
|
|
579
|
-
parts,
|
|
580
|
-
bindings: bindingsOf(module)
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
function getRelations(doc, ref) {
|
|
585
|
-
const tokens = tokensOf(doc);
|
|
586
|
-
const resolve2 = resolver(tokens);
|
|
587
|
-
const edges = edgesOf(doc, tokens, resolve2);
|
|
588
|
-
const components = ownersOf(list(doc.components));
|
|
589
|
-
const modules = ownersOf(list(doc.modules));
|
|
590
|
-
if (ref === void 0 || ref === "") {
|
|
591
|
-
const bindings = [];
|
|
592
|
-
for (const [kind, owners2] of [
|
|
593
|
-
["component", components],
|
|
594
|
-
["module", modules]
|
|
595
|
-
]) {
|
|
596
|
-
for (const owner of owners2) {
|
|
597
|
-
for (const binding of bindingsOf(owner)) {
|
|
598
|
-
bindings.push({ owner: kind, ownerId: str(owner.id), ...binding });
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
const parts = [];
|
|
603
|
-
for (const module of modules) {
|
|
604
|
-
for (const part of list(module.parts).filter(isObject)) {
|
|
605
|
-
const slotId = str(part.slotId);
|
|
606
|
-
const componentId = str(part.componentId);
|
|
607
|
-
if (!slotId || !componentId)
|
|
608
|
-
continue;
|
|
609
|
-
parts.push({ moduleId: str(module.id), slotId, componentId });
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
return { tokens: edges, bindings, parts };
|
|
613
|
-
}
|
|
614
|
-
const key = resolve2(ref);
|
|
615
|
-
if (!key)
|
|
616
|
-
return { found: false, token: ref };
|
|
617
|
-
const boundHere = (owner) => bindingsOf(owner).filter((b) => resolve2(b.tokenId) === key);
|
|
618
|
-
const owners = (items) => items.map((owner) => ({
|
|
619
|
-
id: str(owner.id),
|
|
620
|
-
name: str(owner.name),
|
|
621
|
-
bindings: boundHere(owner)
|
|
622
|
-
})).filter((o) => o.bindings.length).sort(byId);
|
|
623
|
-
return {
|
|
624
|
-
found: true,
|
|
625
|
-
token: tokenSummary(key, tokens.get(key)),
|
|
626
|
-
aliasOf: edges.filter((e) => e.source === key).map((e) => ({ id: e.target, type: e.type })),
|
|
627
|
-
aliasedBy: edges.filter((e) => e.target === key).map((e) => ({ id: e.source, type: e.type })),
|
|
628
|
-
components: owners(components),
|
|
629
|
-
modules: owners(modules)
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
function getImpact(doc, ref) {
|
|
633
|
-
const tokens = tokensOf(doc);
|
|
634
|
-
const resolve2 = resolver(tokens);
|
|
635
|
-
const root = resolve2(ref);
|
|
636
|
-
if (!root)
|
|
637
|
-
return { found: false, token: ref };
|
|
638
|
-
const edges = edgesOf(doc, tokens, resolve2);
|
|
639
|
-
const pointedAtBy = /* @__PURE__ */ new Map();
|
|
640
|
-
for (const edge of edges) {
|
|
641
|
-
const sources = pointedAtBy.get(edge.target) ?? [];
|
|
642
|
-
sources.push(edge.source);
|
|
643
|
-
pointedAtBy.set(edge.target, sources);
|
|
644
|
-
}
|
|
645
|
-
const reached = /* @__PURE__ */ new Map();
|
|
646
|
-
let frontier = [root];
|
|
647
|
-
let depth = 0;
|
|
648
|
-
while (frontier.length) {
|
|
649
|
-
depth += 1;
|
|
650
|
-
const next = [];
|
|
651
|
-
for (const target of frontier) {
|
|
652
|
-
for (const source of (pointedAtBy.get(target) ?? []).slice().sort()) {
|
|
653
|
-
if (source === root || reached.has(source))
|
|
654
|
-
continue;
|
|
655
|
-
reached.set(source, { depth, via: target });
|
|
656
|
-
next.push(source);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
frontier = next.sort();
|
|
660
|
-
}
|
|
661
|
-
const affected = /* @__PURE__ */ new Set([root, ...reached.keys()]);
|
|
662
|
-
const hits = (owner) => bindingsOf(owner).filter((b) => {
|
|
663
|
-
const key = resolve2(b.tokenId);
|
|
664
|
-
return key !== void 0 && affected.has(key);
|
|
665
|
-
});
|
|
666
|
-
const components = ownersOf(list(doc.components)).map((c) => ({ id: str(c.id), name: str(c.name), bindings: hits(c) })).filter((c) => c.bindings.length).sort(byId);
|
|
667
|
-
const affectedComponents = new Set(components.map((c) => c.id));
|
|
668
|
-
const modules = ownersOf(list(doc.modules)).map((m) => {
|
|
669
|
-
const reasons = hits(m).map((b) => ({ kind: "binding", ...b }));
|
|
670
|
-
for (const part of list(m.parts).filter(isObject)) {
|
|
671
|
-
const componentId = str(part.componentId);
|
|
672
|
-
const slotId = str(part.slotId);
|
|
673
|
-
if (componentId && slotId && affectedComponents.has(componentId)) {
|
|
674
|
-
reasons.push({ kind: "part", slotId, componentId });
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
return { id: str(m.id), name: str(m.name), reasons };
|
|
678
|
-
}).filter((m) => m.reasons.length).sort(byId);
|
|
679
|
-
const reachedTokens = [...reached.entries()].map(([id, r]) => ({
|
|
680
|
-
...tokenSummary(id, tokens.get(id)),
|
|
681
|
-
depth: r.depth,
|
|
682
|
-
via: r.via
|
|
683
|
-
})).sort((a, b) => a.depth !== b.depth ? a.depth - b.depth : byId(a, b));
|
|
684
|
-
return {
|
|
685
|
-
found: true,
|
|
686
|
-
token: tokenSummary(root, tokens.get(root)),
|
|
687
|
-
tokens: reachedTokens,
|
|
688
|
-
components,
|
|
689
|
-
modules,
|
|
690
|
-
summary: `Changing ${root} reaches ${plural(reachedTokens.length, "token", "tokens")}, ${plural(components.length, "component", "components")} and ${plural(modules.length, "module", "modules")}.`
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
356
|
|
|
694
357
|
// src/load-tokens.ts
|
|
695
358
|
import { readFile } from "fs/promises";
|
|
@@ -833,15 +496,9 @@ export {
|
|
|
833
496
|
TokenCategorySchema,
|
|
834
497
|
TokenSchema,
|
|
835
498
|
parseFlashAtomicDocument,
|
|
836
|
-
makeChangePlan,
|
|
837
|
-
tokenChangeToPlanEntry,
|
|
838
499
|
GENOME_CONFIG_FILENAME,
|
|
839
500
|
LEGACY_CONFIG_FILENAME,
|
|
840
501
|
GenomeConfigSchema,
|
|
841
|
-
listModules,
|
|
842
|
-
getModule,
|
|
843
|
-
getRelations,
|
|
844
|
-
getImpact,
|
|
845
502
|
normalizeTokenDocument,
|
|
846
503
|
configCandidates,
|
|
847
504
|
resolveTokenFilePath,
|