@tokens-studio/tokenscript-schemas 0.1.3 → 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.
@@ -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
+ }
@@ -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";
@@ -119,27 +120,23 @@ function findSchemasDir(): string {
119
120
  const __filename = fileURLToPath(import.meta.url);
120
121
  const __dirname = dirname(__filename);
121
122
 
122
- // From compiled dist/cli/index.js (bundled) to src/schemas
123
+ // From compiled dist/cli/commands/bundle.js to src/schemas
123
124
  const fromDist = join(__dirname, "../../src/schemas");
124
125
 
125
126
  // From source src/cli/commands/bundle.ts to src/schemas (for tests/dev)
126
127
  const fromSource = join(__dirname, "../../schemas");
127
128
 
128
- // Try to detect which one exists (check dist first for installed package)
129
- try {
130
- const fs = require("node:fs");
131
- if (fs.existsSync(fromDist)) {
132
- return fromDist;
133
- }
134
- if (fs.existsSync(fromSource)) {
135
- return fromSource;
136
- }
137
- } catch {
138
- // 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;
139
136
  }
140
137
 
141
- // Default to dist structure (for installed package)
142
- return fromDist;
138
+ // Default to source structure (for development)
139
+ return fromSource;
143
140
  }
144
141
 
145
142
  /**
package/src/cli/index.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import cac from "cac";
10
10
  import anylogger from "ulog";
11
+ import { type BuildDirOptions, handleBuildCommand } from "./commands/build-dir.js";
11
12
  import { type BundleOptions, handleBundleCommand } from "./commands/bundle.js";
12
13
  import { handleListCommand, type ListOptions } from "./commands/list.js";
13
14
  import { handlePresetsCommand } from "./commands/presets.js";
@@ -32,6 +33,20 @@ cli
32
33
  }
33
34
  });
34
35
 
36
+ // Build command
37
+ cli
38
+ .command("build <directory>", "Build an individual schema directory")
39
+ .option("-o, --output <path>", "Output file path (defaults to stdout)")
40
+ .option("-p, --pretty", "Pretty print JSON output")
41
+ .action(async (directory: string, options: BuildDirOptions) => {
42
+ try {
43
+ await handleBuildCommand(directory, options);
44
+ } catch (error) {
45
+ log.error("Error:", error);
46
+ process.exit(1);
47
+ }
48
+ });
49
+
35
50
  // List command
36
51
  cli
37
52
  .command("list", "List available schemas")