@design-twin/dtwin 0.0.0-stage → 0.1.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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +959 -2
  3. package/figma-plugin/code.js +4207 -0
  4. package/figma-plugin/manifest.json +28 -0
  5. package/figma-plugin/ui.html +686 -0
  6. package/layer/cross-check.js +2746 -0
  7. package/layer/drift-lint.js +1526 -0
  8. package/layer/get-component.js +553 -0
  9. package/layer/map-bootstrap.js +1404 -0
  10. package/layer/map-validate.js +397 -0
  11. package/package.json +53 -4
  12. package/src/abort-reason.ts +12 -0
  13. package/src/asset-compare.ts +175 -0
  14. package/src/asset-owners.ts +280 -0
  15. package/src/atomic-write.ts +79 -0
  16. package/src/bridge-holder.ts +294 -0
  17. package/src/cli-commands.ts +427 -0
  18. package/src/client-presence.ts +146 -0
  19. package/src/clock.ts +45 -0
  20. package/src/commands.ts +561 -0
  21. package/src/daemon-auth.ts +174 -0
  22. package/src/daemon.ts +788 -0
  23. package/src/delivery.ts +85 -0
  24. package/src/design-system-layout.ts +232 -0
  25. package/src/doc-types.ts +1054 -0
  26. package/src/doctor.ts +602 -0
  27. package/src/errmsg.ts +14 -0
  28. package/src/export-reader.ts +409 -0
  29. package/src/export-verdict.ts +258 -0
  30. package/src/figma-mcp.ts +942 -0
  31. package/src/figma-pull.ts +1265 -0
  32. package/src/file-lock.ts +268 -0
  33. package/src/file-memo.ts +62 -0
  34. package/src/hash.ts +6 -0
  35. package/src/hex-color.ts +39 -0
  36. package/src/human-ms.ts +7 -0
  37. package/src/init.ts +424 -0
  38. package/src/instance-overrides.ts +7 -0
  39. package/src/json-file.ts +79 -0
  40. package/src/json-size.ts +108 -0
  41. package/src/json-util.ts +51 -0
  42. package/src/library-layout.ts +193 -0
  43. package/src/node-id.ts +69 -0
  44. package/src/pages-layout.ts +483 -0
  45. package/src/pending-requests.ts +187 -0
  46. package/src/plugin-frames.ts +32 -0
  47. package/src/png.ts +196 -0
  48. package/src/ports.ts +25 -0
  49. package/src/project-layout.ts +189 -0
  50. package/src/quick-keys.ts +68 -0
  51. package/src/read-json.ts +67 -0
  52. package/src/read-opts.ts +125 -0
  53. package/src/read-plan.ts +105 -0
  54. package/src/read-run.ts +119 -0
  55. package/src/route.ts +243 -0
  56. package/src/seed-components.ts +229 -0
  57. package/src/served-session.ts +347 -0
  58. package/src/server-core.ts +1093 -0
  59. package/src/settings.ts +156 -0
  60. package/src/source-stamp.ts +16 -0
  61. package/src/staleness.ts +63 -0
  62. package/src/stdio.ts +156 -0
  63. package/src/svg-normalize.ts +205 -0
  64. package/src/timeouts.ts +76 -0
  65. package/src/timings.ts +118 -0
  66. package/src/token-store.ts +261 -0
  67. package/src/variables-merge.ts +337 -0
  68. package/src/verbs.ts +140 -0
  69. package/src/write-out.ts +1255 -0
