@nebutra/design-sync 0.1.1 → 2.0.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/LICENSE +21 -676
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +32 -3
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +4 -2
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +5 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/io.js +1 -1
- package/dist/providers/design-md.d.ts +49 -0
- package/dist/providers/design-md.d.ts.map +1 -0
- package/dist/providers/design-md.js +264 -0
- package/dist/serialize/from-design-md.d.ts +80 -0
- package/dist/serialize/from-design-md.d.ts.map +1 -0
- package/dist/serialize/from-design-md.js +1329 -0
- package/dist/serialize/to-brand-package.d.ts +37 -0
- package/dist/serialize/to-brand-package.d.ts.map +1 -0
- package/dist/serialize/to-brand-package.js +87 -0
- package/dist/serialize/to-design-md.d.ts +48 -0
- package/dist/serialize/to-design-md.d.ts.map +1 -0
- package/dist/serialize/to-design-md.js +114 -0
- package/dist/serialize/to-design-md.prose.d.ts +55 -0
- package/dist/serialize/to-design-md.prose.d.ts.map +1 -0
- package/dist/serialize/to-design-md.prose.js +143 -0
- package/dist/serialize/to-design-md.resolve.d.ts +36 -0
- package/dist/serialize/to-design-md.resolve.d.ts.map +1 -0
- package/dist/serialize/to-design-md.resolve.js +248 -0
- package/dist/serialize/to-preview-html.d.ts +48 -0
- package/dist/serialize/to-preview-html.d.ts.map +1 -0
- package/dist/serialize/to-preview-html.js +250 -0
- package/dist/serialize/to-preview-html.template.d.ts +75 -0
- package/dist/serialize/to-preview-html.template.d.ts.map +1 -0
- package/dist/serialize/to-preview-html.template.js +267 -0
- package/dist/types.d.ts +22 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +22 -6
- package/src/cli/index.ts +36 -3
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAgHA,wBAAsB,GAAG,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8ExE"}
|
package/dist/cli/index.js
CHANGED
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
// =============================================================================
|
|
17
17
|
import { describeEnv } from "../detect";
|
|
18
18
|
import { createDesignSync } from "../factory";
|
|
19
|
+
import { compileBrandFromTokenSets } from "../serialize/to-brand-package";
|
|
19
20
|
function parseArgs(argv) {
|
|
20
21
|
const [, , command = "help", ...rest] = argv;
|
|
21
22
|
let dryRun = false;
|
|
22
23
|
let json = false;
|
|
23
24
|
const themes = [];
|
|
24
25
|
let provider;
|
|
26
|
+
let brandId;
|
|
27
|
+
let brandName;
|
|
25
28
|
for (let i = 0; i < rest.length; i++) {
|
|
26
29
|
const arg = rest[i];
|
|
27
30
|
if (arg === "--dry-run")
|
|
@@ -46,8 +49,14 @@ function parseArgs(argv) {
|
|
|
46
49
|
i++;
|
|
47
50
|
}
|
|
48
51
|
}
|
|
52
|
+
else if (arg === "--id") {
|
|
53
|
+
brandId = rest[++i];
|
|
54
|
+
}
|
|
55
|
+
else if (arg === "--name") {
|
|
56
|
+
brandName = rest[++i];
|
|
57
|
+
}
|
|
49
58
|
}
|
|
50
|
-
return { command, dryRun, json, themes, provider };
|
|
59
|
+
return { command, dryRun, json, themes, provider, brandId, brandName };
|
|
51
60
|
}
|
|
52
61
|
function emit(payload, json) {
|
|
53
62
|
if (json) {
|
|
@@ -77,12 +86,15 @@ Commands:
|
|
|
77
86
|
detect Print the resolved provider + env diagnostics
|
|
78
87
|
healthcheck Run the provider's healthcheck()
|
|
79
88
|
pull Pull design-tool → repo (DTCG JSON)
|
|
80
|
-
push Push repo → design-tool (defaults to dry-run on figma/penpot)
|
|
89
|
+
push Push repo → design-tool (defaults to dry-run on figma/penpot/design-md)
|
|
90
|
+
brand Pull tokens then compile a Brand Package (Create Center)
|
|
81
91
|
help Show this message
|
|
82
92
|
|
|
83
93
|
Options:
|
|
84
|
-
--provider <X> Override provider (figma|penpot|git-only|memory)
|
|
94
|
+
--provider <X> Override provider (figma|penpot|git-only|memory|design-md)
|
|
85
95
|
--themes a,b Restrict to specific token sets
|
|
96
|
+
--id <id> Brand package id (for brand command)
|
|
97
|
+
--name <name> Brand package display name
|
|
86
98
|
--dry-run Force dry-run on push
|
|
87
99
|
--json Emit machine-readable JSON
|
|
88
100
|
`;
|
|
@@ -124,6 +136,23 @@ export async function run(argv = process.argv) {
|
|
|
124
136
|
: `${result.summary} (provider=${result.provider}, dryRun=${result.dryRun})`, args.json);
|
|
125
137
|
return 0;
|
|
126
138
|
}
|
|
139
|
+
if (args.command === "brand") {
|
|
140
|
+
const themes = args.themes.length > 0 ? { themes: args.themes } : {};
|
|
141
|
+
const pull = await provider.pull(themes);
|
|
142
|
+
const compiled = compileBrandFromTokenSets(pull.sets, {
|
|
143
|
+
...(args.brandId ? { id: args.brandId } : {}),
|
|
144
|
+
...(args.brandName ? { name: args.brandName } : {}),
|
|
145
|
+
});
|
|
146
|
+
emit(args.json
|
|
147
|
+
? {
|
|
148
|
+
provider: pull.provider,
|
|
149
|
+
brand: compiled.brand,
|
|
150
|
+
css: compiled.css,
|
|
151
|
+
warnings: compiled.warnings,
|
|
152
|
+
}
|
|
153
|
+
: `brand=${compiled.brand.id} recipe=${compiled.brand.recipe.buttonDefault} fonts=${compiled.brand.typography.faces?.length ?? 0} warnings=${compiled.warnings.length}`, args.json);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
127
156
|
emitError(`unknown command: ${args.command}\n\n${HELP_TEXT}`, args.json);
|
|
128
157
|
return 2;
|
|
129
158
|
}
|
package/dist/detect.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAuBtD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,sBAAsB,GAAG,SAAS,CAOpC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,sBAAsB,CAa3F;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG;IACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,sBAAsB,CAAC;CAClC,CA2BA"}
|
package/dist/detect.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
// 3. PENPOT_API_URL + PENPOT_TOKEN → "penpot"
|
|
8
8
|
// 4. fallback → "git-only"
|
|
9
9
|
//
|
|
10
|
-
// `memory`
|
|
11
|
-
// DESIGN_SYNC_PROVIDER env var or a config object
|
|
10
|
+
// `memory` and `design-md` are never auto-detected; they must be requested
|
|
11
|
+
// explicitly via the DESIGN_SYNC_PROVIDER env var or a config object.
|
|
12
12
|
// =============================================================================
|
|
13
13
|
const VALID_PROVIDERS = new Set([
|
|
14
14
|
"figma",
|
|
15
15
|
"penpot",
|
|
16
16
|
"git-only",
|
|
17
17
|
"memory",
|
|
18
|
+
"design-md",
|
|
18
19
|
]);
|
|
19
20
|
/**
|
|
20
21
|
* Read `DESIGN_SYNC_PROVIDER` from the environment, validate it, and return
|
|
@@ -64,6 +65,7 @@ export function describeEnv(env = process.env) {
|
|
|
64
65
|
"PENPOT_TOKEN",
|
|
65
66
|
"PENPOT_FILE_ID",
|
|
66
67
|
"PENPOT_TEAM_ID",
|
|
68
|
+
"DESIGN_MD_PATH",
|
|
67
69
|
];
|
|
68
70
|
for (const key of known) {
|
|
69
71
|
if (env[key]?.trim())
|
package/dist/factory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAA0B,MAAM,SAAS,CAAC;AAe5F;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC,
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAA0B,MAAM,SAAS,CAAC;AAe5F;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuC7F;AAED;;;GAGG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAKjE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAEhE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
|
package/dist/factory.js
CHANGED
|
@@ -61,6 +61,11 @@ export async function createDesignSync(config) {
|
|
|
61
61
|
const memConfig = config?.provider === "memory" ? config : undefined;
|
|
62
62
|
return new MemoryProvider(memConfig ?? { provider: "memory" });
|
|
63
63
|
}
|
|
64
|
+
case "design-md": {
|
|
65
|
+
const { DesignMdProvider } = await import("./providers/design-md");
|
|
66
|
+
const mdConfig = config?.provider === "design-md" ? config : undefined;
|
|
67
|
+
return new DesignMdProvider(mdConfig ?? { provider: "design-md" });
|
|
68
|
+
}
|
|
64
69
|
default:
|
|
65
70
|
throw new Error(`Unknown design-sync provider: ${providerType}`);
|
|
66
71
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,14 @@ export { createDesignSync, getDesignSync, resetDesignSync, setDesignSync, } from
|
|
|
3
3
|
export type { FigmaTokensStudioConfig } from "./figma-config/index";
|
|
4
4
|
export { FIGMA_TOKENS_STUDIO_CONFIG } from "./figma-config/index";
|
|
5
5
|
export { defaultTokensDir, defaultTokensStudioDir, readTokenSets, validateDtcgTree, writeTokenSet, } from "./io";
|
|
6
|
+
export type { DesignMdProvider } from "./providers/design-md";
|
|
6
7
|
export { FigmaProvider } from "./providers/figma";
|
|
7
8
|
export { GitOnlyProvider } from "./providers/git-only";
|
|
8
9
|
export { MemoryProvider } from "./providers/memory";
|
|
9
10
|
export { PenpotProvider } from "./providers/penpot";
|
|
10
|
-
export
|
|
11
|
+
export { compileBrandFromTokenSets, mergeTokenTrees, pullAndCompileBrand, serializeToBrandCss, type ToBrandPackageOptions, tokenSetsToReferoShape, } from "./serialize/to-brand-package";
|
|
12
|
+
export { serializeToDesignMd, type ToDesignMdOptions } from "./serialize/to-design-md";
|
|
13
|
+
export { serializeToPreviewHtml, type ToPreviewHtmlOptions, } from "./serialize/to-preview-html";
|
|
14
|
+
export type { BaseProviderConfig, DesignMdProviderConfig, DesignSyncConfig, DesignSyncProvider, DesignSyncProviderType, DesignTokenLeaf, DesignTokenSet, DesignTokenTree, FigmaProviderConfig, GitOnlyProviderConfig, HealthStatus, MemoryProviderConfig, PenpotProviderConfig, PullOptions, PullResult, PushOptions, PushResult, } from "./types";
|
|
11
15
|
export { DesignTokenLeafSchema } from "./types";
|
|
12
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAE/E,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAEpE,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAElE,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,MAAM,CAAC;AASd,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,EACL,yBAAyB,EACzB,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,sBAAsB,GACvB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAEvF,OAAO,EACL,sBAAsB,EACtB,KAAK,oBAAoB,GAC1B,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,sBAAsB,EACtB,eAAe,EACf,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,YAAY,EACZ,oBAAoB,EACpB,oBAAoB,EACpB,WAAW,EACX,UAAU,EACV,WAAW,EACX,UAAU,GACX,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// - Penpot (REST API, self-hostable, China-friendly)
|
|
7
7
|
// - git-only (zero-config default — DTCG files only)
|
|
8
8
|
// - memory (test fixture)
|
|
9
|
+
// - design-md (AI-native DESIGN.md, markdown + YAML front matter)
|
|
9
10
|
//
|
|
10
11
|
// Usage:
|
|
11
12
|
// import { getDesignSync } from "@nebutra/design-sync";
|
|
@@ -23,10 +24,15 @@ export { createDesignSync, getDesignSync, resetDesignSync, setDesignSync, } from
|
|
|
23
24
|
export { FIGMA_TOKENS_STUDIO_CONFIG } from "./figma-config/index";
|
|
24
25
|
// ── DTCG I/O helpers (re-exported for advanced consumers) ───────────────────
|
|
25
26
|
export { defaultTokensDir, defaultTokensStudioDir, readTokenSets, validateDtcgTree, writeTokenSet, } from "./io";
|
|
26
|
-
// ── Providers (tree-shakable direct imports) ────────────────────────────────
|
|
27
27
|
export { FigmaProvider } from "./providers/figma";
|
|
28
28
|
export { GitOnlyProvider } from "./providers/git-only";
|
|
29
29
|
export { MemoryProvider } from "./providers/memory";
|
|
30
30
|
export { PenpotProvider } from "./providers/penpot";
|
|
31
|
+
// ── DTCG → Brand Package (Create Center) ───────────────────────────────────
|
|
32
|
+
export { compileBrandFromTokenSets, mergeTokenTrees, pullAndCompileBrand, serializeToBrandCss, tokenSetsToReferoShape, } from "./serialize/to-brand-package";
|
|
33
|
+
// ── DTCG → DESIGN.md serializer ────────────────────────────────────────────
|
|
34
|
+
export { serializeToDesignMd } from "./serialize/to-design-md";
|
|
35
|
+
// ── DTCG → preview.html serializer ─────────────────────────────────────────
|
|
36
|
+
export { serializeToPreviewHtml, } from "./serialize/to-preview-html";
|
|
31
37
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
32
38
|
export { DesignTokenLeafSchema } from "./types";
|
package/dist/io.js
CHANGED
|
@@ -85,7 +85,7 @@ export async function readTokenSets(tokensDir) {
|
|
|
85
85
|
throw new Error(`[design-sync] DTCG file ${file} must contain an object at the root.`);
|
|
86
86
|
}
|
|
87
87
|
const relativePath = relative(tokensDir, file).split(sep).join("/");
|
|
88
|
-
const name = relativePath.replace(/\.json$/u, "")
|
|
88
|
+
const name = relativePath.replace(/\.json$/u, "");
|
|
89
89
|
sets.push({
|
|
90
90
|
name,
|
|
91
91
|
relativePath,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { LintReport } from "@google/design.md/linter";
|
|
2
|
+
import type { DesignMdProviderConfig, DesignSyncProvider, HealthStatus, PullOptions, PullResult, PushOptions, PushResult } from "../types";
|
|
3
|
+
export { type ImportReport, importFromDesignMd } from "../serialize/from-design-md";
|
|
4
|
+
/**
|
|
5
|
+
* Derive the preview file path from the DESIGN.md path.
|
|
6
|
+
*
|
|
7
|
+
* Replaces a trailing `.md` extension with `.preview.html`;
|
|
8
|
+
* if the path does not end in `.md`, appends `.preview.html`.
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* "/repo/DESIGN.md" → "/repo/DESIGN.preview.html"
|
|
12
|
+
* "/repo/design/tokens.md" → "/repo/design/tokens.preview.html"
|
|
13
|
+
* "/repo/DESIGN" → "/repo/DESIGN.preview.html"
|
|
14
|
+
*/
|
|
15
|
+
export declare function previewHtmlPathFor(mdPath: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Throw if the LintReport contains any error-severity findings.
|
|
18
|
+
* Warnings do not throw — they are surfaced in the push summary.
|
|
19
|
+
* Used as the fail-closed gate before writing a DESIGN.md.
|
|
20
|
+
*
|
|
21
|
+
* Fail-closed on either signal: error-severity findings in `findings[]`
|
|
22
|
+
* OR a non-zero `summary.errors` count.
|
|
23
|
+
*
|
|
24
|
+
* @param report - The LintReport returned by `lint(content)`.
|
|
25
|
+
* @param source - A human-readable label for the file (used in the error message).
|
|
26
|
+
* @throws When error-severity findings are present or `summary.errors > 0`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function assertLintClean(report: LintReport, source: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Dynamically imports the @google/design.md linter and runs it on `content`.
|
|
31
|
+
* Extracted as a named export so tests can verify the `[design-md]`-prefixed
|
|
32
|
+
* error thrown when the dynamic import or lint call itself fails.
|
|
33
|
+
*
|
|
34
|
+
* @throws When the linter import or invocation throws — error is wrapped with a
|
|
35
|
+
* `[design-md] Lint gate failed to run on <label>:` prefix.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runLintGate(content: string, label: string): Promise<LintReport>;
|
|
38
|
+
export declare class DesignMdProvider implements DesignSyncProvider {
|
|
39
|
+
readonly name: "design-md";
|
|
40
|
+
private readonly tokensDir;
|
|
41
|
+
private readonly designMdPath;
|
|
42
|
+
private readonly designSystemName;
|
|
43
|
+
private readonly designSystemDescription;
|
|
44
|
+
constructor(config: DesignMdProviderConfig);
|
|
45
|
+
pull(options?: PullOptions): Promise<PullResult>;
|
|
46
|
+
push(options?: PushOptions): Promise<PushResult>;
|
|
47
|
+
healthcheck(): Promise<HealthStatus>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=design-md.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-md.d.ts","sourceRoot":"","sources":["../../src/providers/design-md.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAM3D,OAAO,KAAK,EACV,sBAAsB,EACtB,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,WAAW,EACX,UAAU,EACX,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,KAAK,YAAY,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAuBpF;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKzD;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAUxE;AAID;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CASrF;AAID,qBAAa,gBAAiB,YAAW,kBAAkB;IACzD,QAAQ,CAAC,IAAI,EAAG,WAAW,CAAU;IAErC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAqB;gBAEjD,MAAM,EAAE,sBAAsB;IAcpC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAuDpD,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAoEpD,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC;CAyD3C"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import { logger } from "@nebutra/logger";
|
|
4
|
+
import { defaultTokensDir, readTokenSets, writeTokenSet } from "../io";
|
|
5
|
+
import { importFromDesignMd } from "../serialize/from-design-md";
|
|
6
|
+
import { serializeToDesignMd } from "../serialize/to-design-md";
|
|
7
|
+
import { serializeToPreviewHtml } from "../serialize/to-preview-html";
|
|
8
|
+
export { importFromDesignMd } from "../serialize/from-design-md";
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// DesignMd Provider — AI-native DESIGN.md format (@google/design.md)
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// The "design tool" here is a DESIGN.md file in the repo root (or a
|
|
13
|
+
// configurable path). Push = repo DTCG → DESIGN.md (with official lint gate).
|
|
14
|
+
// Pull = DESIGN.md → a `themes/<brand>.json` DTCG file in tokensDir.
|
|
15
|
+
//
|
|
16
|
+
// Why DESIGN.md?
|
|
17
|
+
// - Zero external dependencies at runtime (just a file in git)
|
|
18
|
+
// - AI-native: models can read/write DESIGN.md directly
|
|
19
|
+
// - Google's @google/design.md spec mandates lint rules (broken-ref, contrast)
|
|
20
|
+
// - Bridges the gap for indie hackers who want typed tokens without a design tool
|
|
21
|
+
// =============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Default DESIGN.md path: <cwd>/DESIGN.md
|
|
24
|
+
*/
|
|
25
|
+
function defaultDesignMdPath(cwd = process.cwd()) {
|
|
26
|
+
return join(cwd, "DESIGN.md");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Derive the preview file path from the DESIGN.md path.
|
|
30
|
+
*
|
|
31
|
+
* Replaces a trailing `.md` extension with `.preview.html`;
|
|
32
|
+
* if the path does not end in `.md`, appends `.preview.html`.
|
|
33
|
+
*
|
|
34
|
+
* Examples:
|
|
35
|
+
* "/repo/DESIGN.md" → "/repo/DESIGN.preview.html"
|
|
36
|
+
* "/repo/design/tokens.md" → "/repo/design/tokens.preview.html"
|
|
37
|
+
* "/repo/DESIGN" → "/repo/DESIGN.preview.html"
|
|
38
|
+
*/
|
|
39
|
+
export function previewHtmlPathFor(mdPath) {
|
|
40
|
+
if (mdPath.endsWith(".md")) {
|
|
41
|
+
return `${mdPath.slice(0, -3)}.preview.html`;
|
|
42
|
+
}
|
|
43
|
+
return `${mdPath}.preview.html`;
|
|
44
|
+
}
|
|
45
|
+
// ─── Lint Gate ────────────────────────────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Throw if the LintReport contains any error-severity findings.
|
|
48
|
+
* Warnings do not throw — they are surfaced in the push summary.
|
|
49
|
+
* Used as the fail-closed gate before writing a DESIGN.md.
|
|
50
|
+
*
|
|
51
|
+
* Fail-closed on either signal: error-severity findings in `findings[]`
|
|
52
|
+
* OR a non-zero `summary.errors` count.
|
|
53
|
+
*
|
|
54
|
+
* @param report - The LintReport returned by `lint(content)`.
|
|
55
|
+
* @param source - A human-readable label for the file (used in the error message).
|
|
56
|
+
* @throws When error-severity findings are present or `summary.errors > 0`.
|
|
57
|
+
*/
|
|
58
|
+
export function assertLintClean(report, source) {
|
|
59
|
+
const errorFindings = report.findings.filter((f) => f.severity === "error");
|
|
60
|
+
if (errorFindings.length === 0 && report.summary.errors === 0)
|
|
61
|
+
return;
|
|
62
|
+
const count = errorFindings.length || report.summary.errors;
|
|
63
|
+
const detail = errorFindings
|
|
64
|
+
.map((f) => (f.path ? ` ${f.path}: ${f.message}` : ` ${f.message}`))
|
|
65
|
+
.join("\n");
|
|
66
|
+
throw new Error(`[design-md] Lint failed for ${source} — ${count} error(s):\n${detail}`);
|
|
67
|
+
}
|
|
68
|
+
// ─── Lint Runner ─────────────────────────────────────────────────────────────
|
|
69
|
+
/**
|
|
70
|
+
* Dynamically imports the @google/design.md linter and runs it on `content`.
|
|
71
|
+
* Extracted as a named export so tests can verify the `[design-md]`-prefixed
|
|
72
|
+
* error thrown when the dynamic import or lint call itself fails.
|
|
73
|
+
*
|
|
74
|
+
* @throws When the linter import or invocation throws — error is wrapped with a
|
|
75
|
+
* `[design-md] Lint gate failed to run on <label>:` prefix.
|
|
76
|
+
*/
|
|
77
|
+
export async function runLintGate(content, label) {
|
|
78
|
+
try {
|
|
79
|
+
const { lint } = await import("@google/design.md/linter");
|
|
80
|
+
return lint(content);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
throw new Error(`[design-md] Lint gate failed to run on ${label}: ${err?.message ?? String(err)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ─── Provider ────────────────────────────────────────────────────────────────
|
|
87
|
+
export class DesignMdProvider {
|
|
88
|
+
name = "design-md";
|
|
89
|
+
tokensDir;
|
|
90
|
+
designMdPath;
|
|
91
|
+
designSystemName;
|
|
92
|
+
designSystemDescription;
|
|
93
|
+
constructor(config) {
|
|
94
|
+
this.tokensDir = config.tokensDir ?? defaultTokensDir();
|
|
95
|
+
this.designMdPath = config.designMdPath ?? process.env.DESIGN_MD_PATH ?? defaultDesignMdPath();
|
|
96
|
+
this.designSystemName = config.name;
|
|
97
|
+
this.designSystemDescription = config.description;
|
|
98
|
+
logger.info("[design-sync:design-md] Provider initialised", {
|
|
99
|
+
tokensDir: this.tokensDir,
|
|
100
|
+
designMdPath: this.designMdPath,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// ── Pull: DESIGN.md → tokensDir/themes/<brand>.json ──────────────────────
|
|
104
|
+
async pull(options = {}) {
|
|
105
|
+
// Graceful handling: if DESIGN.md does not exist, return an empty result.
|
|
106
|
+
let content;
|
|
107
|
+
try {
|
|
108
|
+
content = await readFile(this.designMdPath, "utf8");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
logger.warn("[design-sync:design-md] DESIGN.md not found, returning empty pull result", {
|
|
112
|
+
designMdPath: this.designMdPath,
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
sets: [],
|
|
116
|
+
written: false,
|
|
117
|
+
provider: "design-md",
|
|
118
|
+
pulledAt: new Date().toISOString(),
|
|
119
|
+
summary: `design-md: DESIGN.md not found at ${this.designMdPath} — no sets pulled`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const { set, report } = importFromDesignMd(content);
|
|
123
|
+
if (report.warnings.length > 0 || report.missingRequired.length > 0) {
|
|
124
|
+
logger.warn("[design-sync:design-md] imported DESIGN.md has gaps", {
|
|
125
|
+
warnings: report.warnings.length,
|
|
126
|
+
missingRequired: report.missingRequired,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const dryRun = options.dryRun ?? false;
|
|
130
|
+
if (!dryRun) {
|
|
131
|
+
await writeTokenSet(this.tokensDir, set);
|
|
132
|
+
logger.info("[design-sync:design-md] pull wrote token set", {
|
|
133
|
+
relativePath: set.relativePath,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const missingCount = report.missingRequired.length;
|
|
137
|
+
const unmappedCount = report.unmapped.length;
|
|
138
|
+
const reportSuffix = missingCount > 0 || unmappedCount > 0
|
|
139
|
+
? ` (missingRequired: ${missingCount}, unmapped: ${unmappedCount})`
|
|
140
|
+
: "";
|
|
141
|
+
return {
|
|
142
|
+
sets: [set],
|
|
143
|
+
written: !dryRun,
|
|
144
|
+
provider: "design-md",
|
|
145
|
+
pulledAt: new Date().toISOString(),
|
|
146
|
+
summary: dryRun
|
|
147
|
+
? `design-md: dry-run — would write ${set.relativePath}${reportSuffix}`
|
|
148
|
+
: `design-md: wrote ${set.relativePath}${reportSuffix}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// ── Push: tokensDir → DESIGN.md (with official lint gate) ────────────────
|
|
152
|
+
async push(options = {}) {
|
|
153
|
+
const all = await readTokenSets(this.tokensDir);
|
|
154
|
+
// Serialize all sets into DESIGN.md content.
|
|
155
|
+
const content = serializeToDesignMd(all, {
|
|
156
|
+
...(this.designSystemName !== undefined ? { name: this.designSystemName } : {}),
|
|
157
|
+
...(this.designSystemDescription !== undefined
|
|
158
|
+
? { description: this.designSystemDescription }
|
|
159
|
+
: {}),
|
|
160
|
+
});
|
|
161
|
+
// Compute a consistent, human-readable path label (relative preferred).
|
|
162
|
+
const mdLabel = relative(process.cwd(), this.designMdPath) || this.designMdPath;
|
|
163
|
+
// Run the official lint gate — fail closed on any error finding.
|
|
164
|
+
const lintReport = await runLintGate(content, mdLabel);
|
|
165
|
+
// Collect warnings for the summary (they don't block the write).
|
|
166
|
+
const warnings = lintReport.findings
|
|
167
|
+
.filter((f) => f.severity === "warning")
|
|
168
|
+
.map((f) => (f.path ? `${f.path}: ${f.message}` : f.message));
|
|
169
|
+
// Throw if any error-severity findings are present.
|
|
170
|
+
assertLintClean(lintReport, mdLabel);
|
|
171
|
+
// Compute the sibling preview path (before the write, so it's always defined)
|
|
172
|
+
const previewPath = previewHtmlPathFor(this.designMdPath);
|
|
173
|
+
const dryRun = options.dryRun ?? false;
|
|
174
|
+
if (!dryRun) {
|
|
175
|
+
await mkdir(dirname(this.designMdPath), { recursive: true });
|
|
176
|
+
await writeFile(this.designMdPath, content, "utf8");
|
|
177
|
+
logger.info("[design-sync:design-md] push wrote DESIGN.md", {
|
|
178
|
+
designMdPath: this.designMdPath,
|
|
179
|
+
sets: all.length,
|
|
180
|
+
});
|
|
181
|
+
// Generate and write the sibling visual preview
|
|
182
|
+
const previewHtml = serializeToPreviewHtml(all, {
|
|
183
|
+
...(this.designSystemName !== undefined ? { name: this.designSystemName } : {}),
|
|
184
|
+
});
|
|
185
|
+
await writeFile(previewPath, previewHtml, "utf8");
|
|
186
|
+
logger.info("[design-sync:design-md] push wrote preview", {
|
|
187
|
+
previewPath,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
logger.info("[design-sync:design-md] push dry-run — lint passed, files NOT written", {
|
|
192
|
+
designMdPath: this.designMdPath,
|
|
193
|
+
previewPath,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const warnSuffix = warnings.length > 0 ? ` (${warnings.length} warning(s))` : "";
|
|
197
|
+
return {
|
|
198
|
+
pushed: !dryRun,
|
|
199
|
+
sets: dryRun ? [this.designMdPath] : [this.designMdPath, previewPath],
|
|
200
|
+
provider: "design-md",
|
|
201
|
+
pushedAt: new Date().toISOString(),
|
|
202
|
+
summary: dryRun
|
|
203
|
+
? `design-md: dry-run — lint passed, would write ${mdLabel}${warnSuffix}`
|
|
204
|
+
: `design-md: wrote ${mdLabel} from ${all.length} DTCG set(s)${warnSuffix}`,
|
|
205
|
+
dryRun,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// ── Healthcheck ───────────────────────────────────────────────────────────
|
|
209
|
+
async healthcheck() {
|
|
210
|
+
const detected = [];
|
|
211
|
+
const missing = [];
|
|
212
|
+
// 1. Check tokensDir exists.
|
|
213
|
+
let tokensDirOk = false;
|
|
214
|
+
try {
|
|
215
|
+
const info = await stat(this.tokensDir);
|
|
216
|
+
if (info.isDirectory()) {
|
|
217
|
+
tokensDirOk = true;
|
|
218
|
+
detected.push("tokensDir");
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
missing.push("tokensDir (not a directory)");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
missing.push("tokensDir (does not exist)");
|
|
226
|
+
}
|
|
227
|
+
// 2. Check the @google/design.md lib is importable (it's a dep, should always be).
|
|
228
|
+
let lintLibOk = false;
|
|
229
|
+
try {
|
|
230
|
+
await import("@google/design.md/linter");
|
|
231
|
+
lintLibOk = true;
|
|
232
|
+
detected.push("@google/design.md");
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
missing.push("@google/design.md (lib not importable)");
|
|
236
|
+
}
|
|
237
|
+
// 3. List DESIGN_MD_PATH in detectedEnv when it's set (it's optional, not required).
|
|
238
|
+
const envPath = process.env.DESIGN_MD_PATH;
|
|
239
|
+
if (envPath) {
|
|
240
|
+
detected.push(`DESIGN_MD_PATH=${envPath}`);
|
|
241
|
+
}
|
|
242
|
+
// 4. Check the parent directory of designMdPath is accessible.
|
|
243
|
+
let mdDirOk = false;
|
|
244
|
+
const mdDir = dirname(this.designMdPath);
|
|
245
|
+
try {
|
|
246
|
+
await access(mdDir);
|
|
247
|
+
mdDirOk = true;
|
|
248
|
+
detected.push("designMdPath.parent");
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
missing.push("designMdPath.parent (not accessible)");
|
|
252
|
+
}
|
|
253
|
+
const ok = tokensDirOk && lintLibOk && mdDirOk;
|
|
254
|
+
return {
|
|
255
|
+
ok,
|
|
256
|
+
provider: "design-md",
|
|
257
|
+
message: ok
|
|
258
|
+
? `design-md: ready — tokensDir found, lint lib present, designMdPath=${this.designMdPath}`
|
|
259
|
+
: `design-md: not ready — missing: ${missing.join(", ")}`,
|
|
260
|
+
detectedEnv: detected,
|
|
261
|
+
missingEnv: missing,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DESIGN.md → DTCG theme importer
|
|
3
|
+
*
|
|
4
|
+
* Parse approach: programmatic via `lint()` from `@google/design.md/linter`.
|
|
5
|
+
* The `lint()` function parses the DESIGN.md content synchronously and returns
|
|
6
|
+
* a `DesignSystemState` with typed Maps for colors, rounded, spacing, and
|
|
7
|
+
* typography. We reshape that state into our DTCG naming convention:
|
|
8
|
+
* - colors.<x> → color.<x> ($type: "color", $value: hex string)
|
|
9
|
+
* - rounded.<x> → radius.<x> ($type: "dimension", $value: "<n><unit>" string)
|
|
10
|
+
* - spacing.<x> → spacing.<x> ($type: "dimension", $value: "<n><unit>" string)
|
|
11
|
+
* - body font → fontFamily.sans ($type: "fontFamily", $value: string)
|
|
12
|
+
*
|
|
13
|
+
* PROSE-COLOR FALLBACK: When @google/design.md extracts zero colors (e.g. the
|
|
14
|
+
* VoltAgent extended DESIGN.md format where colors are written as markdown prose
|
|
15
|
+
* rather than YAML front-matter), `extractColorsFromProse` is called to scrape
|
|
16
|
+
* labeled color literals of the form `**Label** (\`<value>\`)`.
|
|
17
|
+
*
|
|
18
|
+
* NEVER import `@google/design.md` (main entry — auto-runs CLI). Only the
|
|
19
|
+
* `@google/design.md/linter` subpath export is used here.
|
|
20
|
+
*
|
|
21
|
+
* All @google/design.md coupling is isolated in THIS file.
|
|
22
|
+
*/
|
|
23
|
+
import type { DesignTokenSet } from "../types";
|
|
24
|
+
export interface ImportReport {
|
|
25
|
+
/** Human-readable strings noting content that has no structured DTCG equivalent. */
|
|
26
|
+
unmapped: string[];
|
|
27
|
+
/** Registry-required token keys absent from the produced tree (dot-path format). */
|
|
28
|
+
missingRequired: string[];
|
|
29
|
+
/** Non-fatal warnings encountered during import. */
|
|
30
|
+
warnings: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface ImportResult {
|
|
33
|
+
set: DesignTokenSet;
|
|
34
|
+
report: ImportReport;
|
|
35
|
+
}
|
|
36
|
+
export interface ImportFromDesignMdOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Override the slug used for `relativePath` and `name`.
|
|
39
|
+
* Takes precedence over the `name` field in the DESIGN.md front matter.
|
|
40
|
+
*/
|
|
41
|
+
brandName?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract color entries from DESIGN.md prose content (VoltAgent extended format).
|
|
45
|
+
*
|
|
46
|
+
* Looks for labeled color literals matching: `**Label** (\`<value>\`)`
|
|
47
|
+
* where `<value>` is a valid CSS color (hex, rgb, rgba, hsl, hsla, oklch).
|
|
48
|
+
*
|
|
49
|
+
* Also handles the `{colors.xxx} — #hex` variant (backtick-quoted or unquoted hex).
|
|
50
|
+
*
|
|
51
|
+
* Scoping: if a heading matching `/^#{1,3}\s+.*colou?r/i` exists in the
|
|
52
|
+
* document, extraction is limited to the content under that heading (up to
|
|
53
|
+
* the next same-or-higher heading). Otherwise the whole document is scanned.
|
|
54
|
+
*
|
|
55
|
+
* Effect labels (shadow, overlay, glow, gradient, elevation, blur, scrim) are
|
|
56
|
+
* excluded regardless of scoping — they describe visual effects, not palette colors.
|
|
57
|
+
*
|
|
58
|
+
* Token naming: labels are kebab-cased (`"Spotify Green"` → `"spotify-green"`).
|
|
59
|
+
* De-duplicates by token name (first wins). Capped at 48 entries.
|
|
60
|
+
*
|
|
61
|
+
* Returns a plain `Record<string, string>` mapping slug → raw CSS color string.
|
|
62
|
+
*
|
|
63
|
+
* @public — exported so tests can unit-test the helper directly.
|
|
64
|
+
*/
|
|
65
|
+
export declare function extractColorsFromProse(content: string): Record<string, string>;
|
|
66
|
+
/**
|
|
67
|
+
* Parse a DESIGN.md document and produce a DTCG-compliant `DesignTokenSet`
|
|
68
|
+
* scoped to the `themes/` bucket (SSOT-safe — never overwrites core or semantic).
|
|
69
|
+
*
|
|
70
|
+
* The function is SYNCHRONOUS; it never reads from or writes to the filesystem.
|
|
71
|
+
*
|
|
72
|
+
* @param content - Raw DESIGN.md content (markdown + YAML front matter).
|
|
73
|
+
* @param options - Optional: `brandName` to override the slug.
|
|
74
|
+
* @returns `ImportResult` containing the token set and a diagnostic report.
|
|
75
|
+
* @throws If `@google/design.md` fails to parse the content, or if the produced
|
|
76
|
+
* DTCG tree is invalid (invalid leaves). Empty input produces an
|
|
77
|
+
* empty-but-valid tree and does NOT throw.
|
|
78
|
+
*/
|
|
79
|
+
export declare function importFromDesignMd(content: string, options?: ImportFromDesignMdOptions): ImportResult;
|
|
80
|
+
//# sourceMappingURL=from-design-md.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"from-design-md.d.ts","sourceRoot":"","sources":["../../src/serialize/from-design-md.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,UAAU,CAAC;AAIhE,MAAM,WAAW,YAAY;IAC3B,oFAAoF;IACpF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oFAAoF;IACpF,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,oDAAoD;IACpD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,cAAc,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAg5BD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO9E;AAsKD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,yBAAyB,GAClC,YAAY,CA0Td"}
|