@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 +21 -0
- package/README.md +141 -0
- package/dist/__tests__/parser.test.d.ts +1 -0
- package/dist/__tests__/parser.test.js +143 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/parser.d.ts +16 -0
- package/dist/parser.js +161 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +1 -0
- package/package.json +48 -0
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
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseTokens, defineTokens, toCssVarName } from "./parser.js";
|
package/dist/parser.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|