@fgv/ts-res-cli 5.0.0-9 → 5.0.1-1

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.
@@ -1,581 +0,0 @@
1
- /*
2
- * Copyright (c) 2025 Erik Fortune
3
- *
4
- * Permission is hereby granted, free of charge, to any person obtaining a copy
5
- * of this software and associated documentation files (the "Software"), to deal
6
- * in the Software without restriction, including without limitation the rights
7
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- * copies of the Software, and to permit persons to whom the Software is
9
- * furnished to do so, subject to the following conditions:
10
- *
11
- * The above copyright notice and this permission notice shall be included in all
12
- * copies or substantial portions of the Software.
13
- *
14
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
- * SOFTWARE.
21
- */
22
-
23
- import '@fgv/ts-utils-jest';
24
- import { promises as fs } from 'fs';
25
- import * as path from 'path';
26
- import { spawn } from 'child_process';
27
- import * as TsRes from '@fgv/ts-res';
28
-
29
- describe('CLI Integration Tests', () => {
30
- let tempDir: string;
31
- let cliBin: string;
32
-
33
- beforeEach(async () => {
34
- tempDir = await fs.mkdtemp(path.join(__dirname, '../../temp/test-temp-integration-'));
35
- cliBin = path.resolve(__dirname, '../../bin/ts-res-compile.js');
36
- });
37
-
38
- afterEach(async () => {
39
- await fs.rm(tempDir, { recursive: true, force: true });
40
- });
41
-
42
- /**
43
- * Helper function to run CLI commands
44
- */
45
- async function runCli(args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
46
- return new Promise((resolve) => {
47
- const child = spawn('node', [cliBin, ...args], {
48
- stdio: ['pipe', 'pipe', 'pipe']
49
- });
50
-
51
- let stdout = '';
52
- let stderr = '';
53
-
54
- child.stdout.on('data', (data) => {
55
- stdout += data.toString();
56
- });
57
-
58
- child.stderr.on('data', (data) => {
59
- stderr += data.toString();
60
- });
61
-
62
- child.on('close', (exitCode) => {
63
- resolve({ exitCode: exitCode || 0, stdout, stderr });
64
- });
65
- });
66
- }
67
-
68
- describe('Basic CLI functionality', () => {
69
- test('shows help when run without arguments', async () => {
70
- const result = await runCli([]);
71
- expect(result.exitCode).toBe(1); // Commander exits with 1 when no command provided
72
- expect(result.stderr).toMatch(/Usage:/);
73
- expect(result.stderr).toMatch(/Commands:/);
74
- });
75
-
76
- test('shows version with --version flag', async () => {
77
- const result = await runCli(['--version']);
78
- expect(result.exitCode).toBe(0);
79
- expect(result.stdout.trim()).toBe('1.0.0');
80
- });
81
-
82
- test('shows help with --help flag', async () => {
83
- const result = await runCli(['--help']);
84
- expect(result.exitCode).toBe(0);
85
- expect(result.stdout).toMatch(/Compile and optimize ts-res resources/);
86
- });
87
- });
88
-
89
- describe('Compile command integration', () => {
90
- let inputFile: string;
91
- let outputFile: string;
92
-
93
- beforeEach(async () => {
94
- inputFile = path.join(tempDir, 'resources.json');
95
- outputFile = path.join(tempDir, 'compiled.json');
96
-
97
- // Create comprehensive test resources in ResourceCollection format
98
- const resources = {
99
- resources: [
100
- {
101
- id: 'app.title',
102
- resourceTypeName: 'json',
103
- candidates: [
104
- {
105
- json: { title: 'My Application' }
106
- }
107
- ]
108
- },
109
- {
110
- id: 'messages.welcome',
111
- resourceTypeName: 'json',
112
- candidates: [
113
- {
114
- json: { text: 'Welcome!', subtitle: 'Please log in' },
115
- conditions: { language: 'en' }
116
- },
117
- {
118
- json: { text: '¡Bienvenido!', subtitle: 'Por favor inicie sesión' },
119
- conditions: { language: 'es' }
120
- },
121
- {
122
- json: { text: 'Welcome to the USA!', subtitle: 'Please log in to continue' },
123
- conditions: { language: 'en-US', currentTerritory: 'US' }
124
- }
125
- ]
126
- },
127
- {
128
- id: 'buttons.actions',
129
- resourceTypeName: 'json',
130
- candidates: [
131
- {
132
- json: { submit: 'Submit', cancel: 'Cancel', save: 'Save' },
133
- conditions: { language: 'en' }
134
- }
135
- ]
136
- }
137
- ]
138
- };
139
-
140
- await fs.writeFile(inputFile, JSON.stringify(resources, null, 2));
141
- });
142
-
143
- test('compiles resources to source format', async () => {
144
- const result = await runCli([
145
- 'compile',
146
- '--input',
147
- inputFile,
148
- '--output',
149
- outputFile,
150
- '--format',
151
- 'source'
152
- ]);
153
-
154
- expect(result.exitCode).toBe(0);
155
- expect(result.stdout).toMatch(/Successfully compiled resources/);
156
-
157
- // Verify output file exists and has expected structure
158
- const outputContent = await fs.readFile(outputFile, 'utf-8');
159
- const output = JSON.parse(outputContent);
160
-
161
- expect(output).toHaveProperty('resources');
162
- expect(TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(output.resources)).toSucceedAndSatisfy(
163
- (resources) => {
164
- expect(resources?.resources?.length).toBeGreaterThan(0);
165
- const resourceIds = resources?.resources?.map((r) => r.id) || [];
166
- expect(resourceIds).toContain('resources.app.title');
167
- expect(resourceIds).toContain('resources.messages.welcome');
168
- expect(resourceIds).toContain('resources.buttons.actions');
169
- return true;
170
- }
171
- );
172
- });
173
-
174
- test('compiles resources to JavaScript format', async () => {
175
- const jsOutputFile = path.join(tempDir, 'compiled.js');
176
- const result = await runCli([
177
- 'compile',
178
- '--input',
179
- inputFile,
180
- '--output',
181
- jsOutputFile,
182
- '--format',
183
- 'js'
184
- ]);
185
-
186
- expect(result.exitCode).toBe(0);
187
-
188
- const outputContent = await fs.readFile(jsOutputFile, 'utf-8');
189
- expect(outputContent).toMatch(/^module\.exports = /);
190
- expect(outputContent).toMatch(/app\.title/);
191
- });
192
-
193
- test('compiles resources to TypeScript format', async () => {
194
- const tsOutputFile = path.join(tempDir, 'compiled.ts');
195
- const result = await runCli([
196
- 'compile',
197
- '--input',
198
- inputFile,
199
- '--output',
200
- tsOutputFile,
201
- '--format',
202
- 'ts'
203
- ]);
204
-
205
- expect(result.exitCode).toBe(0);
206
-
207
- const outputContent = await fs.readFile(tsOutputFile, 'utf-8');
208
- expect(outputContent).toMatch(/^export const resources = /);
209
- expect(outputContent).toMatch(/ as const;$/);
210
- });
211
-
212
- test('compiles resources to bundle format', async () => {
213
- const bundleOutputFile = path.join(tempDir, 'compiled.bundle.json');
214
- const result = await runCli([
215
- 'compile',
216
- '--input',
217
- inputFile,
218
- '--output',
219
- bundleOutputFile,
220
- '--format',
221
- 'bundle'
222
- ]);
223
-
224
- expect(result.exitCode).toBe(0);
225
-
226
- const outputContent = await fs.readFile(bundleOutputFile, 'utf-8');
227
- const bundle = JSON.parse(outputContent);
228
-
229
- // Bundle should have the expected structure
230
- expect(bundle).toHaveProperty('metadata');
231
- expect(bundle).toHaveProperty('config');
232
- expect(bundle).toHaveProperty('compiledCollection');
233
-
234
- // Metadata should have checksum and date
235
- expect(bundle.metadata).toHaveProperty('checksum');
236
- expect(bundle.metadata).toHaveProperty('dateBuilt');
237
-
238
- // Config should be a valid system configuration
239
- expect(bundle.config).toHaveProperty('qualifierTypes');
240
- expect(bundle.config).toHaveProperty('qualifiers');
241
- expect(bundle.config).toHaveProperty('resourceTypes');
242
-
243
- // Compiled collection should have resources
244
- expect(bundle.compiledCollection).toHaveProperty('resources');
245
- expect(Array.isArray(bundle.compiledCollection.resources)).toBe(true);
246
- expect(bundle.compiledCollection.resources.length).toBeGreaterThan(0);
247
-
248
- // Check that resources include our test data
249
- const resourceIds = bundle.compiledCollection.resources.map((r: any) => r.id);
250
- expect(resourceIds).toContain('resources.app.title');
251
- expect(resourceIds).toContain('resources.messages.welcome');
252
-
253
- // Verify bundle can be loaded back into a resource manager
254
- expect(TsRes.Bundle.Convert.bundle.convert(bundle)).toSucceedAndSatisfy((validatedBundle) => {
255
- expect(TsRes.Bundle.BundleLoader.createManagerFromBundle({ bundle: validatedBundle })).toSucceed();
256
- return true;
257
- });
258
- });
259
-
260
- test('compiles with context filtering', async () => {
261
- const result = await runCli([
262
- 'compile',
263
- '--input',
264
- inputFile,
265
- '--output',
266
- outputFile,
267
- '--context',
268
- '{"language": "en"}',
269
- '--format',
270
- 'source'
271
- ]);
272
-
273
- expect(result.exitCode).toBe(0);
274
-
275
- const outputContent = await fs.readFile(outputFile, 'utf-8');
276
- const output = JSON.parse(outputContent);
277
-
278
- // Should include resources that match English
279
- expect(TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(output.resources)).toSucceedAndSatisfy(
280
- (resources) => {
281
- const resourceIds = resources?.resources?.map((r) => r.id) || [];
282
- expect(resourceIds).toContain('resources.messages.welcome');
283
- expect(resourceIds).toContain('resources.buttons.actions');
284
- return true;
285
- }
286
- );
287
- });
288
-
289
- test('compiles with specific territory context', async () => {
290
- const result = await runCli([
291
- 'compile',
292
- '--input',
293
- inputFile,
294
- '--output',
295
- outputFile,
296
- '--context',
297
- '{"language": "en-US", "currentTerritory": "US"}',
298
- '--format',
299
- 'source'
300
- ]);
301
-
302
- expect(result.exitCode).toBe(0);
303
-
304
- const outputContent = await fs.readFile(outputFile, 'utf-8');
305
- const output = JSON.parse(outputContent);
306
-
307
- expect(TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(output.resources)).toSucceedAndSatisfy(
308
- (resources) => {
309
- const resourceIds = resources?.resources?.map((r) => r.id) || [];
310
- expect(resourceIds).toContain('resources.messages.welcome');
311
-
312
- // Check that we get the US-specific variant
313
- const welcomeResource = resources?.resources?.find((r) => r.id === 'resources.messages.welcome');
314
- expect(welcomeResource).toBeDefined();
315
- const hasUSVariant = welcomeResource?.candidates?.some(
316
- (c) =>
317
- c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value === 'en-US') &&
318
- c.conditions?.some((cond) => cond.qualifierName === 'currentTerritory' && cond.value === 'US')
319
- );
320
- expect(hasUSVariant).toBe(true);
321
- return true;
322
- }
323
- );
324
- });
325
-
326
- test('compiles with metadata included', async () => {
327
- const result = await runCli([
328
- 'compile',
329
- '--input',
330
- inputFile,
331
- '--output',
332
- outputFile,
333
- '--include-metadata'
334
- ]);
335
-
336
- expect(result.exitCode).toBe(0);
337
-
338
- const outputContent = await fs.readFile(outputFile, 'utf-8');
339
- const output = JSON.parse(outputContent);
340
-
341
- // With compiled format and --include-metadata, output should have metadata and compiledCollection
342
- expect(output).toHaveProperty('metadata');
343
- expect(output).toHaveProperty('compiledCollection');
344
- expect(output.metadata).toHaveProperty('totalResources');
345
- expect(output.metadata).toHaveProperty('totalCandidates');
346
- expect(output.metadata).toHaveProperty('resourceTypes');
347
- expect(output.metadata).toHaveProperty('qualifiers');
348
- expect(output.metadata.totalResources).toBeGreaterThan(0);
349
- expect(output.metadata.resourceTypes).toContain('json');
350
- });
351
-
352
- test('handles quiet mode', async () => {
353
- const result = await runCli(['compile', '--input', inputFile, '--output', outputFile, '--quiet']);
354
-
355
- expect(result.exitCode).toBe(0);
356
- expect(result.stdout).not.toMatch(/Successfully compiled/);
357
- });
358
-
359
- test('handles errors gracefully', async () => {
360
- const result = await runCli(['compile', '--input', '/nonexistent/file.json', '--output', outputFile]);
361
-
362
- expect(result.exitCode).toBe(1);
363
- expect(result.stderr).toMatch(/Error:/);
364
- });
365
-
366
- test('validates input format', async () => {
367
- const result = await runCli([
368
- 'compile',
369
- '--input',
370
- inputFile,
371
- '--output',
372
- outputFile,
373
- '--format',
374
- 'invalid'
375
- ]);
376
-
377
- expect(result.exitCode).toBe(1);
378
- expect(result.stderr).toMatch(/Invalid format/);
379
- });
380
- });
381
-
382
- describe('Validate command integration', () => {
383
- test('validates correct resources', async () => {
384
- const inputFile = path.join(tempDir, 'valid-resources.json');
385
- const resources = {
386
- resources: [
387
- {
388
- id: 'test.resource',
389
- resourceTypeName: 'json',
390
- candidates: [
391
- {
392
- json: { message: 'Hello' },
393
- conditions: { language: 'en' }
394
- }
395
- ]
396
- }
397
- ]
398
- };
399
- await fs.writeFile(inputFile, JSON.stringify(resources));
400
-
401
- const result = await runCli(['validate', '--input', inputFile]);
402
-
403
- expect(result.exitCode).toBe(0);
404
- expect(result.stdout).toMatch(/Resources validated successfully/);
405
- });
406
-
407
- test('detects invalid resources', async () => {
408
- const inputFile = path.join(tempDir, 'invalid-resources.json');
409
- const resources = {
410
- resources: [
411
- {
412
- id: 'invalid resource id', // Invalid ID with spaces
413
- resourceTypeName: 'json',
414
- candidates: [
415
- {
416
- json: { message: 'Hello' },
417
- conditions: { language: 'en' }
418
- }
419
- ]
420
- }
421
- ]
422
- };
423
- await fs.writeFile(inputFile, JSON.stringify(resources));
424
-
425
- const result = await runCli(['validate', '--input', inputFile]);
426
-
427
- expect(result.exitCode).toBe(1);
428
- expect(result.stderr).toMatch(/Validation failed/);
429
- });
430
- });
431
-
432
- describe('Info command integration', () => {
433
- test('displays resource information', async () => {
434
- const inputFile = path.join(tempDir, 'info-resources.json');
435
- const resources = {
436
- resources: [
437
- {
438
- id: 'info.test1',
439
- resourceTypeName: 'json',
440
- candidates: [
441
- {
442
- json: { message: 'Hello' },
443
- conditions: { language: 'en' }
444
- }
445
- ]
446
- },
447
- {
448
- id: 'info.test2',
449
- resourceTypeName: 'json',
450
- candidates: [
451
- {
452
- json: { message: 'Hola' },
453
- conditions: { language: 'es' }
454
- }
455
- ]
456
- }
457
- ]
458
- };
459
- await fs.writeFile(inputFile, JSON.stringify(resources));
460
-
461
- const result = await runCli(['info', '--input', inputFile]);
462
-
463
- expect(result.exitCode).toBe(0);
464
-
465
- const info = JSON.parse(result.stdout);
466
- expect(info).toHaveProperty('totalResources');
467
- expect(info).toHaveProperty('totalCandidates');
468
- expect(info).toHaveProperty('resourceTypes');
469
- expect(info).toHaveProperty('qualifiers');
470
- expect(info.totalResources).toBeGreaterThan(0);
471
- expect(info.resourceTypes).toContain('json');
472
- expect(info.qualifiers).toContain('language');
473
- });
474
-
475
- test('displays filtered resource information', async () => {
476
- const inputFile = path.join(tempDir, 'filter-resources.json');
477
- const resources = {
478
- resources: [
479
- {
480
- id: 'filter.test1',
481
- resourceTypeName: 'json',
482
- candidates: [
483
- {
484
- json: { message: 'Hello' },
485
- conditions: { language: 'en' }
486
- }
487
- ]
488
- },
489
- {
490
- id: 'filter.test2',
491
- resourceTypeName: 'json',
492
- candidates: [
493
- {
494
- json: { message: 'Hola' },
495
- conditions: { language: 'es' }
496
- }
497
- ]
498
- }
499
- ]
500
- };
501
- await fs.writeFile(inputFile, JSON.stringify(resources));
502
-
503
- const result = await runCli(['info', '--input', inputFile, '--context', '{"language": "en"}']);
504
-
505
- expect(result.exitCode).toBe(0);
506
-
507
- const info = JSON.parse(result.stdout);
508
- expect(info).toHaveProperty('context');
509
- expect(info.context).toEqual({ language: 'en' });
510
- expect(info.filteredResources).toBeGreaterThan(0);
511
- expect(info.filteredCandidates).toBeGreaterThan(0);
512
- });
513
- });
514
-
515
- describe('Directory input integration', () => {
516
- test('processes multiple files in a directory', async () => {
517
- const inputDir = path.join(tempDir, 'resources');
518
- await fs.mkdir(inputDir);
519
-
520
- // Create multiple resource files
521
- const messages = {
522
- resources: [
523
- {
524
- id: 'dir.messages.hello',
525
- resourceTypeName: 'json',
526
- candidates: [
527
- {
528
- json: { text: 'Hello' },
529
- conditions: { language: 'en' }
530
- }
531
- ]
532
- }
533
- ]
534
- };
535
-
536
- const buttons = {
537
- resources: [
538
- {
539
- id: 'dir.buttons.ok',
540
- resourceTypeName: 'json',
541
- candidates: [
542
- {
543
- json: { label: 'OK' },
544
- conditions: { language: 'en' }
545
- }
546
- ]
547
- }
548
- ]
549
- };
550
-
551
- await fs.writeFile(path.join(inputDir, 'messages.json'), JSON.stringify(messages));
552
- await fs.writeFile(path.join(inputDir, 'buttons.json'), JSON.stringify(buttons));
553
-
554
- const outputFile = path.join(tempDir, 'directory-output.json');
555
- const result = await runCli([
556
- 'compile',
557
- '--input',
558
- inputDir,
559
- '--output',
560
- outputFile,
561
- '--format',
562
- 'source'
563
- ]);
564
-
565
- expect(result.exitCode).toBe(0);
566
-
567
- const outputContent = await fs.readFile(outputFile, 'utf-8');
568
- const output = JSON.parse(outputContent);
569
-
570
- expect(TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(output.resources)).toSucceedAndSatisfy(
571
- (resources) => {
572
- expect(resources?.resources?.length).toBe(2);
573
- const resourceIds = resources?.resources?.map((r) => r.id) || [];
574
- expect(resourceIds).toContain('resources.messages.dir.messages.hello');
575
- expect(resourceIds).toContain('resources.buttons.dir.buttons.ok');
576
- return true;
577
- }
578
- );
579
- });
580
- });
581
- });