@tokens-studio/tokenscript-schemas 0.1.2 → 0.2.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/README.md +36 -7
- package/dist/cli/index.cjs +142 -88
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +141 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +19 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +19 -19
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bundler/{bundle-schema.ts → build-schema.ts} +2 -2
- package/src/bundler/index.ts +25 -25
- package/src/bundler/schema-dependency-resolver.ts +3 -3
- package/src/bundler/selective-bundler.ts +3 -3
- package/src/cli/commands/build-dir.test.ts +354 -0
- package/src/cli/commands/build-dir.ts +90 -0
- package/src/cli/commands/bundle.test.ts +95 -1
- package/src/cli/commands/bundle.ts +22 -15
- package/src/cli/index.ts +16 -0
- package/bundled/functions/adjust_chroma.json +0 -60
- package/bundled/functions/adjust_hue.json +0 -60
- package/bundled/functions/adjust_lightness.json +0 -60
- package/bundled/functions/adjust_to_contrast.json +0 -67
- package/bundled/functions/alpha_blend.json +0 -31
- package/bundled/functions/alpha_scale.json +0 -27
- package/bundled/functions/analogous.json +0 -32
- package/bundled/functions/apca_contrast.json +0 -27
- package/bundled/functions/are_similar.json +0 -73
- package/bundled/functions/auto_text_color.json +0 -66
- package/bundled/functions/best_contrast.json +0 -28
- package/bundled/functions/chroma.json +0 -54
- package/bundled/functions/clamp_chroma.json +0 -66
- package/bundled/functions/clamp_lightness.json +0 -66
- package/bundled/functions/clamp_to_gamut.json +0 -23
- package/bundled/functions/complement.json +0 -24
- package/bundled/functions/contrast_ratio.json +0 -27
- package/bundled/functions/cooler.json +0 -52
- package/bundled/functions/darken.json +0 -28
- package/bundled/functions/delta_e_2000.json +0 -40
- package/bundled/functions/delta_e_76.json +0 -27
- package/bundled/functions/delta_e_ok.json +0 -27
- package/bundled/functions/desaturate.json +0 -28
- package/bundled/functions/distributed.json +0 -36
- package/bundled/functions/diverging.json +0 -36
- package/bundled/functions/grayscale.json +0 -24
- package/bundled/functions/harmonize.json +0 -65
- package/bundled/functions/hue.json +0 -54
- package/bundled/functions/hue_difference.json +0 -27
- package/bundled/functions/in_gamut.json +0 -27
- package/bundled/functions/interpolate.json +0 -66
- package/bundled/functions/invert.json +0 -23
- package/bundled/functions/is_cool.json +0 -23
- package/bundled/functions/is_dark.json +0 -27
- package/bundled/functions/is_light.json +0 -27
- package/bundled/functions/is_neutral.json +0 -65
- package/bundled/functions/is_warm.json +0 -23
- package/bundled/functions/lighten.json +0 -28
- package/bundled/functions/lightness.json +0 -61
- package/bundled/functions/luminance.json +0 -23
- package/bundled/functions/meets_contrast.json +0 -31
- package/bundled/functions/mix.json +0 -32
- package/bundled/functions/monochromatic.json +0 -28
- package/bundled/functions/muted.json +0 -59
- package/bundled/functions/neutral_variant.json +0 -59
- package/bundled/functions/relative_luminance.json +0 -61
- package/bundled/functions/rotate_hue.json +0 -28
- package/bundled/functions/saturate.json +0 -28
- package/bundled/functions/scale_chroma.json +0 -60
- package/bundled/functions/scale_lightness.json +0 -60
- package/bundled/functions/sepia.json +0 -59
- package/bundled/functions/set_chroma.json +0 -28
- package/bundled/functions/set_hue.json +0 -28
- package/bundled/functions/set_lightness.json +0 -28
- package/bundled/functions/shade_scale.json +0 -28
- package/bundled/functions/split_complement.json +0 -28
- package/bundled/functions/steps.json +0 -32
- package/bundled/functions/tetradic.json +0 -24
- package/bundled/functions/tint_scale.json +0 -36
- package/bundled/functions/to_gamut.json +0 -59
- package/bundled/functions/triadic.json +0 -24
- package/bundled/functions/vibrant.json +0 -59
- package/bundled/functions/warmer.json +0 -52
- package/bundled/functions/wcag_level.json +0 -60
- package/bundled/functions.json +0 -2624
- package/bundled/registry.json +0 -3833
- package/bundled/types/css-color.json +0 -151
- package/bundled/types/hex-color.json +0 -25
- package/bundled/types/hsl-color.json +0 -66
- package/bundled/types/hsv-color.json +0 -57
- package/bundled/types/hwb-color.json +0 -66
- package/bundled/types/lab-color.json +0 -57
- package/bundled/types/lch-color.json +0 -57
- package/bundled/types/okhsl-color.json +0 -57
- package/bundled/types/okhsv-color.json +0 -57
- package/bundled/types/oklab-color.json +0 -87
- package/bundled/types/oklch-color.json +0 -57
- package/bundled/types/p3-color.json +0 -57
- package/bundled/types/p3-linear-color.json +0 -57
- package/bundled/types/rgb-color.json +0 -73
- package/bundled/types/srgb-color.json +0 -77
- package/bundled/types/srgb-linear-color.json +0 -67
- package/bundled/types/xyz-d50-color.json +0 -57
- package/bundled/types/xyz-d65-color.json +0 -77
- package/bundled/types.json +0 -1207
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
SchemaSpecification,
|
|
11
11
|
} from "@/bundler/types.js";
|
|
12
12
|
import { extractSchemaName, parseSchemaUri } from "@/utils/schema-uri";
|
|
13
|
-
import {
|
|
13
|
+
import { buildSchemaFromDirectory } from "./build-schema.js";
|
|
14
14
|
|
|
15
15
|
export interface SchemaReference {
|
|
16
16
|
slug: string;
|
|
@@ -181,7 +181,7 @@ export async function collectRequiredSchemas(
|
|
|
181
181
|
schemasDir || process.env.SCHEMAS_DIR || join(process.cwd(), "src/schemas");
|
|
182
182
|
const schemaDir = join(resolvedSchemasDir, categoryDir, slug);
|
|
183
183
|
|
|
184
|
-
spec = await
|
|
184
|
+
spec = await buildSchemaFromDirectory(schemaDir, baseUrl ? { baseUrl } : undefined);
|
|
185
185
|
} catch (error) {
|
|
186
186
|
log.warn(`Failed to load schema ${slug} (${effectiveType}):`, error);
|
|
187
187
|
return;
|
|
@@ -275,7 +275,7 @@ export async function collectDependencyTree(
|
|
|
275
275
|
const schemaDir = join(resolvedSchemasDir, categoryDir, schema.slug);
|
|
276
276
|
|
|
277
277
|
try {
|
|
278
|
-
const spec = await
|
|
278
|
+
const spec = await buildSchemaFromDirectory(schemaDir, baseUrl ? { baseUrl } : undefined);
|
|
279
279
|
const requirements = extractRequirements(spec, extractOptions);
|
|
280
280
|
|
|
281
281
|
// Extract just the slugs from URIs
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { access } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import { buildSchemaFromDirectory } from "./build-schema.js";
|
|
9
9
|
import {
|
|
10
10
|
collectDependencyTree,
|
|
11
11
|
collectRequiredSchemasForList,
|
|
@@ -124,7 +124,7 @@ export async function bundleSelectiveSchemas(
|
|
|
124
124
|
// Bundle type schemas
|
|
125
125
|
for (const typeSlug of deps.types) {
|
|
126
126
|
const schemaDir = join(schemasDir, "types", typeSlug);
|
|
127
|
-
const bundled = await
|
|
127
|
+
const bundled = await buildSchemaFromDirectory(schemaDir, { baseUrl });
|
|
128
128
|
|
|
129
129
|
if (bundled.type === "color") {
|
|
130
130
|
const uri = `${baseUrl}/api/v1/core/${typeSlug}/0/`;
|
|
@@ -138,7 +138,7 @@ export async function bundleSelectiveSchemas(
|
|
|
138
138
|
// Bundle function schemas
|
|
139
139
|
for (const funcSlug of deps.functions) {
|
|
140
140
|
const schemaDir = join(schemasDir, "functions", funcSlug);
|
|
141
|
-
const bundled = await
|
|
141
|
+
const bundled = await buildSchemaFromDirectory(schemaDir, { baseUrl });
|
|
142
142
|
|
|
143
143
|
if (bundled.type === "function") {
|
|
144
144
|
const uri = `${baseUrl}/api/v1/function/${funcSlug}/0/`;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { buildSchemaDir } from "./build-dir.js";
|
|
6
|
+
|
|
7
|
+
// Mock ulog to silence logs during tests
|
|
8
|
+
vi.mock("ulog", () => {
|
|
9
|
+
const mockLogger = () => {};
|
|
10
|
+
mockLogger.error = () => {};
|
|
11
|
+
mockLogger.warn = () => {};
|
|
12
|
+
mockLogger.info = () => {};
|
|
13
|
+
mockLogger.log = () => {};
|
|
14
|
+
mockLogger.debug = () => {};
|
|
15
|
+
mockLogger.trace = () => {};
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
default: () => mockLogger,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Mock console.log to capture stdout
|
|
23
|
+
const originalConsoleLog = console.log;
|
|
24
|
+
let capturedOutput: string[] = [];
|
|
25
|
+
|
|
26
|
+
function mockConsoleLog(...args: any[]) {
|
|
27
|
+
capturedOutput.push(args.join(" "));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("Build Command", () => {
|
|
31
|
+
// Test with real existing schemas
|
|
32
|
+
describe("with real schemas", () => {
|
|
33
|
+
it("should build css-color type schema", async () => {
|
|
34
|
+
const cssColorDir = join(process.cwd(), "src/schemas/types/css-color");
|
|
35
|
+
|
|
36
|
+
// Capture console output
|
|
37
|
+
capturedOutput = [];
|
|
38
|
+
console.log = mockConsoleLog;
|
|
39
|
+
|
|
40
|
+
await buildSchemaDir(cssColorDir);
|
|
41
|
+
|
|
42
|
+
// Restore console
|
|
43
|
+
console.log = originalConsoleLog;
|
|
44
|
+
|
|
45
|
+
const output = capturedOutput[0];
|
|
46
|
+
const result = JSON.parse(output);
|
|
47
|
+
|
|
48
|
+
expect(result.name).toBe("CSS");
|
|
49
|
+
expect(result.type).toBe("color");
|
|
50
|
+
expect(result.initializers).toBeDefined();
|
|
51
|
+
expect(result.conversions).toBeDefined();
|
|
52
|
+
|
|
53
|
+
// Verify scripts are inlined
|
|
54
|
+
expect(result.initializers[0].script.script).toContain("variable");
|
|
55
|
+
expect(result.conversions[0].script.script).toContain("variable");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should build darken function schema", async () => {
|
|
59
|
+
const darkenDir = join(process.cwd(), "src/schemas/functions/darken");
|
|
60
|
+
|
|
61
|
+
// Capture console output
|
|
62
|
+
capturedOutput = [];
|
|
63
|
+
console.log = mockConsoleLog;
|
|
64
|
+
|
|
65
|
+
await buildSchemaDir(darkenDir);
|
|
66
|
+
|
|
67
|
+
// Restore console
|
|
68
|
+
console.log = originalConsoleLog;
|
|
69
|
+
|
|
70
|
+
const output = capturedOutput[0];
|
|
71
|
+
const result = JSON.parse(output);
|
|
72
|
+
|
|
73
|
+
expect(result.name).toBe("Darken");
|
|
74
|
+
expect(result.type).toBe("function");
|
|
75
|
+
expect(result.keyword).toBe("darken");
|
|
76
|
+
expect(result.script).toBeDefined();
|
|
77
|
+
expect(result.requirements).toBeDefined();
|
|
78
|
+
|
|
79
|
+
// Verify script is inlined
|
|
80
|
+
expect(result.script.script).toContain("darken");
|
|
81
|
+
expect(result.script.script).toContain("OKLab");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("with custom test schemas", () => {
|
|
86
|
+
const customSchemasDir = join(process.cwd(), "test-build-schemas");
|
|
87
|
+
const customTypeDir = join(customSchemasDir, "custom-color");
|
|
88
|
+
const customFunctionDir = join(customSchemasDir, "custom-function");
|
|
89
|
+
|
|
90
|
+
beforeAll(async () => {
|
|
91
|
+
// Create custom color type schema
|
|
92
|
+
await mkdir(customTypeDir, { recursive: true });
|
|
93
|
+
|
|
94
|
+
const colorSchemaJson = {
|
|
95
|
+
name: "TestColor",
|
|
96
|
+
type: "color" as const,
|
|
97
|
+
description: "A test color type",
|
|
98
|
+
slug: "test-color",
|
|
99
|
+
schema: {
|
|
100
|
+
type: "object" as const,
|
|
101
|
+
properties: {
|
|
102
|
+
value: { type: "string" as const },
|
|
103
|
+
},
|
|
104
|
+
required: ["value"],
|
|
105
|
+
},
|
|
106
|
+
initializers: [
|
|
107
|
+
{
|
|
108
|
+
title: "Test Initializer",
|
|
109
|
+
keyword: "testcolor",
|
|
110
|
+
description: "Creates a test color",
|
|
111
|
+
script: {
|
|
112
|
+
type: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
|
|
113
|
+
script: "./test-initializer.tokenscript",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
conversions: [
|
|
118
|
+
{
|
|
119
|
+
source: "$self",
|
|
120
|
+
target: "$self",
|
|
121
|
+
description: "Test conversion",
|
|
122
|
+
lossless: true,
|
|
123
|
+
script: {
|
|
124
|
+
type: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
|
|
125
|
+
script: "./test-conversion.tokenscript",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
await writeFile(join(customTypeDir, "schema.json"), JSON.stringify(colorSchemaJson, null, 2));
|
|
132
|
+
|
|
133
|
+
const initializerScript = `// Test initializer
|
|
134
|
+
variable input: List = {input};
|
|
135
|
+
variable value: String = input.get(0);
|
|
136
|
+
variable output: Color.TestColor;
|
|
137
|
+
output.value = value;
|
|
138
|
+
return output;`;
|
|
139
|
+
|
|
140
|
+
await writeFile(join(customTypeDir, "test-initializer.tokenscript"), initializerScript);
|
|
141
|
+
|
|
142
|
+
const conversionScript = `// Test conversion
|
|
143
|
+
variable input: Color.TestColor = {input};
|
|
144
|
+
variable output: Color.TestColor;
|
|
145
|
+
output.value = input.value;
|
|
146
|
+
return output;`;
|
|
147
|
+
|
|
148
|
+
await writeFile(join(customTypeDir, "test-conversion.tokenscript"), conversionScript);
|
|
149
|
+
|
|
150
|
+
// Create custom function schema
|
|
151
|
+
await mkdir(customFunctionDir, { recursive: true });
|
|
152
|
+
|
|
153
|
+
const functionSchemaJson = {
|
|
154
|
+
name: "TestFunction",
|
|
155
|
+
type: "function" as const,
|
|
156
|
+
description: "A test function",
|
|
157
|
+
keyword: "testfunc",
|
|
158
|
+
input: {
|
|
159
|
+
type: "object" as const,
|
|
160
|
+
properties: {
|
|
161
|
+
value: { type: "number" as const },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
script: {
|
|
165
|
+
type: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
|
|
166
|
+
script: "./test-function.tokenscript",
|
|
167
|
+
},
|
|
168
|
+
requirements: [] as string[],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
await writeFile(
|
|
172
|
+
join(customFunctionDir, "schema.json"),
|
|
173
|
+
JSON.stringify(functionSchemaJson, null, 2),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const functionScript = `// Test function
|
|
177
|
+
variable input: List = {input};
|
|
178
|
+
variable value: Number = input.get(0);
|
|
179
|
+
return value * 2;`;
|
|
180
|
+
|
|
181
|
+
await writeFile(join(customFunctionDir, "test-function.tokenscript"), functionScript);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
afterAll(async () => {
|
|
185
|
+
// Clean up
|
|
186
|
+
try {
|
|
187
|
+
await rm(customSchemasDir, { recursive: true, force: true });
|
|
188
|
+
} catch {
|
|
189
|
+
// Ignore
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should build color type schema with inlined scripts", async () => {
|
|
194
|
+
capturedOutput = [];
|
|
195
|
+
console.log = mockConsoleLog;
|
|
196
|
+
|
|
197
|
+
await buildSchemaDir(customTypeDir);
|
|
198
|
+
|
|
199
|
+
console.log = originalConsoleLog;
|
|
200
|
+
|
|
201
|
+
const output = capturedOutput[0];
|
|
202
|
+
const result = JSON.parse(output);
|
|
203
|
+
|
|
204
|
+
expect(result.name).toBe("TestColor");
|
|
205
|
+
expect(result.type).toBe("color");
|
|
206
|
+
expect(result.initializers[0].script.script).toContain("variable input: List");
|
|
207
|
+
expect(result.conversions[0].script.script).toContain("variable input: Color.TestColor");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should build function schema with inlined script", async () => {
|
|
211
|
+
capturedOutput = [];
|
|
212
|
+
console.log = mockConsoleLog;
|
|
213
|
+
|
|
214
|
+
await buildSchemaDir(customFunctionDir);
|
|
215
|
+
|
|
216
|
+
console.log = originalConsoleLog;
|
|
217
|
+
|
|
218
|
+
const output = capturedOutput[0];
|
|
219
|
+
const result = JSON.parse(output);
|
|
220
|
+
|
|
221
|
+
expect(result.name).toBe("TestFunction");
|
|
222
|
+
expect(result.type).toBe("function");
|
|
223
|
+
expect(result.script.script).toContain("variable input: List");
|
|
224
|
+
expect(result.script.script).toContain("return value * 2");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should output pretty JSON with --pretty option", async () => {
|
|
228
|
+
capturedOutput = [];
|
|
229
|
+
console.log = mockConsoleLog;
|
|
230
|
+
|
|
231
|
+
await buildSchemaDir(customTypeDir, { pretty: true });
|
|
232
|
+
|
|
233
|
+
console.log = originalConsoleLog;
|
|
234
|
+
|
|
235
|
+
const output = capturedOutput[0];
|
|
236
|
+
|
|
237
|
+
// Pretty JSON should have newlines and indentation
|
|
238
|
+
expect(output).toContain("\n");
|
|
239
|
+
expect(output).toContain(" ");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should output compact JSON without --pretty option", async () => {
|
|
243
|
+
capturedOutput = [];
|
|
244
|
+
console.log = mockConsoleLog;
|
|
245
|
+
|
|
246
|
+
await buildSchemaDir(customTypeDir, { pretty: false });
|
|
247
|
+
|
|
248
|
+
console.log = originalConsoleLog;
|
|
249
|
+
|
|
250
|
+
const output = capturedOutput[0];
|
|
251
|
+
|
|
252
|
+
// Compact JSON should not have newlines (except maybe trailing)
|
|
253
|
+
const lineCount = output.split("\n").length;
|
|
254
|
+
expect(lineCount).toBeLessThan(5);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should write to file when --output is specified", async () => {
|
|
258
|
+
const outputFile = join(customSchemasDir, "output.json");
|
|
259
|
+
|
|
260
|
+
await buildSchemaDir(customTypeDir, { output: outputFile });
|
|
261
|
+
|
|
262
|
+
expect(existsSync(outputFile)).toBe(true);
|
|
263
|
+
|
|
264
|
+
const content = await readFile(outputFile, "utf-8");
|
|
265
|
+
const result = JSON.parse(content);
|
|
266
|
+
|
|
267
|
+
expect(result.name).toBe("TestColor");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should write pretty JSON to file with --pretty and --output", async () => {
|
|
271
|
+
const outputFile = join(customSchemasDir, "output-pretty.json");
|
|
272
|
+
|
|
273
|
+
await buildSchemaDir(customTypeDir, { output: outputFile, pretty: true });
|
|
274
|
+
|
|
275
|
+
const content = await readFile(outputFile, "utf-8");
|
|
276
|
+
|
|
277
|
+
// Pretty JSON should have newlines and indentation
|
|
278
|
+
expect(content).toContain("\n");
|
|
279
|
+
expect(content).toContain(" ");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("error handling", () => {
|
|
284
|
+
it("should throw error for non-existent directory", async () => {
|
|
285
|
+
await expect(buildSchemaDir("/non-existent/directory")).rejects.toThrow(
|
|
286
|
+
/Directory not found|not found/,
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should throw error for directory without schema.json", async () => {
|
|
291
|
+
const emptyDir = join(process.cwd(), "test-empty-dir");
|
|
292
|
+
|
|
293
|
+
await mkdir(emptyDir, { recursive: true });
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
await expect(buildSchemaDir(emptyDir)).rejects.toThrow(/schema.json not found/);
|
|
297
|
+
} finally {
|
|
298
|
+
await rm(emptyDir, { recursive: true, force: true });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("path resolution", () => {
|
|
304
|
+
const testSchemasDir = join(process.cwd(), "test-path-schemas");
|
|
305
|
+
const testSchemaDir = join(testSchemasDir, "test-schema");
|
|
306
|
+
|
|
307
|
+
beforeAll(async () => {
|
|
308
|
+
await mkdir(testSchemaDir, { recursive: true });
|
|
309
|
+
|
|
310
|
+
const schemaJson = {
|
|
311
|
+
name: "PathTest",
|
|
312
|
+
type: "color" as const,
|
|
313
|
+
description: "Path test schema",
|
|
314
|
+
slug: "path-test",
|
|
315
|
+
schema: {
|
|
316
|
+
type: "object" as const,
|
|
317
|
+
properties: {
|
|
318
|
+
value: { type: "string" as const },
|
|
319
|
+
},
|
|
320
|
+
required: ["value"],
|
|
321
|
+
},
|
|
322
|
+
initializers: [] as any[],
|
|
323
|
+
conversions: [] as any[],
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
await writeFile(join(testSchemaDir, "schema.json"), JSON.stringify(schemaJson, null, 2));
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
afterAll(async () => {
|
|
330
|
+
try {
|
|
331
|
+
await rm(testSchemasDir, { recursive: true, force: true });
|
|
332
|
+
} catch {
|
|
333
|
+
// Ignore
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should resolve relative paths from current working directory", async () => {
|
|
338
|
+
// Use a relative path
|
|
339
|
+
const relativePath = "test-path-schemas/test-schema";
|
|
340
|
+
|
|
341
|
+
capturedOutput = [];
|
|
342
|
+
console.log = mockConsoleLog;
|
|
343
|
+
|
|
344
|
+
await buildSchemaDir(relativePath);
|
|
345
|
+
|
|
346
|
+
console.log = originalConsoleLog;
|
|
347
|
+
|
|
348
|
+
const output = capturedOutput[0];
|
|
349
|
+
const result = JSON.parse(output);
|
|
350
|
+
|
|
351
|
+
expect(result.name).toBe("PathTest");
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build command - Build individual schema directories
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/// <reference types="../../../types/ulog" />
|
|
6
|
+
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import anylogger from "ulog";
|
|
11
|
+
import { buildSchemaFromDirectory } from "@/bundler/build-schema.js";
|
|
12
|
+
|
|
13
|
+
const log = anylogger("build-dir");
|
|
14
|
+
|
|
15
|
+
export interface BuildDirOptions {
|
|
16
|
+
output?: string;
|
|
17
|
+
pretty?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build a schema from a directory containing schema.json
|
|
22
|
+
*/
|
|
23
|
+
export async function buildSchemaDir(
|
|
24
|
+
schemaDir: string,
|
|
25
|
+
options: BuildDirOptions = {},
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const resolvedDir = resolveSchemaDir(schemaDir);
|
|
28
|
+
|
|
29
|
+
if (!existsSync(resolvedDir)) {
|
|
30
|
+
throw new Error(`Directory not found: ${resolvedDir}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const schemaJsonPath = join(resolvedDir, "schema.json");
|
|
34
|
+
if (!existsSync(schemaJsonPath)) {
|
|
35
|
+
throw new Error(`schema.json not found in: ${resolvedDir}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
log.info(`Building schema from: ${resolvedDir}`);
|
|
39
|
+
|
|
40
|
+
// Build the schema using shared bundler logic
|
|
41
|
+
const schema = await buildSchemaFromDirectory(resolvedDir);
|
|
42
|
+
|
|
43
|
+
// Generate output
|
|
44
|
+
const output = options.pretty ? JSON.stringify(schema, null, 2) : JSON.stringify(schema);
|
|
45
|
+
|
|
46
|
+
// Write to stdout or file
|
|
47
|
+
if (options.output) {
|
|
48
|
+
await mkdir(dirname(options.output), { recursive: true });
|
|
49
|
+
await writeFile(options.output, output, "utf-8");
|
|
50
|
+
log.info(`Output written to: ${options.output}`);
|
|
51
|
+
console.log(`✓ Built ${schema.type}:${schema.name} → ${options.output}`);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(output);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve schema directory - handles relative paths
|
|
59
|
+
*/
|
|
60
|
+
function resolveSchemaDir(schemaDir: string): string {
|
|
61
|
+
// If absolute path, use as-is
|
|
62
|
+
if (existsSync(schemaDir)) {
|
|
63
|
+
return schemaDir;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Try relative to current working directory
|
|
67
|
+
const cwd = process.cwd();
|
|
68
|
+
const fromCwd = join(cwd, schemaDir);
|
|
69
|
+
if (existsSync(fromCwd)) {
|
|
70
|
+
return fromCwd;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Return as-is and let error handling catch it
|
|
74
|
+
return schemaDir;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* CLI action handler for build command
|
|
79
|
+
*/
|
|
80
|
+
export async function handleBuildCommand(
|
|
81
|
+
schemaDir: string,
|
|
82
|
+
options: BuildDirOptions = {},
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
try {
|
|
85
|
+
await buildSchemaDir(schemaDir, options);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
log.error("Build failed:", error);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
2
4
|
import { expandPresetSchemas } from "@/bundler/presets/index.js";
|
|
3
5
|
import { bundleSchemas } from "./bundle.js";
|
|
4
6
|
|
|
@@ -88,4 +90,96 @@ describe("Bundle Command", () => {
|
|
|
88
90
|
warnSpy.mockRestore();
|
|
89
91
|
});
|
|
90
92
|
});
|
|
93
|
+
|
|
94
|
+
describe("Custom Schema Directory", () => {
|
|
95
|
+
const customSchemasDir = join(process.cwd(), "test-custom-schemas");
|
|
96
|
+
const customTypeDir = join(customSchemasDir, "types", "custom-color");
|
|
97
|
+
|
|
98
|
+
beforeAll(async () => {
|
|
99
|
+
// Create custom schema directory structure
|
|
100
|
+
await mkdir(customTypeDir, { recursive: true });
|
|
101
|
+
|
|
102
|
+
// Create a custom schema.json
|
|
103
|
+
const schemaJson = {
|
|
104
|
+
name: "CustomColor",
|
|
105
|
+
type: "color" as const,
|
|
106
|
+
description: "A custom color type for testing",
|
|
107
|
+
slug: "custom-color",
|
|
108
|
+
schema: {
|
|
109
|
+
type: "object" as const,
|
|
110
|
+
properties: {
|
|
111
|
+
x: { type: "number" as const },
|
|
112
|
+
y: { type: "number" as const },
|
|
113
|
+
},
|
|
114
|
+
required: ["x", "y"],
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
},
|
|
117
|
+
initializers: [
|
|
118
|
+
{
|
|
119
|
+
keyword: "customcolor",
|
|
120
|
+
script: {
|
|
121
|
+
type: "https://schema.tokenscript.dev.gcp.tokens.studio/api/v1/core/tokenscript/0/",
|
|
122
|
+
script: "./custom-initializer.tokenscript",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
conversions: [] as any[],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
await writeFile(
|
|
130
|
+
join(customTypeDir, "schema.json"),
|
|
131
|
+
JSON.stringify(schemaJson, null, 2),
|
|
132
|
+
"utf-8",
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Create a custom initializer script
|
|
136
|
+
const initializerScript = `
|
|
137
|
+
variable x_val: Number = args.get(0);
|
|
138
|
+
variable y_val: Number = args.get(1);
|
|
139
|
+
variable result: Color.CustomColor = (x: x_val, y: y_val);
|
|
140
|
+
result
|
|
141
|
+
`.trim();
|
|
142
|
+
|
|
143
|
+
await writeFile(join(customTypeDir, "custom-initializer.tokenscript"), initializerScript);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterAll(async () => {
|
|
147
|
+
// Clean up custom schemas directory
|
|
148
|
+
try {
|
|
149
|
+
await rm(customSchemasDir, { recursive: true, force: true });
|
|
150
|
+
} catch {
|
|
151
|
+
// Ignore if directory doesn't exist
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should bundle schemas from custom directory", async () => {
|
|
156
|
+
const result = await bundleSchemas(["type:custom-color"], customSchemasDir);
|
|
157
|
+
|
|
158
|
+
expect(result.output).toContain("SCHEMAS");
|
|
159
|
+
expect(result.output).toContain("custom-color");
|
|
160
|
+
expect(result.metadata.requestedSchemas).toEqual(["type:custom-color"]);
|
|
161
|
+
expect(result.metadata.resolvedDependencies).toContain("custom-color");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should include custom schema in output", async () => {
|
|
165
|
+
const result = await bundleSchemas(["custom-color"], customSchemasDir);
|
|
166
|
+
|
|
167
|
+
// Verify the output contains the custom schema definition
|
|
168
|
+
expect(result.output).toContain("CustomColor");
|
|
169
|
+
expect(result.output).toContain("customcolor");
|
|
170
|
+
expect(result.output).toContain("A custom color type for testing");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should use custom directory metadata in output", async () => {
|
|
174
|
+
const cliArgs = ["type:custom-color", "--schemas-dir", customSchemasDir];
|
|
175
|
+
const result = await bundleSchemas(["type:custom-color"], customSchemasDir, cliArgs);
|
|
176
|
+
|
|
177
|
+
expect(result.metadata.generatedBy).toContain("--schemas-dir");
|
|
178
|
+
expect(result.metadata.generatedBy).toContain(customSchemasDir);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should fail gracefully when schema not found in custom directory", async () => {
|
|
182
|
+
await expect(bundleSchemas(["type:nonexistent-schema"], customSchemasDir)).rejects.toThrow();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
91
185
|
});
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/// <reference types="../../../types/ulog" />
|
|
6
6
|
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
7
8
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
9
|
import { dirname, join } from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -20,6 +21,7 @@ export interface BundleOptions {
|
|
|
20
21
|
config?: string;
|
|
21
22
|
output?: string;
|
|
22
23
|
dryRun?: boolean;
|
|
24
|
+
schemasDir?: string; // Comma-separated list of custom schema directories
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -118,27 +120,23 @@ function findSchemasDir(): string {
|
|
|
118
120
|
const __filename = fileURLToPath(import.meta.url);
|
|
119
121
|
const __dirname = dirname(__filename);
|
|
120
122
|
|
|
121
|
-
// From compiled dist/cli/
|
|
123
|
+
// From compiled dist/cli/commands/bundle.js to src/schemas
|
|
122
124
|
const fromDist = join(__dirname, "../../src/schemas");
|
|
123
125
|
|
|
124
126
|
// From source src/cli/commands/bundle.ts to src/schemas (for tests/dev)
|
|
125
127
|
const fromSource = join(__dirname, "../../schemas");
|
|
126
128
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return fromSource;
|
|
135
|
-
}
|
|
136
|
-
} catch {
|
|
137
|
-
// If fs checks fail, default to dist structure
|
|
129
|
+
// Check source first (for development), then dist (for installed package)
|
|
130
|
+
if (existsSync(fromSource)) {
|
|
131
|
+
return fromSource;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (existsSync(fromDist)) {
|
|
135
|
+
return fromDist;
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
// Default to
|
|
141
|
-
return
|
|
138
|
+
// Default to source structure (for development)
|
|
139
|
+
return fromSource;
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
/**
|
|
@@ -227,11 +225,20 @@ export async function handleBundleCommand(
|
|
|
227
225
|
if (options.dryRun) {
|
|
228
226
|
cliArgs.push("--dry-run");
|
|
229
227
|
}
|
|
228
|
+
if (options.schemasDir) {
|
|
229
|
+
cliArgs.push("--schemas-dir", options.schemasDir);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Use custom schema directory if provided
|
|
233
|
+
const customSchemasDir = options.schemasDir;
|
|
234
|
+
if (customSchemasDir) {
|
|
235
|
+
log.info(`Using custom schema directory: ${customSchemasDir}`);
|
|
236
|
+
}
|
|
230
237
|
|
|
231
238
|
// Bundle schemas
|
|
232
239
|
const { output, metadata, dependencyTree } = await bundleSchemas(
|
|
233
240
|
configSchemas,
|
|
234
|
-
|
|
241
|
+
customSchemasDir,
|
|
235
242
|
cliArgs,
|
|
236
243
|
);
|
|
237
244
|
|