@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.
Files changed (106) hide show
  1. package/README.md +36 -7
  2. package/dist/cli/index.cjs +142 -88
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.js +141 -87
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.cjs +19 -19
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +3 -3
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js +19 -19
  11. package/dist/index.js.map +1 -1
  12. package/package.json +3 -3
  13. package/src/bundler/{bundle-schema.ts → build-schema.ts} +2 -2
  14. package/src/bundler/index.ts +25 -25
  15. package/src/bundler/schema-dependency-resolver.ts +3 -3
  16. package/src/bundler/selective-bundler.ts +3 -3
  17. package/src/cli/commands/build-dir.test.ts +354 -0
  18. package/src/cli/commands/build-dir.ts +90 -0
  19. package/src/cli/commands/bundle.test.ts +95 -1
  20. package/src/cli/commands/bundle.ts +22 -15
  21. package/src/cli/index.ts +16 -0
  22. package/bundled/functions/adjust_chroma.json +0 -60
  23. package/bundled/functions/adjust_hue.json +0 -60
  24. package/bundled/functions/adjust_lightness.json +0 -60
  25. package/bundled/functions/adjust_to_contrast.json +0 -67
  26. package/bundled/functions/alpha_blend.json +0 -31
  27. package/bundled/functions/alpha_scale.json +0 -27
  28. package/bundled/functions/analogous.json +0 -32
  29. package/bundled/functions/apca_contrast.json +0 -27
  30. package/bundled/functions/are_similar.json +0 -73
  31. package/bundled/functions/auto_text_color.json +0 -66
  32. package/bundled/functions/best_contrast.json +0 -28
  33. package/bundled/functions/chroma.json +0 -54
  34. package/bundled/functions/clamp_chroma.json +0 -66
  35. package/bundled/functions/clamp_lightness.json +0 -66
  36. package/bundled/functions/clamp_to_gamut.json +0 -23
  37. package/bundled/functions/complement.json +0 -24
  38. package/bundled/functions/contrast_ratio.json +0 -27
  39. package/bundled/functions/cooler.json +0 -52
  40. package/bundled/functions/darken.json +0 -28
  41. package/bundled/functions/delta_e_2000.json +0 -40
  42. package/bundled/functions/delta_e_76.json +0 -27
  43. package/bundled/functions/delta_e_ok.json +0 -27
  44. package/bundled/functions/desaturate.json +0 -28
  45. package/bundled/functions/distributed.json +0 -36
  46. package/bundled/functions/diverging.json +0 -36
  47. package/bundled/functions/grayscale.json +0 -24
  48. package/bundled/functions/harmonize.json +0 -65
  49. package/bundled/functions/hue.json +0 -54
  50. package/bundled/functions/hue_difference.json +0 -27
  51. package/bundled/functions/in_gamut.json +0 -27
  52. package/bundled/functions/interpolate.json +0 -66
  53. package/bundled/functions/invert.json +0 -23
  54. package/bundled/functions/is_cool.json +0 -23
  55. package/bundled/functions/is_dark.json +0 -27
  56. package/bundled/functions/is_light.json +0 -27
  57. package/bundled/functions/is_neutral.json +0 -65
  58. package/bundled/functions/is_warm.json +0 -23
  59. package/bundled/functions/lighten.json +0 -28
  60. package/bundled/functions/lightness.json +0 -61
  61. package/bundled/functions/luminance.json +0 -23
  62. package/bundled/functions/meets_contrast.json +0 -31
  63. package/bundled/functions/mix.json +0 -32
  64. package/bundled/functions/monochromatic.json +0 -28
  65. package/bundled/functions/muted.json +0 -59
  66. package/bundled/functions/neutral_variant.json +0 -59
  67. package/bundled/functions/relative_luminance.json +0 -61
  68. package/bundled/functions/rotate_hue.json +0 -28
  69. package/bundled/functions/saturate.json +0 -28
  70. package/bundled/functions/scale_chroma.json +0 -60
  71. package/bundled/functions/scale_lightness.json +0 -60
  72. package/bundled/functions/sepia.json +0 -59
  73. package/bundled/functions/set_chroma.json +0 -28
  74. package/bundled/functions/set_hue.json +0 -28
  75. package/bundled/functions/set_lightness.json +0 -28
  76. package/bundled/functions/shade_scale.json +0 -28
  77. package/bundled/functions/split_complement.json +0 -28
  78. package/bundled/functions/steps.json +0 -32
  79. package/bundled/functions/tetradic.json +0 -24
  80. package/bundled/functions/tint_scale.json +0 -36
  81. package/bundled/functions/to_gamut.json +0 -59
  82. package/bundled/functions/triadic.json +0 -24
  83. package/bundled/functions/vibrant.json +0 -59
  84. package/bundled/functions/warmer.json +0 -52
  85. package/bundled/functions/wcag_level.json +0 -60
  86. package/bundled/functions.json +0 -2624
  87. package/bundled/registry.json +0 -3833
  88. package/bundled/types/css-color.json +0 -151
  89. package/bundled/types/hex-color.json +0 -25
  90. package/bundled/types/hsl-color.json +0 -66
  91. package/bundled/types/hsv-color.json +0 -57
  92. package/bundled/types/hwb-color.json +0 -66
  93. package/bundled/types/lab-color.json +0 -57
  94. package/bundled/types/lch-color.json +0 -57
  95. package/bundled/types/okhsl-color.json +0 -57
  96. package/bundled/types/okhsv-color.json +0 -57
  97. package/bundled/types/oklab-color.json +0 -87
  98. package/bundled/types/oklch-color.json +0 -57
  99. package/bundled/types/p3-color.json +0 -57
  100. package/bundled/types/p3-linear-color.json +0 -57
  101. package/bundled/types/rgb-color.json +0 -73
  102. package/bundled/types/srgb-color.json +0 -77
  103. package/bundled/types/srgb-linear-color.json +0 -67
  104. package/bundled/types/xyz-d50-color.json +0 -57
  105. package/bundled/types/xyz-d65-color.json +0 -77
  106. 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 { bundleSchemaFromDirectory } from "./bundle-schema.js";
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 bundleSchemaFromDirectory(schemaDir, baseUrl ? { baseUrl } : undefined);
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 bundleSchemaFromDirectory(schemaDir, baseUrl ? { baseUrl } : undefined);
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 { bundleSchemaFromDirectory } from "./bundle-schema.js";
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 bundleSchemaFromDirectory(schemaDir, { baseUrl });
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 bundleSchemaFromDirectory(schemaDir, { baseUrl });
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 { describe, expect, it, vi } from "vitest";
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/index.js (bundled) to src/schemas
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
- // Try to detect which one exists (check dist first for installed package)
128
- try {
129
- const fs = require("node:fs");
130
- if (fs.existsSync(fromDist)) {
131
- return fromDist;
132
- }
133
- if (fs.existsSync(fromSource)) {
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 dist structure (for installed package)
141
- return fromDist;
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
- undefined,
241
+ customSchemasDir,
235
242
  cliArgs,
236
243
  );
237
244