@vte-js/core 1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vte-js
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # @vte/core
2
+
3
+ Vue Token Engine 的核心包,提供 token 解析和类型工具。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @vte/core
9
+ # 或
10
+ pnpm add @vte/core
11
+ ```
12
+
13
+ ## API
14
+
15
+ ### `defineTokens<T>(tokens)`
16
+
17
+ 定义设计 tokens,返回类型安全的 token 配置。
18
+
19
+ ```typescript
20
+ import { defineTokens } from "@vte/core";
21
+
22
+ const tokens = defineTokens({
23
+ primitive: {
24
+ blue: {
25
+ 500: "#3b82f6",
26
+ },
27
+ },
28
+ semantic: {
29
+ color: {
30
+ primary: "{primitive.blue.500}", // 引用语法
31
+ background: "#ffffff",
32
+ },
33
+ },
34
+ });
35
+ ```
36
+
37
+ ### `parseTokens(sourceFile)`
38
+
39
+ 解析 token 文件,返回扁平化的 TokenMap。
40
+
41
+ ```typescript
42
+ import { parseTokens } from "@vte/core";
43
+
44
+ const tokenMap = await parseTokens("./design-tokens.ts");
45
+
46
+ // tokenMap 是 Map<string, TokenValue>
47
+ // Key: "semantic.color.primary"
48
+ // Value: { path, value, raw, refs }
49
+ ```
50
+
51
+ ### `TokenPath<T>` 类型
52
+
53
+ 从 token 定义中提取所有合法路径的联合类型,用于 IDE 自动补全。
54
+
55
+ ```typescript
56
+ import { type TokenPath } from "@vte/core";
57
+
58
+ type Paths = TokenPath<typeof tokens>;
59
+ // "primitive" | "primitive.blue" | "primitive.blue.500" | ...
60
+ ```
61
+
62
+ ### `TokenMap` 类型
63
+
64
+ ```typescript
65
+ interface TokenValue {
66
+ path: string; // token 路径
67
+ value: string; // 解析后的值
68
+ raw: string; // 原始值
69
+ refs: string[]; // 引用链
70
+ }
71
+
72
+ type TokenMap = Map<string, TokenValue>;
73
+ ```
74
+
75
+ ## Token 语法
76
+
77
+ ### 直接值
78
+
79
+ ```typescript
80
+ defineTokens({
81
+ color: {
82
+ primary: "#3b82f6",
83
+ spacing: "0.5rem",
84
+ },
85
+ });
86
+ ```
87
+
88
+ ### 引用语法
89
+
90
+ 使用 `{path}` 语法引用其他 token:
91
+
92
+ ```typescript
93
+ defineTokens({
94
+ primitive: {
95
+ blue: { 500: "#3b82f6" },
96
+ },
97
+ semantic: {
98
+ color: {
99
+ primary: "{primitive.blue.500}", // 引用 primitive.blue.500
100
+ },
101
+ },
102
+ });
103
+ ```
104
+
105
+ ### 支持链式引用
106
+
107
+ ```typescript
108
+ defineTokens({
109
+ primitive: { blue: { 500: "#3b82f6" } },
110
+ semantic: { color: { primary: "{primitive.blue.500}" } },
111
+ component: { button: { bg: "{semantic.color.primary}" } },
112
+ });
113
+ // component.button.bg → semantic.color.primary → primitive.blue.500 → #3b82f6
114
+ ```
115
+
116
+ ## 错误处理
117
+
118
+ ### 循环引用
119
+
120
+ ```typescript
121
+ // ❌ 会抛出错误
122
+ defineTokens({
123
+ a: "{b}",
124
+ b: "{a}",
125
+ });
126
+ // Error: [VTE] Circular reference detected: a → b → a
127
+ ```
128
+
129
+ ### 未解析引用
130
+
131
+ ```typescript
132
+ // ❌ 会抛出错误
133
+ defineTokens({
134
+ color: "{nonexistent.path}",
135
+ });
136
+ // Error: [VTE] Unresolved reference: "{nonexistent.path}" in token "color"
137
+ ```
138
+
139
+ ## License
140
+
141
+ ISC
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import path from "path";
3
+ import { parseTokens, defineTokens } from "../index.js";
4
+ const playgroundDir = path.resolve(__dirname, "../../../../playground");
5
+ describe("parseTokens", () => {
6
+ it("should parse design tokens from file", async () => {
7
+ const tokenPath = path.join(playgroundDir, "design-tokens.ts");
8
+ const map = await parseTokens(tokenPath);
9
+ expect(map).toBeDefined();
10
+ expect(map.size).toBeGreaterThan(0);
11
+ });
12
+ it("should flatten nested objects to dot notation", async () => {
13
+ const tokenPath = path.join(playgroundDir, "design-tokens.ts");
14
+ const map = await parseTokens(tokenPath);
15
+ expect(map.has("primitive.blue.500")).toBe(true);
16
+ expect(map.has("semantic.color.primary")).toBe(true);
17
+ expect(map.has("component.button.height")).toBe(true);
18
+ });
19
+ it("should resolve token references", async () => {
20
+ const tokenPath = path.join(playgroundDir, "design-tokens.ts");
21
+ const map = await parseTokens(tokenPath);
22
+ const primary = map.get("semantic.color.primary");
23
+ expect(primary?.value).toBe("#3b82f6");
24
+ const buttonHeight = map.get("component.button.height");
25
+ expect(buttonHeight?.value).toBe("1rem");
26
+ });
27
+ it("should resolve chain references", async () => {
28
+ const tokenPath = path.join(playgroundDir, "design-tokens.ts");
29
+ const map = await parseTokens(tokenPath);
30
+ // component.button.height -> semantic.spacing.md -> "1rem"
31
+ const buttonHeight = map.get("component.button.height");
32
+ expect(buttonHeight?.value).toBe("1rem");
33
+ expect(buttonHeight?.refs).toEqual(["semantic.spacing.md"]);
34
+ });
35
+ it("should throw on circular references", async () => {
36
+ const tokenPath = path.join(playgroundDir, "design-tokens-circular.ts");
37
+ await expect(parseTokens(tokenPath)).rejects.toThrow(/\[VTE\] Circular reference detected/);
38
+ });
39
+ it("should throw on unresolved references", async () => {
40
+ // Create a temporary file with unresolved reference
41
+ const fs = await import("fs");
42
+ const tmpFile = path.join(playgroundDir, "__tmp_unresolved.ts");
43
+ fs.writeFileSync(tmpFile, `
44
+ export default {
45
+ color: {
46
+ primary: "{nonexistent.path}",
47
+ },
48
+ };
49
+ `);
50
+ try {
51
+ await expect(parseTokens(tmpFile)).rejects.toThrow(/\[VTE\] Unresolved reference/);
52
+ }
53
+ finally {
54
+ fs.unlinkSync(tmpFile);
55
+ }
56
+ });
57
+ it("should store raw value for reference tokens", async () => {
58
+ const tokenPath = path.join(playgroundDir, "design-tokens.ts");
59
+ const map = await parseTokens(tokenPath);
60
+ const primary = map.get("semantic.color.primary");
61
+ expect(primary?.raw).toBe("{primitive.blue.500}");
62
+ expect(primary?.refs).toEqual(["primitive.blue.500"]);
63
+ });
64
+ it("should store string value for primitive tokens", async () => {
65
+ const tokenPath = path.join(playgroundDir, "design-tokens.ts");
66
+ const map = await parseTokens(tokenPath);
67
+ const blue500 = map.get("primitive.blue.500");
68
+ expect(blue500?.value).toBe("#3b82f6");
69
+ expect(blue500?.raw).toBe("#3b82f6");
70
+ expect(blue500?.refs).toEqual([]);
71
+ });
72
+ it("should handle numeric string values", async () => {
73
+ const fs = await import("fs");
74
+ const tmpFile = path.join(playgroundDir, "__tmp_numeric.ts");
75
+ fs.writeFileSync(tmpFile, `
76
+ export default {
77
+ spacing: {
78
+ small: 8,
79
+ medium: 16,
80
+ },
81
+ };
82
+ `);
83
+ try {
84
+ const map = await parseTokens(tmpFile);
85
+ expect(map.get("spacing.small")?.value).toBe("8");
86
+ expect(map.get("spacing.medium")?.value).toBe("16");
87
+ }
88
+ finally {
89
+ fs.unlinkSync(tmpFile);
90
+ }
91
+ });
92
+ it("should handle deeply nested tokens", async () => {
93
+ const fs = await import("fs");
94
+ const tmpFile = path.join(playgroundDir, "__tmp_deep.ts");
95
+ fs.writeFileSync(tmpFile, `
96
+ export default {
97
+ a: {
98
+ b: {
99
+ c: {
100
+ d: "deep-value",
101
+ },
102
+ },
103
+ },
104
+ };
105
+ `);
106
+ try {
107
+ const map = await parseTokens(tmpFile);
108
+ expect(map.has("a.b.c.d")).toBe(true);
109
+ expect(map.get("a.b.c.d")?.value).toBe("deep-value");
110
+ }
111
+ finally {
112
+ fs.unlinkSync(tmpFile);
113
+ }
114
+ });
115
+ });
116
+ describe("defineTokens", () => {
117
+ it("should return the input object", () => {
118
+ const tokens = defineTokens({
119
+ color: {
120
+ primary: "#3b82f6",
121
+ },
122
+ });
123
+ expect(tokens).toEqual({ color: { primary: "#3b82f6" } });
124
+ });
125
+ it("should preserve nested structure", () => {
126
+ const tokens = defineTokens({
127
+ semantic: {
128
+ color: {
129
+ primary: "#3b82f6",
130
+ },
131
+ },
132
+ });
133
+ expect(tokens.semantic.color.primary).toBe("#3b82f6");
134
+ });
135
+ it("should preserve reference syntax in values", () => {
136
+ const tokens = defineTokens({
137
+ color: {
138
+ primary: "{primitive.blue.500}",
139
+ },
140
+ });
141
+ expect(tokens.color.primary).toBe("{primitive.blue.500}");
142
+ });
143
+ });
@@ -0,0 +1,3 @@
1
+ export { parseTokens, defineTokens, toCssVarName } from "./parser.js";
2
+ export type { TokenValue, TokenMap } from "./parser.js";
3
+ export type { DeepReadonly, FlattenPaths, TokenPath, TokenRef, TokenDefinition, TokenConfig, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { parseTokens, defineTokens, toCssVarName } from "./parser.js";
@@ -0,0 +1,16 @@
1
+ import type { TokenDefinition, TokenConfig } from "./types.js";
2
+ export interface TokenValue {
3
+ path: string;
4
+ value: string;
5
+ raw: string;
6
+ refs: string[];
7
+ }
8
+ export type TokenMap = Map<string, TokenValue>;
9
+ /**
10
+ * Convert a token path to a CSS variable name.
11
+ * @example toCssVarName("semantic.color.primary") => "--vte-semantic-color-primary"
12
+ * @example toCssVarName("semantic.color.primary", "my") => "--my-semantic-color-primary"
13
+ */
14
+ export declare function toCssVarName(tokenPath: string, prefix?: string): string;
15
+ export declare function parseTokens(sourceFile: string): Promise<TokenMap>;
16
+ export declare function defineTokens<T extends object>(tokens: TokenDefinition<T>): TokenConfig<T>;
package/dist/parser.js ADDED
@@ -0,0 +1,161 @@
1
+ import { transformFile } from "@swc/core";
2
+ import vm from "vm";
3
+ import path from "path";
4
+ /**
5
+ * Convert a token path to a CSS variable name.
6
+ * @example toCssVarName("semantic.color.primary") => "--vte-semantic-color-primary"
7
+ * @example toCssVarName("semantic.color.primary", "my") => "--my-semantic-color-primary"
8
+ */
9
+ export function toCssVarName(tokenPath, prefix = "vte") {
10
+ return `--${prefix}-${tokenPath.replace(/\./g, "-")}`;
11
+ }
12
+ /**
13
+ * Load and evaluate a token file using @swc/core for AST transformation.
14
+ * This replaces the unsafe require() approach with proper TS parsing.
15
+ */
16
+ async function loadTokenFile(sourceFile) {
17
+ const resolvedPath = path.resolve(sourceFile);
18
+ const result = await transformFile(resolvedPath, {
19
+ jsc: {
20
+ parser: {
21
+ syntax: "typescript",
22
+ decorators: true,
23
+ },
24
+ target: "es2020",
25
+ },
26
+ module: {
27
+ type: "commonjs",
28
+ },
29
+ isModule: true,
30
+ });
31
+ const transpiledCode = result.code;
32
+ // Create a sandboxed context with require and module exports
33
+ const moduleExports = {};
34
+ const moduleObj = { exports: moduleExports };
35
+ const sandbox = {
36
+ require: (id) => {
37
+ // Allow @vte-js/core for defineTokens
38
+ if (id === "@vte-js/core" || id.endsWith("/vte-core/dist/index.js")) {
39
+ return {
40
+ defineTokens: (tokens) => tokens,
41
+ };
42
+ }
43
+ // Allow node built-in modules
44
+ if (id === "path" || id === "fs" || id === "os") {
45
+ return require(id);
46
+ }
47
+ throw new Error(`[VTE] Cannot require "${id}" in token files`);
48
+ },
49
+ module: moduleObj,
50
+ exports: moduleExports,
51
+ console,
52
+ setTimeout,
53
+ setInterval,
54
+ clearTimeout,
55
+ clearInterval,
56
+ };
57
+ // Execute the transpiled code in sandbox
58
+ const script = new vm.Script(transpiledCode, {
59
+ filename: resolvedPath,
60
+ });
61
+ const context = vm.createContext(sandbox);
62
+ script.runInContext(context);
63
+ // Get the exported value (handle both default and named exports)
64
+ const rawTokens = moduleObj.exports.default ?? moduleObj.exports;
65
+ return rawTokens;
66
+ }
67
+ export async function parseTokens(sourceFile) {
68
+ const rawTokens = await loadTokenFile(sourceFile);
69
+ const tokenMap = new Map();
70
+ const refGraph = {};
71
+ function flatten(obj, prefix = "") {
72
+ for (const key in obj) {
73
+ const path = prefix ? `${prefix}.${key}` : key;
74
+ const value = obj[key];
75
+ if (typeof value === "string" &&
76
+ value.startsWith("{") &&
77
+ value.endsWith("}")) {
78
+ const refPath = value.slice(1, -1);
79
+ refGraph[path] = [refPath];
80
+ tokenMap.set(path, {
81
+ path,
82
+ value: "",
83
+ raw: value,
84
+ refs: [refPath],
85
+ });
86
+ }
87
+ else if (typeof value === "object" && value !== null) {
88
+ flatten(value, path);
89
+ }
90
+ else {
91
+ tokenMap.set(path, {
92
+ path,
93
+ value: String(value),
94
+ raw: String(value),
95
+ refs: [],
96
+ });
97
+ }
98
+ }
99
+ }
100
+ flatten(rawTokens);
101
+ // 循环引用检测:DFS 沿引用链遍历,发现环则抛出带路径的错误
102
+ function detectCircularRefs() {
103
+ const visiting = new Set();
104
+ const visited = new Set();
105
+ function dfs(node, chain) {
106
+ if (visited.has(node))
107
+ return;
108
+ if (visiting.has(node)) {
109
+ const cycleStart = chain.indexOf(node);
110
+ const cycle = chain.slice(cycleStart).concat(node);
111
+ throw new Error(`[VTE] Circular reference detected: ${cycle.join(" → ")}`);
112
+ }
113
+ visiting.add(node);
114
+ chain.push(node);
115
+ const refs = refGraph[node];
116
+ if (refs) {
117
+ for (const ref of refs) {
118
+ dfs(ref, chain);
119
+ }
120
+ }
121
+ chain.pop();
122
+ visiting.delete(node);
123
+ visited.add(node);
124
+ }
125
+ for (const node of Object.keys(refGraph)) {
126
+ dfs(node, []);
127
+ }
128
+ }
129
+ detectCircularRefs();
130
+ // BFS 解析引用
131
+ function resolveRefs() {
132
+ let changed = true;
133
+ const maxDepth = 10;
134
+ let depth = 0;
135
+ while (changed && depth < maxDepth) {
136
+ changed = false;
137
+ depth++;
138
+ for (const [path, token] of tokenMap) {
139
+ if (token.refs.length > 0 && !token.value) {
140
+ const refPath = token.refs[0];
141
+ const refToken = tokenMap.get(refPath);
142
+ if (refToken?.value) {
143
+ token.value = refToken.value;
144
+ changed = true;
145
+ }
146
+ }
147
+ }
148
+ }
149
+ for (const [path, token] of tokenMap) {
150
+ if (token.refs.length > 0 && !token.value) {
151
+ throw new Error(`[VTE] Unresolved reference: "${token.raw}" in token "${path}". ` +
152
+ `Check if "${token.refs[0]}" exists.`);
153
+ }
154
+ }
155
+ }
156
+ resolveRefs();
157
+ return tokenMap;
158
+ }
159
+ export function defineTokens(tokens) {
160
+ return tokens;
161
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 深度只读类型
3
+ */
4
+ export type DeepReadonly<T> = T extends (infer U)[] ? ReadonlyArray<DeepReadonly<U>> : T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends object ? {
5
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
6
+ } : T;
7
+ /**
8
+ * 将嵌套对象展平为点路径联合类型
9
+ * 例如: { a: { b: { c: 1 } } } => "a" | "a.b" | "a.b.c"
10
+ */
11
+ export type FlattenPaths<T, Prefix extends string = ""> = T extends object ? {
12
+ [K in keyof T & string]: T[K] extends object ? FlattenPaths<T[K], Prefix extends "" ? K : `${Prefix}.${K}`> : Prefix extends "" ? K : `${Prefix}.${K}`;
13
+ }[keyof T & string] : never;
14
+ /**
15
+ * Token 路径类型:从 token 定义中提取所有合法的点路径
16
+ * 用于 IDE 自动补全 $token.path
17
+ *
18
+ * @example
19
+ * const tokens = defineTokens({ semantic: { color: { primary: "#3b82f6" } } });
20
+ * type Paths = TokenPath<typeof tokens>;
21
+ * // "semantic" | "semantic.color" | "semantic.color.primary"
22
+ *
23
+ * // 在 Vue 组件中使用
24
+ * type ValidPath = TokenPath<typeof import("./design-tokens").default>;
25
+ * const path: ValidPath = "$semantic.color.primary"; // ✅
26
+ */
27
+ export type TokenPath<T> = T extends object ? FlattenPaths<T> : never;
28
+ /**
29
+ * Token 引用类型: {path.to.token}
30
+ */
31
+ export type TokenRef<P extends string> = `{${P}}`;
32
+ /**
33
+ * defineTokens 的输入类型约束
34
+ * - 所有叶子值必须是 string
35
+ * - 引用值必须符合 {path} 格式
36
+ */
37
+ export type TokenDefinition<T> = {
38
+ [K in keyof T]: T[K] extends string ? T[K] extends `{${string}}` ? T[K] : T[K] : T[K] extends object ? TokenDefinition<T[K]> : never;
39
+ };
40
+ /**
41
+ * defineTokens 返回类型:深度只读
42
+ */
43
+ export type TokenConfig<T> = DeepReadonly<T>;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@vte-js/core",
3
+ "version": "1.0.0",
4
+ "description": "Core parser and type definitions for Vue Token Engine",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "@swc/core": "^1.3.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^26.1.0"
26
+ },
27
+ "keywords": [
28
+ "vue",
29
+ "design-tokens",
30
+ "css-variables",
31
+ "core"
32
+ ],
33
+ "author": "vte-js",
34
+ "license": "ISC",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/vte-js/vte.git",
38
+ "directory": "packages/vte-core"
39
+ },
40
+ "homepage": "https://github.com/vte-js/vte/tree/main/packages/vte-core",
41
+ "bugs": {
42
+ "url": "https://github.com/vte-js/vte/issues"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc",
46
+ "dev": "tsc --watch"
47
+ }
48
+ }