@styleframe/cli 4.0.0 → 4.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/CHANGELOG.md +43 -0
- package/dist/build-CBdsifMN.js +47 -0
- package/dist/build-D7cinF0l.cjs +50 -0
- package/dist/build-dtcg-BpbjBRCf.js +429 -0
- package/dist/build-dtcg-vuGHy-Sl.cjs +434 -0
- package/dist/chunk-D6vf50IK.cjs +28 -0
- package/dist/dtcg-D1-iITOr.js +14 -0
- package/dist/dtcg-D84AfyzO.cjs +13 -0
- package/dist/export-CBdPGGEq.js +66 -0
- package/dist/export-DmPAU9Wh.cjs +69 -0
- package/dist/export-ONk9eKoZ.cjs +86 -0
- package/dist/export-suUS16eO.js +83 -0
- package/dist/figma-BvXoqRPU.cjs +13 -0
- package/dist/figma-D2RJh56T.js +14 -0
- package/dist/import-BQrcHNjK.cjs +126 -0
- package/dist/import-Bll_uBvJ.js +123 -0
- package/dist/import-MqLYxb8d.js +114 -0
- package/dist/import-ibQc_GXm.cjs +117 -0
- package/dist/index.cjs +16 -16
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.js +16 -17
- package/dist/init-CAO0mA_w.js +262 -0
- package/dist/init-CaJoUVv2.cjs +265 -0
- package/package.json +22 -20
- package/dist/build-BIWOTFZD.cjs +0 -49
- package/dist/build-CANA04j1.js +0 -49
- package/dist/export-BMneJTdq.cjs +0 -517
- package/dist/export-Cx6awh55.js +0 -517
- package/dist/import-BLbc2zWU.cjs +0 -90
- package/dist/import-a5DtGEAY.js +0 -90
- package/dist/index-BTHfb82h.js +0 -14
- package/dist/index-jMzviwjD.cjs +0 -14
- package/dist/init-CKeTXHp5.js +0 -234
- package/dist/init-CO7VnQKe.cjs +0 -234
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @styleframe/cli
|
|
2
2
|
|
|
3
|
+
## 4.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#238](https://github.com/styleframe-dev/styleframe/pull/238) [`4ace91d`](https://github.com/styleframe-dev/styleframe/commit/4ace91d5e15020c29d585848ee66f6250946b2d1) Thanks [@alexgrozav](https://github.com/alexgrozav)! - Bundle type declarations on build. The shared Vite config now enables `vite-plugin-dts`'s `bundleTypes`, so each package ships a single rolled-up `.d.ts` per entry (via `@microsoft/api-extractor`) instead of a tree of per-file declarations. `@microsoft/api-extractor` is now a peer dependency of `@styleframe/config-vite`.
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`4ace91d`](https://github.com/styleframe-dev/styleframe/commit/4ace91d5e15020c29d585848ee66f6250946b2d1)]:
|
|
10
|
+
- @styleframe/loader@3.0.3
|
|
11
|
+
|
|
12
|
+
## 4.1.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- [#229](https://github.com/styleframe-dev/styleframe/pull/229) [`19e9866`](https://github.com/styleframe-dev/styleframe/commit/19e986618c669546972924840189fc5f16f1a1cd) Thanks [@alexgrozav](https://github.com/alexgrozav)! - **Fix:** Resolve critical token loss bug in Figma plugin export where 121 of 464 variables were dropped due to `setNestedToken` overwriting group subtrees instead of promoting parent tokens to `$root`.
|
|
17
|
+
|
|
18
|
+
**Feature:** Add mode selector dropdown to Figma plugin Export tab for filtering variables by specific theme/mode before export.
|
|
19
|
+
|
|
20
|
+
**Feature:** Add CLI commands for Figma-compatible DTCG sync: `figma export` and `figma import` enable bidirectional token synchronization between Figma collections and design token files.
|
|
21
|
+
|
|
22
|
+
**Improvement:** Embed collection metadata in DTCG output via `$extensions.dev.styleframe.collection` for better round-trip awareness.
|
|
23
|
+
|
|
24
|
+
**Improvement:** Restore BOOLEAN variable support by converting to string type with `dev.styleframe.boolean: true` extension metadata.
|
|
25
|
+
|
|
26
|
+
**Fix:** Correct UI variable count display to read from `msg.result.tokens` instead of wrapper object, ensuring accurate exported variable count and collection name extraction.
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- [#231](https://github.com/styleframe-dev/styleframe/pull/231) [`c0cfbdc`](https://github.com/styleframe-dev/styleframe/commit/c0cfbdc65ddca0b5ea4a420902be2a23580b392f) Thanks [@alexgrozav](https://github.com/alexgrozav)! - Update `styleframe init` to install v3.x dependencies instead of v2.x. Remove `@styleframe/pro` from scaffolded dependencies.
|
|
31
|
+
|
|
32
|
+
- [#235](https://github.com/styleframe-dev/styleframe/pull/235) [`6acd766`](https://github.com/styleframe-dev/styleframe/commit/6acd766eefc82139d8cd98dfb9b553449945d704) Thanks [@alexgrozav](https://github.com/alexgrozav)! - Generate DTS output as two files that describe the virtual modules in two complementary forms. The transpiler's `dts` mode now emits:
|
|
33
|
+
- `styleframe.d.ts` — top-level exports describing the `virtual:styleframe` module
|
|
34
|
+
- `shims.d.ts` — self-contained ambient declarations: a `declare module "virtual:styleframe"` carrying the full typed exports, plus the `virtual:styleframe.css` string shim
|
|
35
|
+
|
|
36
|
+
Non-Vue consumers resolve both virtual modules from `shims.d.ts` alone (picked up via the `.styleframe/**/*.d.ts` include) with **zero `paths` configuration**. Vue consumers additionally map `virtual:styleframe` to `styleframe.d.ts` via a `compilerOptions.paths` entry, because `vue-tsc` won't resolve a bare-specifier ambient module imported inside a `.vue` SFC. `styleframe init` detects plain Vue projects (a `vue` dependency without `nuxt`) and writes that `paths` entry only for them, merging into any existing `paths`; non-Vue projects just get the includes. Nuxt is excluded because its module already injects the mapping via `prepare:types`, and writing `paths` into an `extends`-based Nuxt root tsconfig would replace (not merge) Nuxt's inherited aliases.
|
|
37
|
+
|
|
38
|
+
The Nuxt module registers the same `virtual:styleframe` path mapping into Nuxt's generated types via the `prepare:types` hook, so imports type-check without manual tsconfig changes. It also fixes the module's `configKey`/`name`, which were leftover `unpluginStarter` placeholders that caused `styleframe: {}` options in `nuxt.config` to be ignored.
|
|
39
|
+
|
|
40
|
+
- [#233](https://github.com/styleframe-dev/styleframe/pull/233) [`0ef38e6`](https://github.com/styleframe-dev/styleframe/commit/0ef38e69ca941cefab31463c23980f52cae1541f) Thanks [@alexgrozav](https://github.com/alexgrozav)! - Migrate from Vite 7 to Vite 8 with native Rolldown integration. Replace esbuild transforms with Oxc in the plugin, rename `rollupOptions` to `rolldownOptions`, upgrade `vite-plugin-dts` v4 to v5 (`rollupTypes` → `bundleTypes`), and bump vitest from v3 to v4.
|
|
41
|
+
|
|
42
|
+
- Updated dependencies [[`19e9866`](https://github.com/styleframe-dev/styleframe/commit/19e986618c669546972924840189fc5f16f1a1cd), [`0ef38e6`](https://github.com/styleframe-dev/styleframe/commit/0ef38e69ca941cefab31463c23980f52cae1541f)]:
|
|
43
|
+
- @styleframe/figma@2.1.0
|
|
44
|
+
- @styleframe/loader@3.0.2
|
|
45
|
+
|
|
3
46
|
## 4.0.0
|
|
4
47
|
|
|
5
48
|
### Major Changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { build, loadConfiguration } from "@styleframe/loader";
|
|
4
|
+
import consola from "consola";
|
|
5
|
+
//#region src/commands/build.ts
|
|
6
|
+
var build_default = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "build",
|
|
9
|
+
description: "Build Styleframe project from source files"
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
entry: {
|
|
13
|
+
type: "positional",
|
|
14
|
+
description: "Entry point file(s) for the build",
|
|
15
|
+
default: "styleframe.config.ts",
|
|
16
|
+
valueHint: "path"
|
|
17
|
+
},
|
|
18
|
+
outputDir: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Output directory for built files",
|
|
21
|
+
default: "styleframe",
|
|
22
|
+
alias: ["o", "out"],
|
|
23
|
+
valueHint: "path"
|
|
24
|
+
},
|
|
25
|
+
clean: {
|
|
26
|
+
type: "boolean",
|
|
27
|
+
description: "Clean output directory before build",
|
|
28
|
+
default: false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
async run({ args }) {
|
|
32
|
+
consola.info(`Loading configuration from "${path.relative(process.cwd(), path.resolve(args.entry))}"...`);
|
|
33
|
+
const instance = await loadConfiguration({ entry: args.entry });
|
|
34
|
+
consola.info("Building styleframe...");
|
|
35
|
+
try {
|
|
36
|
+
await build(instance, {
|
|
37
|
+
outputDir: args.outputDir,
|
|
38
|
+
clean: args.clean
|
|
39
|
+
});
|
|
40
|
+
consola.success("Styleframe built successfully!");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
consola.error("Failed to build Styleframe:", error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
//#endregion
|
|
47
|
+
export { build_default as default };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const require_chunk = require("./chunk-D6vf50IK.cjs");
|
|
2
|
+
let citty = require("citty");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
node_path = require_chunk.__toESM(node_path, 1);
|
|
5
|
+
let _styleframe_loader = require("@styleframe/loader");
|
|
6
|
+
let consola = require("consola");
|
|
7
|
+
consola = require_chunk.__toESM(consola, 1);
|
|
8
|
+
//#region src/commands/build.ts
|
|
9
|
+
var build_default = (0, citty.defineCommand)({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "build",
|
|
12
|
+
description: "Build Styleframe project from source files"
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
entry: {
|
|
16
|
+
type: "positional",
|
|
17
|
+
description: "Entry point file(s) for the build",
|
|
18
|
+
default: "styleframe.config.ts",
|
|
19
|
+
valueHint: "path"
|
|
20
|
+
},
|
|
21
|
+
outputDir: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Output directory for built files",
|
|
24
|
+
default: "styleframe",
|
|
25
|
+
alias: ["o", "out"],
|
|
26
|
+
valueHint: "path"
|
|
27
|
+
},
|
|
28
|
+
clean: {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
description: "Clean output directory before build",
|
|
31
|
+
default: false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async run({ args }) {
|
|
35
|
+
consola.default.info(`Loading configuration from "${node_path.default.relative(process.cwd(), node_path.default.resolve(args.entry))}"...`);
|
|
36
|
+
const instance = await (0, _styleframe_loader.loadConfiguration)({ entry: args.entry });
|
|
37
|
+
consola.default.info("Building styleframe...");
|
|
38
|
+
try {
|
|
39
|
+
await (0, _styleframe_loader.build)(instance, {
|
|
40
|
+
outputDir: args.outputDir,
|
|
41
|
+
clean: args.clean
|
|
42
|
+
});
|
|
43
|
+
consola.default.success("Styleframe built successfully!");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
consola.default.error("Failed to build Styleframe:", error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
//#endregion
|
|
50
|
+
exports.default = build_default;
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { classifyValue, formatAlias } from "@styleframe/dtcg";
|
|
2
|
+
//#region src/commands/dtcg/evaluate.ts
|
|
3
|
+
function isReference(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && "type" in value && value.type === "reference";
|
|
5
|
+
}
|
|
6
|
+
function isCss(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && "type" in value && value.type === "css";
|
|
8
|
+
}
|
|
9
|
+
function isPrimitive(value) {
|
|
10
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a Reference's *primitive* value (following alias chains). Returns
|
|
14
|
+
* null if the chain is broken (missing target or cycle).
|
|
15
|
+
*/
|
|
16
|
+
function resolveReferenceTarget(ref, context) {
|
|
17
|
+
const visited = context.visited ?? /* @__PURE__ */ new Set();
|
|
18
|
+
if (visited.has(ref.name)) return {
|
|
19
|
+
resolved: null,
|
|
20
|
+
reason: `Reference cycle: ${[...visited, ref.name].join(" → ")}`
|
|
21
|
+
};
|
|
22
|
+
const target = context.variables.get(ref.name);
|
|
23
|
+
if (!target) {
|
|
24
|
+
if (ref.fallback !== void 0 && ref.fallback !== null) return evaluateValue(ref.fallback, context);
|
|
25
|
+
return {
|
|
26
|
+
resolved: null,
|
|
27
|
+
reason: `Unknown reference target: ${ref.name}`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const nextContext = {
|
|
31
|
+
variables: context.variables,
|
|
32
|
+
visited: new Set([...visited, ref.name]),
|
|
33
|
+
maxViewport: context.maxViewport
|
|
34
|
+
};
|
|
35
|
+
return evaluateValue(target.value, nextContext);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Try to evaluate a CSS template literal. Two strategies:
|
|
39
|
+
*
|
|
40
|
+
* 1. **String-template fold**: replace each `Reference` part with its
|
|
41
|
+
* resolved primitive (stringified) and concatenate. If the resulting
|
|
42
|
+
* string parses as a known CSS form (cubic-bezier(...), a hex color,
|
|
43
|
+
* a dimension, a duration), return it.
|
|
44
|
+
* 2. **Pure arithmetic**: if every part reduces to a finite number or one
|
|
45
|
+
* of `+`/`-`/`*`/`/`/`(`/`)` operators with whitespace, evaluate the
|
|
46
|
+
* arithmetic and return the numeric result.
|
|
47
|
+
*
|
|
48
|
+
* Otherwise, return `{resolved: null, cssExpression: <fold>}` so the caller
|
|
49
|
+
* can preserve the expression in an extension.
|
|
50
|
+
*/
|
|
51
|
+
function evaluateCss(css, context) {
|
|
52
|
+
const parts = [];
|
|
53
|
+
let unevaluable = false;
|
|
54
|
+
let unevaluableReason;
|
|
55
|
+
for (const part of css.value) {
|
|
56
|
+
if (typeof part === "string") {
|
|
57
|
+
parts.push({
|
|
58
|
+
kind: "literal",
|
|
59
|
+
text: part
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const evaluated = evaluateValue(part, context);
|
|
64
|
+
if (evaluated.resolved === null) {
|
|
65
|
+
unevaluable = true;
|
|
66
|
+
unevaluableReason = evaluated.reason;
|
|
67
|
+
parts.push({
|
|
68
|
+
kind: "value",
|
|
69
|
+
text: ""
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const text = String(evaluated.resolved);
|
|
74
|
+
const numeric = typeof evaluated.resolved === "number" ? evaluated.resolved : void 0;
|
|
75
|
+
parts.push({
|
|
76
|
+
kind: "value",
|
|
77
|
+
text,
|
|
78
|
+
numeric
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const folded = parts.map((p) => p.text).join("");
|
|
82
|
+
if (unevaluable) return {
|
|
83
|
+
resolved: null,
|
|
84
|
+
cssExpression: folded,
|
|
85
|
+
reason: unevaluableReason ?? "Computed expression includes an unresolvable reference"
|
|
86
|
+
};
|
|
87
|
+
if (parts.every((p) => {
|
|
88
|
+
if (p.kind === "value") return p.numeric !== void 0;
|
|
89
|
+
return /^[\d.\s+\-*/()]*$/.test(p.text);
|
|
90
|
+
}) && parts.some((p) => p.kind === "value")) try {
|
|
91
|
+
const result = safeArithmetic(folded.trim());
|
|
92
|
+
if (typeof result === "number" && Number.isFinite(result)) return { resolved: result };
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return {
|
|
95
|
+
resolved: null,
|
|
96
|
+
cssExpression: folded,
|
|
97
|
+
reason: `Arithmetic evaluation failed: ${err.message}`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const subResult = substituteFluidUnits(folded, context.maxViewport ?? 1440);
|
|
101
|
+
if (subResult && /^[\d.\s+\-*/()]+$/.test(subResult.rebased)) try {
|
|
102
|
+
const result = safeArithmetic(subResult.rebased.trim());
|
|
103
|
+
if (typeof result === "number" && Number.isFinite(result)) return {
|
|
104
|
+
resolved: result,
|
|
105
|
+
...subResult.substituted ? { normalisationSource: "fluid-max" } : {}
|
|
106
|
+
};
|
|
107
|
+
} catch {}
|
|
108
|
+
return { resolved: folded };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Replace fluid-friendly literals so a `calc()` expression mixing `100vw`,
|
|
112
|
+
* `rem`, and `px` reduces to pure arithmetic.
|
|
113
|
+
*
|
|
114
|
+
* Returns `null` when the input still contains units we don't substitute
|
|
115
|
+
* (e.g. `vh`, `%`, `em`) — the caller should fall back to the
|
|
116
|
+
* verbatim-string path.
|
|
117
|
+
*
|
|
118
|
+
* `substituted` indicates whether at least one fluid unit was actually
|
|
119
|
+
* substituted. `false` means we only stripped a bare `calc(...)` wrapper —
|
|
120
|
+
* the expression was effectively pure arithmetic and the result should NOT
|
|
121
|
+
* be flagged as fluid-normalised.
|
|
122
|
+
*/
|
|
123
|
+
function substituteFluidUnits(expression, maxViewport) {
|
|
124
|
+
let substituted = false;
|
|
125
|
+
let rebased = expression.replace(/\bcalc\b/g, "");
|
|
126
|
+
if (rebased.includes("100vw")) {
|
|
127
|
+
rebased = rebased.replace(/100vw/g, String(maxViewport));
|
|
128
|
+
substituted = true;
|
|
129
|
+
}
|
|
130
|
+
if (/(-?\d*\.?\d+)\s*rem/.test(rebased)) {
|
|
131
|
+
rebased = rebased.replace(/(-?\d*\.?\d+)\s*rem/g, "($1 * 16)");
|
|
132
|
+
substituted = true;
|
|
133
|
+
}
|
|
134
|
+
if (/(-?\d*\.?\d+)\s*px/.test(rebased)) {
|
|
135
|
+
rebased = rebased.replace(/(-?\d*\.?\d+)\s*px/g, "$1");
|
|
136
|
+
substituted = true;
|
|
137
|
+
}
|
|
138
|
+
if (/[a-zA-Z%]/.test(rebased)) return null;
|
|
139
|
+
return {
|
|
140
|
+
rebased,
|
|
141
|
+
substituted
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Safe arithmetic over a whitespace-trimmed expression containing only
|
|
146
|
+
* numbers, parentheses, and `+`/`-`/`*`/`/`. Throws on anything else.
|
|
147
|
+
*/
|
|
148
|
+
function safeArithmetic(expression) {
|
|
149
|
+
if (!/^[\d.\s+\-*/()]+$/.test(expression)) throw new Error(`Disallowed characters in expression: "${expression}"`);
|
|
150
|
+
return new Function(`return (${expression});`)();
|
|
151
|
+
}
|
|
152
|
+
function evaluateValue(value, context) {
|
|
153
|
+
if (value === null || value === void 0) return {
|
|
154
|
+
resolved: null,
|
|
155
|
+
reason: "Value is null/undefined"
|
|
156
|
+
};
|
|
157
|
+
if (isPrimitive(value)) return { resolved: value };
|
|
158
|
+
if (isReference(value)) {
|
|
159
|
+
const targetResolution = resolveReferenceTarget(value, context);
|
|
160
|
+
if ((!context.visited || context.visited.size === 0) && targetResolution.resolved !== null) return {
|
|
161
|
+
...targetResolution,
|
|
162
|
+
aliasTarget: value.name
|
|
163
|
+
};
|
|
164
|
+
return targetResolution;
|
|
165
|
+
}
|
|
166
|
+
if (isCss(value)) return evaluateCss(value, context);
|
|
167
|
+
if (Array.isArray(value)) return {
|
|
168
|
+
resolved: null,
|
|
169
|
+
reason: "Heterogeneous array — DTCG composite encoding not yet implemented"
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
resolved: null,
|
|
173
|
+
reason: `Unsupported value shape: ${typeof value}`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/commands/dtcg/build-dtcg.ts
|
|
178
|
+
var TOKENS_SCHEMA_URL = "https://design-tokens.org/schemas/2025.10/tokens.json";
|
|
179
|
+
var RESOLVER_SCHEMA_URL = "https://www.designtokens.org/schemas/2025.10/resolver.json";
|
|
180
|
+
function buildVariableMap(root) {
|
|
181
|
+
const map = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const v of root.variables) map.set(v.name, v);
|
|
183
|
+
for (const theme of root.themes) for (const v of theme.variables) if (!map.has(v.name)) map.set(v.name, v);
|
|
184
|
+
return map;
|
|
185
|
+
}
|
|
186
|
+
function processVariable(variable, context) {
|
|
187
|
+
const evaluation = evaluateValue(variable.value, context);
|
|
188
|
+
const classification = classifyValueForVariable(variable.name, evaluation);
|
|
189
|
+
return {
|
|
190
|
+
name: variable.name,
|
|
191
|
+
evaluation,
|
|
192
|
+
classification
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function classifyValueForVariable(name, evaluation) {
|
|
196
|
+
if (evaluation.resolved === null) return void 0;
|
|
197
|
+
return classifyValue(evaluation.resolved, { path: name });
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Place a token at `dotPath` in `doc`. Two collision cases need handling:
|
|
201
|
+
*
|
|
202
|
+
* 1. **Token at a path whose group already has children.** If we set
|
|
203
|
+
* `border-width = {...token}` after `border-width.thin = {...}` was
|
|
204
|
+
* written, a naive overwrite drops the children. Instead, we promote
|
|
205
|
+
* the parent token into a `$root` slot inside the existing group.
|
|
206
|
+
* 2. **Children written into a slot that's already a token.** Reverse
|
|
207
|
+
* ordering: if `border-width = {...token}` was written first and we
|
|
208
|
+
* now need to add `border-width.thin = {...}`, we move the existing
|
|
209
|
+
* token into `$root` and continue descending into the new group.
|
|
210
|
+
*
|
|
211
|
+
* Both cases are handled by checking whether the slot looks like a token
|
|
212
|
+
* (`$value` present) or a group, and by upgrading to `$root` whenever a
|
|
213
|
+
* token coexists with siblings at the same path.
|
|
214
|
+
*/
|
|
215
|
+
function setNestedToken(doc, dotPath, token) {
|
|
216
|
+
const segments = dotPath.split(".");
|
|
217
|
+
let cursor = doc;
|
|
218
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
219
|
+
const segment = segments[i];
|
|
220
|
+
const next = cursor[segment];
|
|
221
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) cursor[segment] = {};
|
|
222
|
+
else if ("$value" in next) cursor[segment] = { $root: next };
|
|
223
|
+
cursor = cursor[segment];
|
|
224
|
+
}
|
|
225
|
+
const leaf = segments[segments.length - 1];
|
|
226
|
+
const existing = cursor[leaf];
|
|
227
|
+
if (typeof existing === "object" && existing !== null && !Array.isArray(existing) && !("$value" in existing)) existing.$root = token;
|
|
228
|
+
else cursor[leaf] = token;
|
|
229
|
+
}
|
|
230
|
+
function hasRootToken(doc, dotPath) {
|
|
231
|
+
const segments = dotPath.split(".");
|
|
232
|
+
let cursor = doc;
|
|
233
|
+
for (const segment of segments) {
|
|
234
|
+
if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) return false;
|
|
235
|
+
cursor = cursor[segment];
|
|
236
|
+
}
|
|
237
|
+
return typeof cursor === "object" && cursor !== null && !Array.isArray(cursor) && "$root" in cursor;
|
|
238
|
+
}
|
|
239
|
+
function setNestedOverride(context, dotPath, value, type) {
|
|
240
|
+
const segments = dotPath.split(".");
|
|
241
|
+
let cursor = context;
|
|
242
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
243
|
+
const segment = segments[i];
|
|
244
|
+
const next = cursor[segment];
|
|
245
|
+
if (typeof next !== "object" || next === null || Array.isArray(next) || "$value" in next) cursor[segment] = {};
|
|
246
|
+
cursor = cursor[segment];
|
|
247
|
+
}
|
|
248
|
+
const token = { $value: value };
|
|
249
|
+
if (type !== void 0) token.$type = type;
|
|
250
|
+
cursor[segments[segments.length - 1]] = token;
|
|
251
|
+
}
|
|
252
|
+
function makeAliasToken(target, type, description) {
|
|
253
|
+
const token = { $value: formatAlias(target) };
|
|
254
|
+
if (type !== void 0) token.$type = type;
|
|
255
|
+
if (description) token.$description = description;
|
|
256
|
+
return token;
|
|
257
|
+
}
|
|
258
|
+
function makeValueToken(classification, description, fluidBound) {
|
|
259
|
+
const token = {
|
|
260
|
+
$value: classification.value,
|
|
261
|
+
$type: classification.type
|
|
262
|
+
};
|
|
263
|
+
if (description) token.$description = description;
|
|
264
|
+
if (fluidBound) token.$extensions = { "dev.styleframe": { fluidBound } };
|
|
265
|
+
return token;
|
|
266
|
+
}
|
|
267
|
+
function makeExpressionToken(expression, description) {
|
|
268
|
+
const token = {
|
|
269
|
+
$value: expression,
|
|
270
|
+
$extensions: { "dev.styleframe": { expression } }
|
|
271
|
+
};
|
|
272
|
+
if (description) token.$description = description;
|
|
273
|
+
return token;
|
|
274
|
+
}
|
|
275
|
+
function valuesEqual(a, b) {
|
|
276
|
+
if (a === b) return true;
|
|
277
|
+
if (typeof a !== typeof b) return false;
|
|
278
|
+
if (typeof a === "object") return JSON.stringify(a) === JSON.stringify(b);
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
function capitalizeContextName(name) {
|
|
282
|
+
if (name.length === 0) return name;
|
|
283
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
284
|
+
}
|
|
285
|
+
function buildDTCG(root, options = {}) {
|
|
286
|
+
const includeSchema = options.includeSchema ?? true;
|
|
287
|
+
const schemaUrl = options.schemaUrl ?? TOKENS_SCHEMA_URL;
|
|
288
|
+
const modifierName = options.modifierName ?? "theme";
|
|
289
|
+
const collectionName = options.collectionName ?? "Design Tokens";
|
|
290
|
+
const defaultModeName = options.defaultModeName ?? "Default";
|
|
291
|
+
const tokensSourceRef = options.tokensSourceRef ?? "tokens.json";
|
|
292
|
+
const variableMap = buildVariableMap(root);
|
|
293
|
+
const fluidMaxVariable = variableMap.get("fluid.max-width");
|
|
294
|
+
const fluidMaxResolved = fluidMaxVariable ? evaluateValue(fluidMaxVariable.value, { variables: variableMap }).resolved : null;
|
|
295
|
+
const maxViewport = typeof fluidMaxResolved === "number" ? fluidMaxResolved : 1440;
|
|
296
|
+
const context = {
|
|
297
|
+
variables: variableMap,
|
|
298
|
+
maxViewport
|
|
299
|
+
};
|
|
300
|
+
const processed = /* @__PURE__ */ new Map();
|
|
301
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
302
|
+
for (const variable of root.variables) {
|
|
303
|
+
const p = processVariable(variable, context);
|
|
304
|
+
processed.set(variable.name, p);
|
|
305
|
+
if (p.classification) typeMap.set(variable.name, p.classification.type);
|
|
306
|
+
}
|
|
307
|
+
const resolveTypeFromAliasChain = (name, seen) => {
|
|
308
|
+
if (seen.has(name)) return void 0;
|
|
309
|
+
const direct = typeMap.get(name);
|
|
310
|
+
if (direct) return direct;
|
|
311
|
+
const p = processed.get(name);
|
|
312
|
+
if (p?.evaluation.aliasTarget) return resolveTypeFromAliasChain(p.evaluation.aliasTarget, new Set([...seen, name]));
|
|
313
|
+
};
|
|
314
|
+
const tokens = {};
|
|
315
|
+
if (includeSchema) tokens.$schema = schemaUrl;
|
|
316
|
+
tokens.$extensions = { "dev.styleframe": { collection: collectionName } };
|
|
317
|
+
const diagnostics = [];
|
|
318
|
+
let emittedCount = 0;
|
|
319
|
+
let fluidNormalisedCount = 0;
|
|
320
|
+
for (const variable of root.variables) {
|
|
321
|
+
const p = processed.get(variable.name);
|
|
322
|
+
if (!p) continue;
|
|
323
|
+
const { evaluation, classification } = p;
|
|
324
|
+
if (evaluation.aliasTarget) {
|
|
325
|
+
const targetType = resolveTypeFromAliasChain(evaluation.aliasTarget, /* @__PURE__ */ new Set());
|
|
326
|
+
setNestedToken(tokens, variable.name, makeAliasToken(evaluation.aliasTarget, targetType));
|
|
327
|
+
emittedCount++;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (classification) {
|
|
331
|
+
const fluidBound = evaluation.normalisationSource === "fluid-max" ? "max" : void 0;
|
|
332
|
+
const promoted = fluidBound && classification.type === "number" && typeof classification.value === "number" ? {
|
|
333
|
+
type: "dimension",
|
|
334
|
+
value: {
|
|
335
|
+
value: classification.value,
|
|
336
|
+
unit: "px"
|
|
337
|
+
}
|
|
338
|
+
} : classification;
|
|
339
|
+
setNestedToken(tokens, variable.name, makeValueToken(promoted, void 0, fluidBound));
|
|
340
|
+
if (fluidBound) fluidNormalisedCount++;
|
|
341
|
+
emittedCount++;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (evaluation.cssExpression) {
|
|
345
|
+
setNestedToken(tokens, variable.name, makeExpressionToken(evaluation.cssExpression));
|
|
346
|
+
diagnostics.push({
|
|
347
|
+
name: variable.name,
|
|
348
|
+
level: "warn",
|
|
349
|
+
reason: evaluation.reason ?? "Computed expression — preserved as dev.styleframe.expression extension"
|
|
350
|
+
});
|
|
351
|
+
emittedCount++;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (evaluation.resolved !== null && typeof evaluation.resolved !== "boolean") {
|
|
355
|
+
setNestedToken(tokens, variable.name, {
|
|
356
|
+
$value: String(evaluation.resolved),
|
|
357
|
+
$extensions: { "dev.styleframe": { unknownType: true } }
|
|
358
|
+
});
|
|
359
|
+
diagnostics.push({
|
|
360
|
+
name: variable.name,
|
|
361
|
+
level: "warn",
|
|
362
|
+
reason: "Could not infer DTCG $type — emitted as untyped string"
|
|
363
|
+
});
|
|
364
|
+
emittedCount++;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
diagnostics.push({
|
|
368
|
+
name: variable.name,
|
|
369
|
+
level: "warn",
|
|
370
|
+
reason: evaluation.reason ?? "Unrepresentable value — skipped"
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const themedThemes = root.themes.filter((t) => t.variables.length > 0);
|
|
374
|
+
let resolver;
|
|
375
|
+
if (themedThemes.length > 0) {
|
|
376
|
+
const contexts = {};
|
|
377
|
+
for (const theme of themedThemes) {
|
|
378
|
+
const contextDoc = {};
|
|
379
|
+
for (const variable of theme.variables) {
|
|
380
|
+
const themeProcessed = processVariable(variable, context);
|
|
381
|
+
const defaultProcessed = processed.get(variable.name);
|
|
382
|
+
let themeValue;
|
|
383
|
+
let themeType;
|
|
384
|
+
if (themeProcessed.evaluation.aliasTarget) {
|
|
385
|
+
themeValue = formatAlias(themeProcessed.evaluation.aliasTarget);
|
|
386
|
+
themeType = resolveTypeFromAliasChain(themeProcessed.evaluation.aliasTarget, /* @__PURE__ */ new Set());
|
|
387
|
+
} else if (themeProcessed.classification) {
|
|
388
|
+
themeValue = themeProcessed.classification.value;
|
|
389
|
+
themeType = themeProcessed.classification.type;
|
|
390
|
+
}
|
|
391
|
+
if (themeValue === void 0) continue;
|
|
392
|
+
let defaultValue;
|
|
393
|
+
if (defaultProcessed?.evaluation.aliasTarget) defaultValue = formatAlias(defaultProcessed.evaluation.aliasTarget);
|
|
394
|
+
else if (defaultProcessed?.classification) defaultValue = defaultProcessed.classification.value;
|
|
395
|
+
if (defaultValue !== void 0 && valuesEqual(themeValue, defaultValue)) continue;
|
|
396
|
+
setNestedOverride(contextDoc, hasRootToken(tokens, variable.name) ? `${variable.name}.$root` : variable.name, themeValue, themeType);
|
|
397
|
+
}
|
|
398
|
+
contexts[capitalizeContextName(theme.name)] = contextDoc;
|
|
399
|
+
}
|
|
400
|
+
for (const key of Object.keys(contexts)) if (Object.keys(contexts[key]).length === 0) delete contexts[key];
|
|
401
|
+
const themeContextNames = Object.keys(contexts);
|
|
402
|
+
if (themeContextNames.length > 0) {
|
|
403
|
+
const allContexts = { [defaultModeName]: [] };
|
|
404
|
+
for (const name of themeContextNames) allContexts[name] = [contexts[name]];
|
|
405
|
+
resolver = {
|
|
406
|
+
$schema: RESOLVER_SCHEMA_URL,
|
|
407
|
+
version: "2025.10",
|
|
408
|
+
modifiers: { [modifierName]: {
|
|
409
|
+
contexts: allContexts,
|
|
410
|
+
default: defaultModeName
|
|
411
|
+
} },
|
|
412
|
+
resolutionOrder: [{
|
|
413
|
+
type: "set",
|
|
414
|
+
sources: [{ $ref: tokensSourceRef }]
|
|
415
|
+
}, { $ref: `#/modifiers/${modifierName}` }]
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
tokens,
|
|
421
|
+
resolver,
|
|
422
|
+
diagnostics,
|
|
423
|
+
emittedCount,
|
|
424
|
+
fluidNormalisedCount,
|
|
425
|
+
maxViewport
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
//#endregion
|
|
429
|
+
export { buildDTCG as t };
|