@@ -0,0 +1,85 @@
1
+ // delivery.ts — how an MCP export tool hands its read back: inline, written to disk, or refused; and, for a
2
+ // write, the note that says what it replaced or kept.
3
+ //
4
+ // The rules:
5
+ // - writeToDisk:true is an explicit write, and the reply is never measured (a real export is tens of MB);
6
+ // - otherwise the reply comes back inline when it fits the client's limit (write-out.ts inlineLimitChars);
7
+ // - one that does not fit is written to disk on its own (an implicit spill, which keeps `.prev` copies of what it
8
+ // replaces) — unless writeToDisk:false was passed, which is a refusal naming the size: an inline result past the
9
+ // limit is not delivered whole, it is cut off mid-JSON;
10
+ // - the note for a write, from write-out's change report (the .prev files it kept, the layer files it replaced, the
11
+ // .prev files an earlier spill left on disk): only an implicit spill says any of it. An explicit write overwriting
12
+ // its own files is the point of the write, so it keeps nothing and says nothing about earlier copies.
13
+ //
14
+ // Pure: no filesystem, no clock. What was on disk comes in as the change report; figma-mcp.ts does the writing.
15
+ import path from "node:path";
16
+ import type { ChangeReport } from "./write-out.ts";
17
+
18
+ export type Delivery =
19
+ | { kind: "inline"; text: string }
20
+ | { kind: "write"; implicit: false }
21
+ /** `why`: the size the reply was past, for the note. */
22
+ | { kind: "write"; implicit: true; why: string }
23
+ | { kind: "refuse"; message: string };
24
+ export type WriteDelivery = Extract<Delivery, { kind: "write" }>;
25
+
26
+ /** `measure(limit)`: the inline text when it is at most `limit` characters, else null. Called only when inline is an option. */
27
+ export function decideDelivery(writeToDisk: boolean | undefined, measure: (limit: number) => string | null, limit: number): Delivery {
28
+ if (writeToDisk === true) return { kind: "write", implicit: false };
29
+ const text = measure(limit);
30
+ if (text !== null) return { kind: "inline", text };
31
+ const why = `this export is past the ${limit.toLocaleString("en-US")} characters (~${Math.round(limit / 4000)}k tokens) the client accepts inline (Claude Code keeps larger results out of the conversation, and MAX_MCP_OUTPUT_TOKENS can only lower this)`;
32
+ if (writeToDisk === false) return { kind: "refuse", message: `${why}, and writeToDisk:false was passed. Omit it (or pass true) to get the files plus a compact index, or narrow the scope (a node id instead of a page, a lower depth).` };
33
+ return { kind: "write", implicit: true, why };
34
+ }
35
+
36
+ /** What a write produced, as the note needs it: a screen (its JSON), a screenshot (its PNG, relative to `dir`), or a
37
+ * catalog with or without a page walk. `changes`: write-out's report (absent = nothing kept, replaced or earlier). */
38
+ export type Written =
39
+ | { kind: "screen"; dir: string; screen: string; changes?: ChangeReport | undefined }
40
+ | { kind: "screenshot"; dir: string; reference: string; changes?: ChangeReport | undefined }
41
+ | { kind: "export"; dir: string; changes?: ChangeReport | undefined };
42
+
43
+ export interface WriteNote {
44
+ note: string;
45
+ /** The file the write is about: the screen JSON, the PNG, or the export directory. */
46
+ file: string;
47
+ /** A screen spill's .prev: the one it kept, else the one an earlier spill left. */
48
+ prev?: string;
49
+ /** An implicit full/design-system spill: how many .prev copies it kept (0 when none differed). */
50
+ prevKept?: number;
51
+ }
52
+
53
+ const WRITTEN = "Export written to disk; node payloads intentionally omitted from this result. Read the files under outDir (start with the index) to inspect them at your own granularity.";
54
+ const ONE_LEVEL = "(one level — the next implicit spill replaces it)";
55
+
56
+ export function describeWrite(d: WriteDelivery, w: Written): WriteNote {
57
+ const file = w.kind === "screen" ? w.screen : w.kind === "screenshot" ? path.join(w.dir, w.reference) : w.dir;
58
+ if (!d.implicit) return { note: WRITTEN, file };
59
+ const spilled = `Written to disk WITHOUT being asked: ${d.why}, so returning it inline would have been cut off mid-JSON. `;
60
+ const c: ChangeReport = w.changes ?? { kept: [], replaced: [], earlier: [] };
61
+ // A screenshot keeps nothing: its PNG is never overwritten (write-out versions a differing one).
62
+ if (w.kind === "screenshot") return { note: spilled + WRITTEN, file };
63
+ if (w.kind === "screen") {
64
+ // writeScreen keeps ONLY the screen JSON's .prev — its siblings are replaced with no copy.
65
+ const kept = c.kept[0];
66
+ if (kept) return { note: spilled + `It replaced ${w.screen}; the previous version is kept at ${kept} ${ONE_LEVEL}. ` + WRITTEN, file, prev: kept };
67
+ // An unchanged design keeps no new .prev, but the one the first spill wrote still holds the user's original: point at
68
+ // it (an agent that never saw that first result has no other pointer to it).
69
+ const earlier = c.earlier[0];
70
+ if (earlier) return { note: spilled + `An earlier version is still at ${earlier} (this export matched what was on disk). ` + WRITTEN, file, prev: earlier };
71
+ return { note: spilled + WRITTEN, file };
72
+ }
73
+ const n = c.replaced.length;
74
+ const replaced = n ? `It replaced ${n} existing screen file(s) under ${path.join(w.dir, "pages")}` : "";
75
+ let changed = "";
76
+ if (c.kept.length) {
77
+ changed = `${replaced ? replaced + ". The" : "The"} previous version of each changed file — ${c.kept.length} file(s) under pages/ or design-system/ (or design-system.json), e.g. ${c.kept[0]} — is kept beside it as <file>.prev ${ONE_LEVEL}. `;
78
+ } else if (c.earlier.length) {
79
+ // Nothing new kept, but an earlier spill's .prev files still hold what was replaced then.
80
+ changed = `${replaced ? replaced + ". " : ""}Earlier versions are still kept as ${c.earlier.length} .prev file(s) beside their files (this export matched what was on disk). `;
81
+ } else if (replaced) {
82
+ changed = `${replaced} (none differed from what was on disk, so no .prev was kept). `;
83
+ }
84
+ return { note: spilled + changed + WRITTEN, file, prevKept: c.kept.length };
85
+ }
@@ -0,0 +1,232 @@
1
+ // The ONE definition of how the design-system catalog is split across files — the sibling of
2
+ // pages-layout.ts, and it exists for the same two reasons.
3
+ //
4
+ // 1) SIZE. A single flat design-system.json holding every variable, every style, every component and a
5
+ // lint report would be, on a real design-system file, the biggest document in the export after the
6
+ // layer tree, and an agent that only wants the token table would have to load the component
7
+ // catalog with it.
8
+ // 2) TAXONOMY. Those keys are not one thing. Figma's own model has three separate systems, verified
9
+ // against developers.figma.com:
10
+ // - Variables live in VariableCollections; a collection owns `modes` and `variableIds`, and every
11
+ // Variable defines a value per mode (resolvedType COLOR/FLOAT/STRING/BOOLEAN).
12
+ // -> tokens.json
13
+ // - Styles are the older, separate system: PaintStyle/TextStyle/EffectStyle/GridStyle and
14
+ // CustomAnimationStyle, each its own BaseStyle object with its own id/name/description.
15
+ // Variables can be BOUND into a style's fields (`boundVariables`), which is a reference between
16
+ // the two systems, not a merger.
17
+ // -> one styles.<kind>.json per style kind, STYLE_KIND_ROWS below (split the same way
18
+ // tokens/components/hygiene are, and for the same reason: a caller after only typography does
19
+ // not have to load every gradient and shadow in the file to get it)
20
+ // - Components/ComponentSets are a third concept, with variant properties of their own.
21
+ // -> components.local.json / components.library.json
22
+ // The component split is a correctness fix, not just tidiness: entries flagged `remote: true` are
23
+ // NOT nodes in this file. They are library mains recovered by walking instances (see libraries.ts),
24
+ // so they have no id/page/pageId and their props may be INFERRED from the sample of instances
25
+ // present. Mixing them into one array with real local components meant every consumer had to know
26
+ // that half the rows are a different kind of thing. Now the filename says it.
27
+ // 3) hygiene is a lint-style report ABOUT the data, not data — it gets its own file.
28
+ //
29
+ // design-system.json stays, slim: the stamp (`exportedAt`/`file`/`colorProfile`) plus a `files`
30
+ // pointer map, so an existing consumer that opens design-system.json still finds its way. The stamp is
31
+ // repeated into every split file on purpose — export-verdict.ts and design-to-code/drift-lint.ts read
32
+ // `exportedAt` off whichever file they are handed, and design-to-code/tokens.ts, map-bootstrap.ts and
33
+ // drift-lint.ts are now pointed at tokens.json / components.local.json directly.
34
+ //
35
+ // Kept dependency-free (no Node imports, ES2019-safe) for the same reason as pages-layout.ts: the
36
+ // bridge CLI imports it and esbuild inlines it into the plugin bundle, so the disk writer and the
37
+ // browser-download writer build the identical set of files.
38
+
39
+ // The parts live in a design-system/ SUBDIRECTORY, not next to design-system.json, because two
40
+ // of these names are already taken at the export root by files the user HAND-AUTHORS and cannot
41
+ // regenerate: design/tokens.json (Figma variable -> your code token) and design/components.json
42
+ // (Figma component -> your code component). A generated tokens.json at the root would overwrite the
43
+ // styling source of truth on every pull. The subdirectory keeps the taxonomy names AND that boundary:
44
+ // design/ root = your config + screens, design/design-system/ = the regenerable catalog.
45
+ import { safe } from "./pages-layout.ts"; // same filesystem-boundary sanitiser pages-layout.ts uses for layer files
46
+ import type { PageLayoutFile } from "./pages-layout.ts";
47
+ import { ifDefined } from "./json-util.ts";
48
+ import type {
49
+ DesignSystemStamp, DesignSystemManifest, TokensDoc, StylesDoc, AnyStyle, StyleKind, StyleKey, StyleKeyed, HygieneDoc, CatalogVariant,
50
+ CatalogComponent, ComponentsCatalog, ComponentDetailFile, DesignSystemStyles, DesignSystemDoc,
51
+ } from "./doc-types.ts";
52
+
53
+ /** Every document the split writes, in `files` order: tokens, one style file per kind, the two
54
+ * component catalogs, hygiene, any per-component detail files, and the slim manifest (last). */
55
+ export type DesignSystemFileData =
56
+ | TokensDoc | StylesDoc | ComponentsCatalog | HygieneDoc | ComponentDetailFile | DesignSystemManifest;
57
+
58
+ export interface DesignSystemLayout {
59
+ /** Every file to write, INCLUDING the slim manifest (last). */
60
+ files: Array<PageLayoutFile<DesignSystemFileData>>;
61
+ /** The slim manifest alone: stamp + `files` pointer map + `counts`. */
62
+ manifest: DesignSystemManifest;
63
+ counts: DesignSystemManifest["counts"];
64
+ /** The subdirectory the split parts live under ("design-system"). */
65
+ dir: string;
66
+ }
67
+
68
+ const DIR = "design-system";
69
+ const COMPONENTS_DIR = "components"; // sibling subdir of design-system/, holds one detail file per COMPONENT_SET
70
+ const TOKENS = "tokens.json";
71
+ const COMPONENTS_LOCAL = "components.local.json";
72
+ const COMPONENTS_LIBRARY = "components.library.json";
73
+ const HYGIENE = "hygiene.json";
74
+ const MANIFEST = "design-system.json";
75
+
76
+ /** One style kind as the split files it: the plugin's `styles.<kind>` list lands in `file`, which the
77
+ * manifest points at, and counts, under `key`. Both are typed from the kind, so a row cannot drift. */
78
+ export type StyleKindRow = { [K in StyleKind]: { kind: K; file: `styles.${K}.json`; key: StyleKey<K> } }[StyleKind];
79
+ /** The style kinds, one row each — the ONE place a kind's file and manifest key are named. Both layouts
80
+ * (this one and library-layout.ts) and design-diff loop over it; a kind in doc-types.ts StyleByKind with
81
+ * no row here does not compile. The order is the order of the files and of their `files`/`counts` keys. */
82
+ export const STYLE_KIND_ROWS: { readonly [K in StyleKind]: Extract<StyleKindRow, { kind: K }> } = {
83
+ paint: { kind: "paint", file: "styles.paint.json", key: "stylesPaint" },
84
+ text: { kind: "text", file: "styles.text.json", key: "stylesText" },
85
+ effect: { kind: "effect", file: "styles.effect.json", key: "stylesEffect" },
86
+ grid: { kind: "grid", file: "styles.grid.json", key: "stylesGrid" },
87
+ animation: { kind: "animation", file: "styles.animation.json", key: "stylesAnimation" }, // local custom animation styles
88
+ };
89
+ export const STYLE_KINDS: readonly StyleKindRow[] = Object.values(STYLE_KIND_ROWS);
90
+
91
+ /** A kind's styles from the plugin's styles block — `[]` when the block has no list for it. */
92
+ export function stylesOf(styles: DesignSystemStyles | null | undefined, kind: StyleKind): AnyStyle[] {
93
+ const list = styles ? styles[kind] : undefined;
94
+ return Array.isArray(list) ? list : [];
95
+ }
96
+ /** One value per style kind under its manifest key, in STYLE_KINDS order — what a manifest's `files`
97
+ * and `counts` spread between their other keys. */
98
+ export function byStyleKey<V>(value: (row: StyleKindRow) => V): StyleKeyed<V> {
99
+ const out: Partial<StyleKeyed<V>> = {};
100
+ for (const row of STYLE_KINDS) out[row.key] = value(row);
101
+ return out as StyleKeyed<V>; // every key set: the loop walks every kind
102
+ }
103
+
104
+ // A library entry is one the plugin flagged `remote: true` (components.ts sets it for a consumed
105
+ // library main). The task's other signal — "has id/page/pageId" — is the same partition seen from the
106
+ // local side; `remote` is the flag the producer actually writes, so that is what we key on, and
107
+ // anything without it is treated as local.
108
+ export function isLibraryEntry(c: { remote?: unknown } | null | undefined): boolean {
109
+ return !!(c && c.remote === true);
110
+ }
111
+
112
+ /**
113
+ * @param ds the plugin's designSystem doc
114
+ * @param sep path separator: "/" for real directories, "__" for flat download names — the
115
+ * same single knob pages-layout.ts has, and for the same reason (a browser
116
+ * download cannot create directories).
117
+ * @returns `files` is every file to write INCLUDING the slim manifest (last).
118
+ */
119
+ export function buildDesignSystemLayout(ds: DesignSystemDoc | null | undefined, sep: string): DesignSystemLayout {
120
+ const d: DesignSystemDoc = ds || {};
121
+ const join = (name: string): string => DIR + (sep || "/") + name;
122
+ // The stamp every split file carries. Written through byte for byte — nothing here re-derives it.
123
+ const stamp: DesignSystemStamp = { ...ifDefined("exportedAt", d.exportedAt), ...ifDefined("file", d.file), ...ifDefined("colorProfile", d.colorProfile) };
124
+ const components = Array.isArray(d.components) ? d.components : [];
125
+ const rawLocal = components.filter((c) => !isLibraryEntry(c));
126
+ const library = components.filter(isLibraryEntry);
127
+ const hygiene = Array.isArray(d.hygiene) ? d.hygiene : [];
128
+
129
+ // The heavy part of a COMPONENT_SET entry — variants[].node, a full serialized node tree per variant
130
+ // (opt-in via the variantVisuals read in components.ts) — is what makes components.local.json huge on
131
+ // a real design-system file. Neither drift-lint.ts nor map-bootstrap.ts ever reads `.node` (both key
132
+ // off name/id/key/type/props), so it is safe to split out unread. Everything else on the entry,
133
+ // INCLUDING each variant's id/name/key/values, stays in the catalog byte for byte.
134
+ const usedNames = new Set<string>();
135
+ const uniqueDetailName = (name: string | undefined, id: string | undefined): string => {
136
+ const base = safe(name || "component") + "__" + safe(id);
137
+ if (!usedNames.has(base)) { usedNames.add(base); return base; }
138
+ let i = 2;
139
+ while (usedNames.has(base + "_" + i)) i++;
140
+ usedNames.add(base + "_" + i);
141
+ return base + "_" + i;
142
+ };
143
+ const componentFiles: Array<PageLayoutFile<ComponentDetailFile>> = [];
144
+ const local = rawLocal.map((c): CatalogComponent => {
145
+ // A standalone COMPONENT (variantVisuals also walks these now, components.ts) carries its own
146
+ // node tree directly on `.node` rather than under `.variants[].node` — same reason to split it out
147
+ // (unread by drift-lint.ts/map-bootstrap.ts, and it is the single biggest field on the entry), but
148
+ // mirrored as its own nodeFile pointer since there is no variants array to slim here.
149
+ if (c.type === "COMPONENT" && c.node) {
150
+ const { node, ...rest } = c;
151
+ const detailName = uniqueDetailName(c.name, c.id);
152
+ const detailPath = DIR + (sep || "/") + COMPONENTS_DIR + (sep || "/") + detailName + ".json";
153
+ componentFiles.push({
154
+ path: detailPath,
155
+ data: { ...stamp, ...ifDefined("id", c.id), ...ifDefined("key", c.key), name: c.name, node },
156
+ });
157
+ return { ...rest, nodeFile: detailPath };
158
+ }
159
+ if (c.type !== "COMPONENT_SET" || !Array.isArray(c.variants) || !c.variants.some((v) => v && v.node)) {
160
+ return c; // no exported node trees (variantVisuals was off, or nothing serialized) -> no detail file, no pointer
161
+ }
162
+ // Object.assign + delete is `const { node, ...rest } = v || {}` (same keys, same order) without
163
+ // destructuring a possibly-empty object.
164
+ const slimVariants = c.variants.map((v): CatalogVariant => {
165
+ const rest: CatalogVariant = Object.assign({}, v);
166
+ delete rest.node;
167
+ return rest;
168
+ });
169
+ const detailName = uniqueDetailName(c.name, c.id);
170
+ const detailPath = DIR + (sep || "/") + COMPONENTS_DIR + (sep || "/") + detailName + ".json";
171
+ componentFiles.push({
172
+ path: detailPath,
173
+ data: { ...stamp, ...ifDefined("setId", c.id), ...ifDefined("setKey", c.key), name: c.name, variants: c.variants },
174
+ });
175
+ return { ...c, variants: slimVariants, variantsFile: detailPath };
176
+ });
177
+ const files: Array<PageLayoutFile<DesignSystemFileData>> = [
178
+ { path: join(TOKENS), data: { ...stamp, ...ifDefined("collections", d.collections), ...ifDefined("variables", d.variables) } },
179
+ ...STYLE_KINDS.map((row) => ({ path: join(row.file), data: { ...stamp, styles: stylesOf(d.styles, row.kind) } })),
180
+ { path: join(COMPONENTS_LOCAL), data: { ...stamp, components: local } },
181
+ { path: join(COMPONENTS_LIBRARY), data: { ...stamp, components: library } },
182
+ { path: join(HYGIENE), data: { ...stamp, hygiene } },
183
+ ...componentFiles,
184
+ ];
185
+
186
+ const counts: DesignSystemManifest["counts"] = {
187
+ collections: (d.collections || []).length,
188
+ variables: (d.variables || []).length,
189
+ ...byStyleKey((row) => stylesOf(d.styles, row.kind).length),
190
+ components: local.length,
191
+ libraryComponents: library.length,
192
+ hygiene: hygiene.length,
193
+ };
194
+
195
+ // The pointer map carries the SAME relative paths the files actually landed under (separator and
196
+ // all), so a consumer joins them against the export dir and needs to know nothing else — including
197
+ // the download path, where the "directory" is really a "__" in the filename.
198
+ const manifest: DesignSystemManifest = {
199
+ ...stamp,
200
+ files: {
201
+ tokens: join(TOKENS),
202
+ ...byStyleKey((row) => join(row.file)),
203
+ componentsLocal: join(COMPONENTS_LOCAL),
204
+ componentsLibrary: join(COMPONENTS_LIBRARY),
205
+ hygiene: join(HYGIENE),
206
+ },
207
+ counts,
208
+ };
209
+ // componentsDir is a POINTER, and a pointer that resolves to nothing is worse than an absent key:
210
+ // a tool walking `files` verbatim gets ENOENT on the one entry that was never written. The
211
+ // directory only exists when a COMPONENT_SET actually produced a detail file (variantVisuals is
212
+ // opt-in), so the key only exists then too.
213
+ // Note the trailing separator: without it `design-system/components.local.json` — which is ALWAYS
214
+ // written — matches the prefix `design-system/components` and the key never gets omitted.
215
+ const detailPrefix = DIR + (sep || "/") + COMPONENTS_DIR + (sep || "/");
216
+ if (files.some((f) => String(f.path).startsWith(detailPrefix))) {
217
+ manifest.files.componentsDir = DIR + (sep || "/") + COMPONENTS_DIR;
218
+ }
219
+ files.push({ path: MANIFEST, data: manifest }); // the manifest itself stays at the export ROOT
220
+ return { files, manifest, counts, dir: DIR };
221
+ }
222
+
223
+ export const DESIGN_SYSTEM_DIR: string = DIR;
224
+
225
+ /** The file/dir names the split uses besides the style files (STYLE_KIND_ROWS) — the manifest's own name included (MANIFEST). */
226
+ export interface DesignSystemFileNames {
227
+ TOKENS: string; COMPONENTS_LOCAL: string; COMPONENTS_LIBRARY: string; COMPONENTS_DIR: string; HYGIENE: string; MANIFEST: string;
228
+ }
229
+ export const DESIGN_SYSTEM_FILES: DesignSystemFileNames = { TOKENS, COMPONENTS_LOCAL, COMPONENTS_LIBRARY, COMPONENTS_DIR, HYGIENE, MANIFEST };
230
+ /** Every file buildDesignSystemLayout always writes, in `files` order (the manifest, MANIFEST, last; the per-component
231
+ * detail files under COMPONENTS_DIR are not in it — they exist only under variantVisuals). */
232
+ export const DESIGN_SYSTEM_FILE_NAMES: readonly string[] = [TOKENS, ...STYLE_KINDS.map((row) => row.file), COMPONENTS_LOCAL, COMPONENTS_LIBRARY, HYGIENE, MANIFEST];