@nebutra/design-sync 0.1.0 → 0.1.1
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/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +160 -0
- package/dist/detect.d.ts +21 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +79 -0
- package/dist/factory.d.ts +42 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +89 -0
- package/dist/figma-config/index.d.ts +37 -0
- package/dist/figma-config/index.d.ts.map +1 -0
- package/dist/figma-config/index.js +12 -0
- package/dist/figma-config/tokens-studio.config.json +34 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/io.d.ts +26 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +119 -0
- package/dist/providers/figma.d.ts +21 -0
- package/dist/providers/figma.d.ts.map +1 -0
- package/dist/providers/figma.js +179 -0
- package/dist/providers/git-only.d.ts +11 -0
- package/dist/providers/git-only.d.ts.map +1 -0
- package/dist/providers/git-only.js +104 -0
- package/dist/providers/memory.d.ts +17 -0
- package/dist/providers/memory.d.ts.map +1 -0
- package/dist/providers/memory.js +65 -0
- package/dist/providers/penpot.d.ts +21 -0
- package/dist/providers/penpot.d.ts.map +1 -0
- package/dist/providers/penpot.js +137 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/package.json +3 -3
package/dist/io.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
3
|
+
import { DesignTokenLeafSchema } from "./types";
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// DTCG Filesystem I/O
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Shared helpers used by every provider. Reading and writing the canonical
|
|
8
|
+
// W3C DTCG JSON tree on disk is identical across Figma / Penpot / git-only —
|
|
9
|
+
// the only thing that differs is what the provider does AFTER it has the data.
|
|
10
|
+
// =============================================================================
|
|
11
|
+
const TOKEN_FILE_EXT = ".json";
|
|
12
|
+
/**
|
|
13
|
+
* Recursively list every `.json` file beneath `root`. Skips dotfiles.
|
|
14
|
+
*/
|
|
15
|
+
async function listJsonFiles(root) {
|
|
16
|
+
const out = [];
|
|
17
|
+
async function walk(dir) {
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = await readdir(dir);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (entry.startsWith("."))
|
|
27
|
+
continue;
|
|
28
|
+
const full = join(dir, entry);
|
|
29
|
+
const info = await stat(full);
|
|
30
|
+
if (info.isDirectory()) {
|
|
31
|
+
await walk(full);
|
|
32
|
+
}
|
|
33
|
+
else if (info.isFile() && entry.endsWith(TOKEN_FILE_EXT)) {
|
|
34
|
+
out.push(full);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
await walk(root);
|
|
39
|
+
return out.sort();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validate that every leaf in a DTCG tree carries `$value` and `$type`.
|
|
43
|
+
* Returns a list of dot-paths that violate the contract.
|
|
44
|
+
*/
|
|
45
|
+
export function validateDtcgTree(tree, pathPrefix = "") {
|
|
46
|
+
const errors = [];
|
|
47
|
+
if (tree === null || typeof tree !== "object" || Array.isArray(tree)) {
|
|
48
|
+
return errors;
|
|
49
|
+
}
|
|
50
|
+
const node = tree;
|
|
51
|
+
// A leaf has `$value` (or `value` for legacy Tokens Studio v1).
|
|
52
|
+
const hasValue = "$value" in node || "value" in node;
|
|
53
|
+
if (hasValue) {
|
|
54
|
+
const parsed = DesignTokenLeafSchema.safeParse(node);
|
|
55
|
+
if (!parsed.success) {
|
|
56
|
+
errors.push(`${pathPrefix || "<root>"}: ${parsed.error.message}`);
|
|
57
|
+
}
|
|
58
|
+
return errors;
|
|
59
|
+
}
|
|
60
|
+
for (const [key, value] of Object.entries(node)) {
|
|
61
|
+
if (key.startsWith("$"))
|
|
62
|
+
continue;
|
|
63
|
+
const childPath = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
64
|
+
errors.push(...validateDtcgTree(value, childPath));
|
|
65
|
+
}
|
|
66
|
+
return errors;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read all DTCG token files under `tokensDir` into memory.
|
|
70
|
+
* Each file becomes one `DesignTokenSet`.
|
|
71
|
+
*/
|
|
72
|
+
export async function readTokenSets(tokensDir) {
|
|
73
|
+
const files = await listJsonFiles(tokensDir);
|
|
74
|
+
const sets = [];
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const raw = await readFile(file, "utf8");
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(raw);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
throw new Error(`[design-sync] Failed to parse DTCG file ${file}: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
85
|
+
throw new Error(`[design-sync] DTCG file ${file} must contain an object at the root.`);
|
|
86
|
+
}
|
|
87
|
+
const relativePath = relative(tokensDir, file).split(sep).join("/");
|
|
88
|
+
const name = relativePath.replace(/\.json$/u, "").replace(/\//gu, "/");
|
|
89
|
+
sets.push({
|
|
90
|
+
name,
|
|
91
|
+
relativePath,
|
|
92
|
+
tokens: parsed,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return sets;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Write a `DesignTokenSet` back to disk under `tokensDir`. Creates
|
|
99
|
+
* parent directories as needed and uses 2-space indentation with a
|
|
100
|
+
* trailing newline (matches `.tokens-studio/config.json#format`).
|
|
101
|
+
*/
|
|
102
|
+
export async function writeTokenSet(tokensDir, set) {
|
|
103
|
+
const target = join(tokensDir, set.relativePath);
|
|
104
|
+
await mkdir(dirname(target), { recursive: true });
|
|
105
|
+
const serialized = `${JSON.stringify(set.tokens, null, 2)}\n`;
|
|
106
|
+
await writeFile(target, serialized, "utf8");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Default tokens directory for the Nebutra-Sailor monorepo.
|
|
110
|
+
*/
|
|
111
|
+
export function defaultTokensDir(cwd = process.cwd()) {
|
|
112
|
+
return join(cwd, "packages", "design", "design-tokens", "tokens");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Default Tokens Studio metadata directory for the Nebutra-Sailor monorepo.
|
|
116
|
+
*/
|
|
117
|
+
export function defaultTokensStudioDir(cwd = process.cwd()) {
|
|
118
|
+
return join(cwd, ".tokens-studio");
|
|
119
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DesignSyncProvider, FigmaProviderConfig, HealthStatus, PullOptions, PullResult, PushOptions, PushResult } from "../types";
|
|
2
|
+
export declare class FigmaProvider implements DesignSyncProvider {
|
|
3
|
+
readonly name: "figma";
|
|
4
|
+
private readonly tokensDir;
|
|
5
|
+
private readonly tokensStudioDir;
|
|
6
|
+
private readonly personalAccessToken;
|
|
7
|
+
private readonly fileId;
|
|
8
|
+
private readonly githubRepo;
|
|
9
|
+
private readonly githubBranch;
|
|
10
|
+
constructor(config: FigmaProviderConfig);
|
|
11
|
+
pull(options?: PullOptions): Promise<PullResult>;
|
|
12
|
+
push(options?: PushOptions): Promise<PushResult>;
|
|
13
|
+
healthcheck(): Promise<HealthStatus>;
|
|
14
|
+
/**
|
|
15
|
+
* Assert that `.tokens-studio/{config,metadata,themes}.json` exist + parse.
|
|
16
|
+
* The plugin refuses to load the design system if any of these are missing
|
|
17
|
+
* or malformed; surfacing the failure here prevents silent drift.
|
|
18
|
+
*/
|
|
19
|
+
private assertTokensStudioMetadata;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=figma.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figma.d.ts","sourceRoot":"","sources":["../../src/providers/figma.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,WAAW,EACX,UAAU,EACX,MAAM,UAAU,CAAC;AAyBlB,qBAAa,aAAc,YAAW,kBAAkB;IACtD,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAU;IAEjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,MAAM,EAAE,mBAAmB;IAgBjC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAiBpD,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAiDpD,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC;IAwC1C;;;;OAIG;YACW,0BAA0B;CA+BzC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { logger } from "@nebutra/logger";
|
|
4
|
+
import { defaultTokensDir, defaultTokensStudioDir, readTokenSets, validateDtcgTree } from "../io";
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// Figma Provider — Figma + Tokens Studio
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Architecture (matches the legacy `.tokens-studio/config.json` setup):
|
|
9
|
+
//
|
|
10
|
+
// Figma (Tokens Studio plugin) ──► GitHub branch ──► repo (DTCG JSON)
|
|
11
|
+
// ▲ │
|
|
12
|
+
// └─── pull from main ──────────┘
|
|
13
|
+
//
|
|
14
|
+
// The plugin owns the git transport. This provider:
|
|
15
|
+
// - validates that the Tokens Studio config + DTCG tree are well-formed
|
|
16
|
+
// - "pull" reads the DTCG files the plugin already wrote to git
|
|
17
|
+
// - "push" is a dry-run scaffold for the Figma Variables REST API
|
|
18
|
+
// (PATCH /v1/files/:file_key/variables) — the user must opt in by
|
|
19
|
+
// providing FIGMA_PERSONAL_ACCESS_TOKEN + FIGMA_FILE_ID and removing
|
|
20
|
+
// the early-exit guard.
|
|
21
|
+
//
|
|
22
|
+
// All Tokens Studio metadata that previously lived in `.tokens-studio/` is
|
|
23
|
+
// still consumed from disk so the existing designer onboarding does not break.
|
|
24
|
+
// =============================================================================
|
|
25
|
+
const FIGMA_API_ROOT = "https://api.figma.com";
|
|
26
|
+
export class FigmaProvider {
|
|
27
|
+
name = "figma";
|
|
28
|
+
tokensDir;
|
|
29
|
+
tokensStudioDir;
|
|
30
|
+
personalAccessToken;
|
|
31
|
+
fileId;
|
|
32
|
+
githubRepo;
|
|
33
|
+
githubBranch;
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.tokensDir = config.tokensDir ?? defaultTokensDir();
|
|
36
|
+
this.tokensStudioDir = config.tokensStudioDir ?? defaultTokensStudioDir();
|
|
37
|
+
this.personalAccessToken =
|
|
38
|
+
config.personalAccessToken ?? process.env.FIGMA_PERSONAL_ACCESS_TOKEN ?? undefined;
|
|
39
|
+
this.fileId = config.fileId ?? process.env.FIGMA_FILE_ID ?? undefined;
|
|
40
|
+
this.githubRepo = config.githubRepo ?? process.env.FIGMA_GITHUB_REPO ?? undefined;
|
|
41
|
+
this.githubBranch = config.githubBranch ?? process.env.FIGMA_GITHUB_BRANCH ?? "main";
|
|
42
|
+
logger.info("[design-sync:figma] Provider initialised", {
|
|
43
|
+
tokensDir: this.tokensDir,
|
|
44
|
+
hasToken: Boolean(this.personalAccessToken),
|
|
45
|
+
hasFileId: Boolean(this.fileId),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async pull(options = {}) {
|
|
49
|
+
// Tokens Studio plugin already wrote DTCG files to git on `pull`.
|
|
50
|
+
// We just re-read them from disk and (optionally) validate Tokens
|
|
51
|
+
// Studio metadata so the result mirrors what the plugin would emit.
|
|
52
|
+
const sets = await readTokenSets(this.tokensDir);
|
|
53
|
+
const filtered = filterSets(sets, options.themes);
|
|
54
|
+
await this.assertTokensStudioMetadata();
|
|
55
|
+
return {
|
|
56
|
+
sets: filtered,
|
|
57
|
+
written: false,
|
|
58
|
+
provider: "figma",
|
|
59
|
+
pulledAt: new Date().toISOString(),
|
|
60
|
+
summary: `figma: read ${filtered.length} DTCG token set(s) (Tokens Studio plugin owns the git transport)`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async push(options = {}) {
|
|
64
|
+
// Pre-flight: validate DTCG before attempting any remote call.
|
|
65
|
+
const all = await readTokenSets(this.tokensDir);
|
|
66
|
+
const sets = filterSets(all, options.themes);
|
|
67
|
+
for (const set of sets) {
|
|
68
|
+
const errors = validateDtcgTree(set.tokens);
|
|
69
|
+
if (errors.length > 0) {
|
|
70
|
+
throw new Error(`[design-sync:figma] DTCG validation failed for ${set.relativePath}:\n - ${errors.join("\n - ")}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const credsReady = Boolean(this.personalAccessToken && this.fileId);
|
|
74
|
+
const explicitDryRun = options.dryRun ?? false;
|
|
75
|
+
const dryRun = explicitDryRun || !credsReady;
|
|
76
|
+
if (dryRun) {
|
|
77
|
+
logger.warn("[design-sync:figma] push skipped (dry-run scaffold)", {
|
|
78
|
+
reason: credsReady ? "explicit dryRun" : "missing credentials",
|
|
79
|
+
sets: sets.length,
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
pushed: false,
|
|
83
|
+
sets: sets.map((s) => s.relativePath),
|
|
84
|
+
provider: "figma",
|
|
85
|
+
pushedAt: new Date().toISOString(),
|
|
86
|
+
summary: credsReady
|
|
87
|
+
? `figma: dry-run — would PATCH ${sets.length} set(s) to ${FIGMA_API_ROOT}/v1/files/${this.fileId}/variables`
|
|
88
|
+
: "figma: dry-run — FIGMA_PERSONAL_ACCESS_TOKEN or FIGMA_FILE_ID not set",
|
|
89
|
+
dryRun: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Real push placeholder — kept guarded until the user opts in.
|
|
93
|
+
// The integration will call the Figma Variables REST API:
|
|
94
|
+
// PATCH /v1/files/:file_key/variables
|
|
95
|
+
// X-Figma-Token: <personalAccessToken>
|
|
96
|
+
// See https://www.figma.com/developers/api#variables
|
|
97
|
+
throw new Error("[design-sync:figma] live push to Figma Variables REST API is not yet implemented. " +
|
|
98
|
+
"Use { dryRun: true } or unset FIGMA_PERSONAL_ACCESS_TOKEN until the integration is wired up. " +
|
|
99
|
+
"See packages/design/design-sync/DESIGN.md for the rollout plan.");
|
|
100
|
+
}
|
|
101
|
+
async healthcheck() {
|
|
102
|
+
const detected = [];
|
|
103
|
+
const missing = [];
|
|
104
|
+
if (this.personalAccessToken)
|
|
105
|
+
detected.push("FIGMA_PERSONAL_ACCESS_TOKEN");
|
|
106
|
+
else
|
|
107
|
+
missing.push("FIGMA_PERSONAL_ACCESS_TOKEN");
|
|
108
|
+
if (this.fileId)
|
|
109
|
+
detected.push("FIGMA_FILE_ID");
|
|
110
|
+
else
|
|
111
|
+
missing.push("FIGMA_FILE_ID");
|
|
112
|
+
if (this.githubRepo)
|
|
113
|
+
detected.push("FIGMA_GITHUB_REPO");
|
|
114
|
+
if (this.githubBranch)
|
|
115
|
+
detected.push(`FIGMA_GITHUB_BRANCH=${this.githubBranch}`);
|
|
116
|
+
let tokensStudioOk = true;
|
|
117
|
+
let tokensStudioMessage = "";
|
|
118
|
+
try {
|
|
119
|
+
await this.assertTokensStudioMetadata();
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
tokensStudioOk = false;
|
|
123
|
+
tokensStudioMessage = error.message;
|
|
124
|
+
missing.push("tokens-studio metadata");
|
|
125
|
+
}
|
|
126
|
+
const ok = missing.length === 0 && tokensStudioOk;
|
|
127
|
+
return {
|
|
128
|
+
ok,
|
|
129
|
+
provider: "figma",
|
|
130
|
+
message: ok
|
|
131
|
+
? "figma: credentials present + Tokens Studio metadata valid"
|
|
132
|
+
: `figma: not ready — ${[
|
|
133
|
+
missing.length > 0 ? `missing ${missing.join(", ")}` : "",
|
|
134
|
+
tokensStudioMessage,
|
|
135
|
+
]
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.join("; ")}`,
|
|
138
|
+
detectedEnv: detected,
|
|
139
|
+
missingEnv: missing,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Assert that `.tokens-studio/{config,metadata,themes}.json` exist + parse.
|
|
144
|
+
* The plugin refuses to load the design system if any of these are missing
|
|
145
|
+
* or malformed; surfacing the failure here prevents silent drift.
|
|
146
|
+
*/
|
|
147
|
+
async assertTokensStudioMetadata() {
|
|
148
|
+
const required = ["config.json", "metadata.json", "themes.json"];
|
|
149
|
+
const errors = [];
|
|
150
|
+
try {
|
|
151
|
+
const info = await stat(this.tokensStudioDir);
|
|
152
|
+
if (!info.isDirectory()) {
|
|
153
|
+
throw new Error(`${this.tokensStudioDir} is not a directory`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw new Error(`[design-sync:figma] Tokens Studio metadata directory missing at ${this.tokensStudioDir}: ${error.message}`);
|
|
158
|
+
}
|
|
159
|
+
for (const file of required) {
|
|
160
|
+
const path = join(this.tokensStudioDir, file);
|
|
161
|
+
try {
|
|
162
|
+
const raw = await readFile(path, "utf8");
|
|
163
|
+
JSON.parse(raw);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
errors.push(`${path}: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (errors.length > 0) {
|
|
170
|
+
throw new Error(`[design-sync:figma] Tokens Studio metadata invalid:\n - ${errors.join("\n - ")}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function filterSets(sets, themes) {
|
|
175
|
+
if (!themes || themes.length === 0)
|
|
176
|
+
return sets;
|
|
177
|
+
const wanted = new Set(themes);
|
|
178
|
+
return sets.filter((s) => wanted.has(s.name) || wanted.has(s.relativePath));
|
|
179
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DesignSyncProvider, GitOnlyProviderConfig, HealthStatus, PullOptions, PullResult, PushOptions, PushResult } from "../types";
|
|
2
|
+
export declare class GitOnlyProvider implements DesignSyncProvider {
|
|
3
|
+
readonly name: "git-only";
|
|
4
|
+
private readonly tokensDir;
|
|
5
|
+
private readonly tokensStudioDir;
|
|
6
|
+
constructor(config: GitOnlyProviderConfig);
|
|
7
|
+
pull(options?: PullOptions): Promise<PullResult>;
|
|
8
|
+
push(options?: PushOptions): Promise<PushResult>;
|
|
9
|
+
healthcheck(): Promise<HealthStatus>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=git-only.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-only.d.ts","sourceRoot":"","sources":["../../src/providers/git-only.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,WAAW,EACX,UAAU,EACX,MAAM,UAAU,CAAC;AAalB,qBAAa,eAAgB,YAAW,kBAAkB;IACxD,QAAQ,CAAC,IAAI,EAAG,UAAU,CAAU;IAEpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAE7B,MAAM,EAAE,qBAAqB;IAQnC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAapD,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAmCpD,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC;CAmC3C"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { logger } from "@nebutra/logger";
|
|
3
|
+
import { defaultTokensDir, defaultTokensStudioDir, readTokenSets, validateDtcgTree, writeTokenSet, } from "../io";
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Git-Only Provider — DTCG-only, no design tool
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// This is the zero-config default for indie hackers, AI-driven dev workflows,
|
|
8
|
+
// and any team that does NOT use a design tool. It treats the local
|
|
9
|
+
// `packages/design/design-tokens/tokens` directory as the single source of truth.
|
|
10
|
+
//
|
|
11
|
+
// `pull` simply re-reads the DTCG files (a no-op refresh).
|
|
12
|
+
// `push` validates + re-serialises (formats) the DTCG files in place.
|
|
13
|
+
// =============================================================================
|
|
14
|
+
export class GitOnlyProvider {
|
|
15
|
+
name = "git-only";
|
|
16
|
+
tokensDir;
|
|
17
|
+
tokensStudioDir;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.tokensDir = config.tokensDir ?? defaultTokensDir();
|
|
20
|
+
this.tokensStudioDir = config.tokensStudioDir ?? defaultTokensStudioDir();
|
|
21
|
+
logger.info("[design-sync:git-only] Provider initialised", {
|
|
22
|
+
tokensDir: this.tokensDir,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async pull(options = {}) {
|
|
26
|
+
const sets = await readTokenSets(this.tokensDir);
|
|
27
|
+
const filtered = filterSets(sets, options.themes);
|
|
28
|
+
return {
|
|
29
|
+
sets: filtered,
|
|
30
|
+
written: false,
|
|
31
|
+
provider: "git-only",
|
|
32
|
+
pulledAt: new Date().toISOString(),
|
|
33
|
+
summary: `Read ${filtered.length} DTCG token set(s) from ${this.tokensDir}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async push(options = {}) {
|
|
37
|
+
const all = await readTokenSets(this.tokensDir);
|
|
38
|
+
const sets = filterSets(all, options.themes);
|
|
39
|
+
// Validate every set before re-serialising — fail closed on bad DTCG.
|
|
40
|
+
for (const set of sets) {
|
|
41
|
+
const errors = validateDtcgTree(set.tokens);
|
|
42
|
+
if (errors.length > 0) {
|
|
43
|
+
throw new Error(`[design-sync:git-only] DTCG validation failed for ${set.relativePath}:\n - ${errors.join("\n - ")}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const dryRun = options.dryRun ?? false;
|
|
47
|
+
if (!dryRun) {
|
|
48
|
+
for (const set of sets) {
|
|
49
|
+
await writeTokenSet(this.tokensDir, set);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
pushed: !dryRun,
|
|
54
|
+
sets: sets.map((s) => s.relativePath),
|
|
55
|
+
provider: "git-only",
|
|
56
|
+
pushedAt: new Date().toISOString(),
|
|
57
|
+
summary: dryRun
|
|
58
|
+
? `Validated ${sets.length} DTCG file(s) (dry-run, no files modified)`
|
|
59
|
+
: `Reformatted ${sets.length} DTCG file(s) under ${this.tokensDir}`,
|
|
60
|
+
dryRun,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async healthcheck() {
|
|
64
|
+
const detected = [];
|
|
65
|
+
const missing = [];
|
|
66
|
+
try {
|
|
67
|
+
const info = await stat(this.tokensDir);
|
|
68
|
+
if (info.isDirectory()) {
|
|
69
|
+
detected.push("tokensDir");
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
missing.push("tokensDir (not a directory)");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
missing.push("tokensDir (does not exist)");
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const info = await stat(this.tokensStudioDir);
|
|
80
|
+
if (info.isDirectory()) {
|
|
81
|
+
detected.push("tokensStudioDir");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// tokens-studio dir is optional for git-only mode
|
|
86
|
+
}
|
|
87
|
+
const ok = missing.length === 0;
|
|
88
|
+
return {
|
|
89
|
+
ok,
|
|
90
|
+
provider: "git-only",
|
|
91
|
+
message: ok
|
|
92
|
+
? `git-only ready — DTCG source dir found at ${this.tokensDir}`
|
|
93
|
+
: `git-only misconfigured — missing: ${missing.join(", ")}`,
|
|
94
|
+
detectedEnv: detected,
|
|
95
|
+
missingEnv: missing,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function filterSets(sets, themes) {
|
|
100
|
+
if (!themes || themes.length === 0)
|
|
101
|
+
return sets;
|
|
102
|
+
const wanted = new Set(themes);
|
|
103
|
+
return sets.filter((s) => wanted.has(s.name) || wanted.has(s.relativePath));
|
|
104
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DesignSyncProvider, DesignTokenSet, HealthStatus, MemoryProviderConfig, PullOptions, PullResult, PushOptions, PushResult } from "../types";
|
|
2
|
+
export declare class MemoryProvider implements DesignSyncProvider {
|
|
3
|
+
readonly name: "memory";
|
|
4
|
+
private sets;
|
|
5
|
+
readonly pullCalls: PullOptions[];
|
|
6
|
+
readonly pushCalls: PushOptions[];
|
|
7
|
+
constructor(config: MemoryProviderConfig);
|
|
8
|
+
pull(options?: PullOptions): Promise<PullResult>;
|
|
9
|
+
push(options?: PushOptions): Promise<PushResult>;
|
|
10
|
+
healthcheck(): Promise<HealthStatus>;
|
|
11
|
+
/**
|
|
12
|
+
* Replace the in-memory set list — useful in tests for asserting how
|
|
13
|
+
* the provider responds to different remote states.
|
|
14
|
+
*/
|
|
15
|
+
replaceSets(sets: DesignTokenSet[]): void;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/providers/memory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,WAAW,EACX,UAAU,EACV,WAAW,EACX,UAAU,EACX,MAAM,UAAU,CAAC;AAWlB,qBAAa,cAAe,YAAW,kBAAkB;IACvD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,IAAI,CAAmB;IAC/B,SAAgB,SAAS,EAAE,WAAW,EAAE,CAAM;IAC9C,SAAgB,SAAS,EAAE,WAAW,EAAE,CAAM;gBAElC,MAAM,EAAE,oBAAoB;IAKlC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAYpD,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAcpD,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC;IAU1C;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,IAAI;CAG1C"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { logger } from "@nebutra/logger";
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Memory Provider — in-memory test fixture
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Used in unit tests and CI dry-runs where touching the filesystem or
|
|
6
|
+
// hitting a remote API would be wasteful. Tracks `pull`/`push` calls so
|
|
7
|
+
// tests can assert behaviour.
|
|
8
|
+
// NOT for production.
|
|
9
|
+
// =============================================================================
|
|
10
|
+
export class MemoryProvider {
|
|
11
|
+
name = "memory";
|
|
12
|
+
sets;
|
|
13
|
+
pullCalls = [];
|
|
14
|
+
pushCalls = [];
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.sets = config.initialSets ?? [];
|
|
17
|
+
logger.info("[design-sync:memory] Provider initialised (test fixture only)");
|
|
18
|
+
}
|
|
19
|
+
async pull(options = {}) {
|
|
20
|
+
this.pullCalls.push(options);
|
|
21
|
+
const sets = filterSets(this.sets, options.themes);
|
|
22
|
+
return {
|
|
23
|
+
sets,
|
|
24
|
+
written: false,
|
|
25
|
+
provider: "memory",
|
|
26
|
+
pulledAt: new Date().toISOString(),
|
|
27
|
+
summary: `memory: returned ${sets.length} in-memory token set(s)`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async push(options = {}) {
|
|
31
|
+
this.pushCalls.push(options);
|
|
32
|
+
const filtered = filterSets(this.sets, options.themes);
|
|
33
|
+
const dryRun = options.dryRun ?? false;
|
|
34
|
+
return {
|
|
35
|
+
pushed: !dryRun,
|
|
36
|
+
sets: filtered.map((s) => s.relativePath),
|
|
37
|
+
provider: "memory",
|
|
38
|
+
pushedAt: new Date().toISOString(),
|
|
39
|
+
summary: `memory: ${dryRun ? "dry-run" : "applied"} ${filtered.length} set(s)`,
|
|
40
|
+
dryRun,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async healthcheck() {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
provider: "memory",
|
|
47
|
+
message: "memory provider always healthy (test fixture)",
|
|
48
|
+
detectedEnv: [],
|
|
49
|
+
missingEnv: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Replace the in-memory set list — useful in tests for asserting how
|
|
54
|
+
* the provider responds to different remote states.
|
|
55
|
+
*/
|
|
56
|
+
replaceSets(sets) {
|
|
57
|
+
this.sets = [...sets];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function filterSets(sets, themes) {
|
|
61
|
+
if (!themes || themes.length === 0)
|
|
62
|
+
return sets;
|
|
63
|
+
const wanted = new Set(themes);
|
|
64
|
+
return sets.filter((s) => wanted.has(s.name) || wanted.has(s.relativePath));
|
|
65
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DesignSyncProvider, HealthStatus, PenpotProviderConfig, PullOptions, PullResult, PushOptions, PushResult } from "../types";
|
|
2
|
+
export declare class PenpotProvider implements DesignSyncProvider {
|
|
3
|
+
readonly name: "penpot";
|
|
4
|
+
private readonly tokensDir;
|
|
5
|
+
private readonly tokensStudioDir;
|
|
6
|
+
private readonly apiUrl;
|
|
7
|
+
private readonly token;
|
|
8
|
+
private readonly fileId;
|
|
9
|
+
private readonly teamId;
|
|
10
|
+
constructor(config: PenpotProviderConfig);
|
|
11
|
+
pull(options?: PullOptions): Promise<PullResult>;
|
|
12
|
+
push(options?: PushOptions): Promise<PushResult>;
|
|
13
|
+
healthcheck(): Promise<HealthStatus>;
|
|
14
|
+
/**
|
|
15
|
+
* Reserved for future use — exposes the configured tokens-studio directory
|
|
16
|
+
* so consumers (e.g. CI workflows) can attach Penpot-side metadata mirrors.
|
|
17
|
+
*/
|
|
18
|
+
getTokensStudioDir(): string;
|
|
19
|
+
private credsReady;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=penpot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"penpot.d.ts","sourceRoot":"","sources":["../../src/providers/penpot.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,WAAW,EACX,UAAU,EACV,WAAW,EACX,UAAU,EACX,MAAM,UAAU,CAAC;AAmBlB,qBAAa,cAAe,YAAW,kBAAkB;IACvD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;gBAEhC,MAAM,EAAE,oBAAoB;IAelC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IA0BpD,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IA8CpD,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC;IAyB1C;;;OAGG;IACH,kBAAkB,IAAI,MAAM;IAI5B,OAAO,CAAC,UAAU;CAGnB"}
|