@fgv/ts-res-cli 5.0.0-3 → 5.0.0-31
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 +50 -2
- package/eslint.config.js +16 -0
- package/lib/cli.d.ts +4 -0
- package/lib/cli.js +68 -2
- package/lib/compiler.d.ts +5 -0
- package/lib/compiler.js +36 -1
- package/lib/options.d.ts +1 -1
- package/package.json +19 -18
- package/.eslintrc.js +0 -9
- package/.rush/temp/chunked-rush-logs/ts-res-cli.build.chunks.jsonl +0 -6
- package/.rush/temp/operation/build/log-chunks.jsonl +0 -6
- package/.rush/temp/operation/build/state.json +0 -3
- package/.rush/temp/shrinkwrap-deps.json +0 -649
- package/config/jest.config.json +0 -9
- package/config/rig.json +0 -16
- package/lib/cli.d.ts.map +0 -1
- package/lib/cli.js.map +0 -1
- package/lib/compiler.d.ts.map +0 -1
- package/lib/compiler.js.map +0 -1
- package/lib/defaultConfiguration.d.ts.map +0 -1
- package/lib/defaultConfiguration.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/options.d.ts.map +0 -1
- package/lib/options.js.map +0 -1
- package/src/cli.ts +0 -607
- package/src/compiler.ts +0 -504
- package/src/defaultConfiguration.ts +0 -78
- package/src/index.ts +0 -25
- package/src/options.ts +0 -101
- package/test/integration/cli-configuration.test.ts +0 -419
- package/test/integration/cli-context-filter-working.test.ts +0 -341
- package/test/integration/cli-integration.test.ts +0 -533
- package/test/unit/cli.test.ts +0 -413
- package/test/unit/compiler.test.ts +0 -1084
- package/tsconfig.json +0 -8
|
@@ -1,1084 +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 { ResourceCompiler } from '../../src/compiler';
|
|
25
|
-
import { ICompileOptions } from '../../src/options';
|
|
26
|
-
import { promises as fs } from 'fs';
|
|
27
|
-
import * as path from 'path';
|
|
28
|
-
import * as TsRes from '@fgv/ts-res';
|
|
29
|
-
|
|
30
|
-
describe('ResourceCompiler', () => {
|
|
31
|
-
let tempDir: string;
|
|
32
|
-
let inputFile: string;
|
|
33
|
-
let outputFile: string;
|
|
34
|
-
|
|
35
|
-
beforeEach(async () => {
|
|
36
|
-
// Create temporary directory for test files
|
|
37
|
-
tempDir = await fs.mkdtemp(path.join(__dirname, '../../temp/test-temp-'));
|
|
38
|
-
inputFile = path.join(tempDir, 'input.json');
|
|
39
|
-
outputFile = path.join(tempDir, 'output.json');
|
|
40
|
-
|
|
41
|
-
// Create test input file in ResourceCollection format
|
|
42
|
-
const testResources = {
|
|
43
|
-
resources: [
|
|
44
|
-
{
|
|
45
|
-
id: 'test.message',
|
|
46
|
-
resourceTypeName: 'json',
|
|
47
|
-
candidates: [
|
|
48
|
-
{
|
|
49
|
-
json: { text: 'Hello' },
|
|
50
|
-
conditions: { language: 'en' }
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
json: { text: 'Hola' },
|
|
54
|
-
conditions: { language: 'es' }
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
};
|
|
60
|
-
await fs.writeFile(inputFile, JSON.stringify(testResources, null, 2));
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
afterEach(async () => {
|
|
64
|
-
// Clean up temporary directory
|
|
65
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('constructor', () => {
|
|
69
|
-
test('creates a compiler with options', () => {
|
|
70
|
-
const options: ICompileOptions = {
|
|
71
|
-
input: inputFile,
|
|
72
|
-
output: outputFile,
|
|
73
|
-
format: 'source',
|
|
74
|
-
debug: false,
|
|
75
|
-
verbose: false,
|
|
76
|
-
quiet: false,
|
|
77
|
-
validate: true,
|
|
78
|
-
includeMetadata: false
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const compiler = new ResourceCompiler(options);
|
|
82
|
-
expect(compiler).toBeDefined();
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('compile method', () => {
|
|
87
|
-
test('compiles resources to compiled format by default', async () => {
|
|
88
|
-
const options: ICompileOptions = {
|
|
89
|
-
input: inputFile,
|
|
90
|
-
output: outputFile,
|
|
91
|
-
format: 'compiled',
|
|
92
|
-
debug: false,
|
|
93
|
-
verbose: false,
|
|
94
|
-
quiet: false,
|
|
95
|
-
validate: true,
|
|
96
|
-
includeMetadata: false
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const compiler = new ResourceCompiler(options);
|
|
100
|
-
const result = await compiler.compile();
|
|
101
|
-
|
|
102
|
-
expect(result).toSucceed();
|
|
103
|
-
|
|
104
|
-
// Check that output file was created
|
|
105
|
-
const outputExists = await fs.stat(outputFile).then(
|
|
106
|
-
() => true,
|
|
107
|
-
() => false
|
|
108
|
-
);
|
|
109
|
-
expect(outputExists).toBe(true);
|
|
110
|
-
|
|
111
|
-
// Check compiled output structure
|
|
112
|
-
const outputContent = await fs.readFile(outputFile, 'utf-8');
|
|
113
|
-
const compiledCollectionData = JSON.parse(outputContent);
|
|
114
|
-
|
|
115
|
-
// Use the ts-res converter to validate the compiled resource collection
|
|
116
|
-
const validationResult =
|
|
117
|
-
TsRes.ResourceJson.Compiled.Convert.compiledResourceCollection.convert(compiledCollectionData);
|
|
118
|
-
expect(validationResult).toSucceedAndSatisfy((compiledCollection) => {
|
|
119
|
-
// Verify it's a properly structured compiled resource collection
|
|
120
|
-
expect(compiledCollection.qualifierTypes).toBeDefined();
|
|
121
|
-
expect(compiledCollection.qualifiers).toBeDefined();
|
|
122
|
-
expect(compiledCollection.resourceTypes).toBeDefined();
|
|
123
|
-
expect(compiledCollection.conditions).toBeDefined();
|
|
124
|
-
expect(compiledCollection.conditionSets).toBeDefined();
|
|
125
|
-
expect(compiledCollection.decisions).toBeDefined();
|
|
126
|
-
expect(compiledCollection.resources).toBeDefined();
|
|
127
|
-
|
|
128
|
-
// Verify these are arrays with expected content
|
|
129
|
-
expect(Array.isArray(compiledCollection.qualifierTypes)).toBe(true);
|
|
130
|
-
expect(Array.isArray(compiledCollection.qualifiers)).toBe(true);
|
|
131
|
-
expect(Array.isArray(compiledCollection.resourceTypes)).toBe(true);
|
|
132
|
-
expect(Array.isArray(compiledCollection.conditions)).toBe(true);
|
|
133
|
-
expect(Array.isArray(compiledCollection.conditionSets)).toBe(true);
|
|
134
|
-
expect(Array.isArray(compiledCollection.decisions)).toBe(true);
|
|
135
|
-
expect(Array.isArray(compiledCollection.resources)).toBe(true);
|
|
136
|
-
|
|
137
|
-
// Verify we have some resources
|
|
138
|
-
expect(compiledCollection.resources.length).toBeGreaterThan(0);
|
|
139
|
-
|
|
140
|
-
// Verify basic structural integrity
|
|
141
|
-
expect(compiledCollection.qualifierTypes.length).toBeGreaterThan(0);
|
|
142
|
-
expect(compiledCollection.qualifiers.length).toBeGreaterThan(0);
|
|
143
|
-
expect(compiledCollection.resourceTypes.length).toBeGreaterThan(0);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('compiles resources to compiled format with metadata', async () => {
|
|
148
|
-
const options: ICompileOptions = {
|
|
149
|
-
input: inputFile,
|
|
150
|
-
output: outputFile,
|
|
151
|
-
format: 'compiled',
|
|
152
|
-
debug: false,
|
|
153
|
-
verbose: false,
|
|
154
|
-
quiet: false,
|
|
155
|
-
validate: true,
|
|
156
|
-
includeMetadata: true
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const compiler = new ResourceCompiler(options);
|
|
160
|
-
const result = await compiler.compile();
|
|
161
|
-
|
|
162
|
-
expect(result).toSucceed();
|
|
163
|
-
|
|
164
|
-
const outputContent = await fs.readFile(outputFile, 'utf-8');
|
|
165
|
-
const outputData = JSON.parse(outputContent);
|
|
166
|
-
|
|
167
|
-
// When includeMetadata is true and format is compiled, it should wrap the compiled collection in a blob
|
|
168
|
-
expect(outputData).toHaveProperty('compiledCollection');
|
|
169
|
-
expect(outputData).toHaveProperty('metadata');
|
|
170
|
-
|
|
171
|
-
// Check metadata structure
|
|
172
|
-
expect(outputData.metadata).toHaveProperty('totalResources');
|
|
173
|
-
expect(outputData.metadata).toHaveProperty('totalCandidates');
|
|
174
|
-
expect(outputData.metadata).toHaveProperty('resourceTypes');
|
|
175
|
-
expect(outputData.metadata).toHaveProperty('qualifiers');
|
|
176
|
-
|
|
177
|
-
// Validate the compiled collection using ts-res converter
|
|
178
|
-
const compiledCollection = outputData.compiledCollection;
|
|
179
|
-
const validationResult =
|
|
180
|
-
TsRes.ResourceJson.Compiled.Convert.compiledResourceCollection.convert(compiledCollection);
|
|
181
|
-
expect(validationResult).toSucceedAndSatisfy((validCollection) => {
|
|
182
|
-
expect(validCollection.qualifierTypes).toBeDefined();
|
|
183
|
-
expect(validCollection.qualifiers).toBeDefined();
|
|
184
|
-
expect(validCollection.resourceTypes).toBeDefined();
|
|
185
|
-
expect(validCollection.conditions).toBeDefined();
|
|
186
|
-
expect(validCollection.conditionSets).toBeDefined();
|
|
187
|
-
expect(validCollection.decisions).toBeDefined();
|
|
188
|
-
expect(validCollection.resources).toBeDefined();
|
|
189
|
-
expect(validCollection.resources.length).toBeGreaterThan(0);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test('compiles resources to source format', async () => {
|
|
194
|
-
const options: ICompileOptions = {
|
|
195
|
-
input: inputFile,
|
|
196
|
-
output: outputFile,
|
|
197
|
-
format: 'source',
|
|
198
|
-
debug: false,
|
|
199
|
-
verbose: false,
|
|
200
|
-
quiet: false,
|
|
201
|
-
validate: true,
|
|
202
|
-
includeMetadata: false
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const compiler = new ResourceCompiler(options);
|
|
206
|
-
const result = await compiler.compile();
|
|
207
|
-
|
|
208
|
-
expect(result).toSucceed();
|
|
209
|
-
|
|
210
|
-
// Check that output file was created
|
|
211
|
-
const outputExists = await fs.stat(outputFile).then(
|
|
212
|
-
() => true,
|
|
213
|
-
() => false
|
|
214
|
-
);
|
|
215
|
-
expect(outputExists).toBe(true);
|
|
216
|
-
|
|
217
|
-
// Check output content
|
|
218
|
-
const outputContent = await fs.readFile(outputFile, 'utf-8');
|
|
219
|
-
const outputData = JSON.parse(outputContent);
|
|
220
|
-
|
|
221
|
-
expect(outputData).toHaveProperty('resources');
|
|
222
|
-
expect(
|
|
223
|
-
TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(outputData.resources)
|
|
224
|
-
).toSucceedAndSatisfy((resources) => {
|
|
225
|
-
expect(resources?.resources?.length).toBeGreaterThan(0);
|
|
226
|
-
expect(resources?.resources?.some((r) => r.id === 'input.test.message')).toBe(true);
|
|
227
|
-
return true;
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test('compiles with context filtering and excludes non-matching variants', async () => {
|
|
232
|
-
const options: ICompileOptions = {
|
|
233
|
-
input: inputFile,
|
|
234
|
-
output: outputFile,
|
|
235
|
-
contextFilter: 'language=en-US',
|
|
236
|
-
format: 'source',
|
|
237
|
-
debug: false,
|
|
238
|
-
verbose: false,
|
|
239
|
-
quiet: false,
|
|
240
|
-
validate: true,
|
|
241
|
-
includeMetadata: true
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const compiler = new ResourceCompiler(options);
|
|
245
|
-
const result = await compiler.compile();
|
|
246
|
-
|
|
247
|
-
expect(result).toSucceed();
|
|
248
|
-
|
|
249
|
-
const outputContent = await fs.readFile(outputFile, 'utf-8');
|
|
250
|
-
const outputData = JSON.parse(outputContent);
|
|
251
|
-
|
|
252
|
-
expect(
|
|
253
|
-
TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(outputData.resources)
|
|
254
|
-
).toSucceedAndSatisfy((resources) => {
|
|
255
|
-
const messageResource = resources?.resources?.find((r) => r.id === 'input.test.message');
|
|
256
|
-
expect(messageResource).toBeDefined();
|
|
257
|
-
|
|
258
|
-
// Should only contain English variants
|
|
259
|
-
const hasEnglish = messageResource?.candidates?.some((c) =>
|
|
260
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value.includes('en'))
|
|
261
|
-
);
|
|
262
|
-
expect(hasEnglish).toBe(true);
|
|
263
|
-
|
|
264
|
-
// Should NOT contain Spanish variants
|
|
265
|
-
const hasSpanish = messageResource?.candidates?.some((c) =>
|
|
266
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value.includes('es'))
|
|
267
|
-
);
|
|
268
|
-
expect(hasSpanish).toBe(false);
|
|
269
|
-
|
|
270
|
-
return true;
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// Verify metadata shows filtering occurred
|
|
274
|
-
expect(outputData.metadata).toBeDefined();
|
|
275
|
-
expect(outputData.metadata.totalCandidates).toBe(2); // Both en and es
|
|
276
|
-
expect(outputData.metadata.filteredCandidates).toBe(1); // Only en after filtering
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test('context filtering with multiple resources shows selective filtering', async () => {
|
|
280
|
-
// Create a more complex test file with multiple resources and languages
|
|
281
|
-
const complexTestFile = path.join(tempDir, 'complex-input.json');
|
|
282
|
-
const complexTestResources = {
|
|
283
|
-
resources: [
|
|
284
|
-
{
|
|
285
|
-
id: 'greeting.hello',
|
|
286
|
-
resourceTypeName: 'json',
|
|
287
|
-
candidates: [
|
|
288
|
-
{
|
|
289
|
-
json: { text: 'Hello' },
|
|
290
|
-
conditions: { language: 'en' }
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
json: { text: 'Hola' },
|
|
294
|
-
conditions: { language: 'es' }
|
|
295
|
-
}
|
|
296
|
-
]
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
id: 'button.ok',
|
|
300
|
-
resourceTypeName: 'json',
|
|
301
|
-
candidates: [
|
|
302
|
-
{
|
|
303
|
-
json: { label: 'OK' },
|
|
304
|
-
conditions: { language: 'en' }
|
|
305
|
-
}
|
|
306
|
-
]
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
id: 'error.notfound',
|
|
310
|
-
resourceTypeName: 'json',
|
|
311
|
-
candidates: [
|
|
312
|
-
{
|
|
313
|
-
json: { message: 'Not found' },
|
|
314
|
-
conditions: { language: 'en' }
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
json: { message: 'No encontrado' },
|
|
318
|
-
conditions: { language: 'es' }
|
|
319
|
-
}
|
|
320
|
-
]
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
id: 'app.name',
|
|
324
|
-
resourceTypeName: 'json',
|
|
325
|
-
candidates: [
|
|
326
|
-
{
|
|
327
|
-
json: { title: 'My App' }
|
|
328
|
-
}
|
|
329
|
-
]
|
|
330
|
-
}
|
|
331
|
-
]
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
await fs.writeFile(complexTestFile, JSON.stringify(complexTestResources, null, 2));
|
|
335
|
-
|
|
336
|
-
const complexOutputFile = path.join(tempDir, 'complex-output.json');
|
|
337
|
-
const options: ICompileOptions = {
|
|
338
|
-
input: complexTestFile,
|
|
339
|
-
output: complexOutputFile,
|
|
340
|
-
contextFilter: 'language=en',
|
|
341
|
-
format: 'source',
|
|
342
|
-
debug: false,
|
|
343
|
-
verbose: false,
|
|
344
|
-
quiet: false,
|
|
345
|
-
validate: true,
|
|
346
|
-
includeMetadata: true
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
const compiler = new ResourceCompiler(options);
|
|
350
|
-
const result = await compiler.compile();
|
|
351
|
-
expect(result).toSucceed();
|
|
352
|
-
|
|
353
|
-
const outputContent = await fs.readFile(complexOutputFile, 'utf-8');
|
|
354
|
-
const outputData = JSON.parse(outputContent);
|
|
355
|
-
|
|
356
|
-
// Verify what should be included using ResourceCollectionDecl format
|
|
357
|
-
expect(
|
|
358
|
-
TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(outputData.resources)
|
|
359
|
-
).toSucceedAndSatisfy((resources) => {
|
|
360
|
-
const resourceIds = resources?.resources?.map((r) => r.id) || [];
|
|
361
|
-
expect(resourceIds).toContain('complex-input.greeting.hello'); // Has en variant
|
|
362
|
-
expect(resourceIds).toContain('complex-input.button.ok'); // Only has en
|
|
363
|
-
expect(resourceIds).toContain('complex-input.app.name'); // No language condition (default)
|
|
364
|
-
expect(resourceIds).toContain('complex-input.error.notfound'); // Now has en
|
|
365
|
-
|
|
366
|
-
// Verify greeting.hello only has English, not Spanish
|
|
367
|
-
const greetingResource = resources?.resources?.find((r) => r.id === 'complex-input.greeting.hello');
|
|
368
|
-
expect(greetingResource).toBeDefined();
|
|
369
|
-
|
|
370
|
-
const hasEnglish = greetingResource?.candidates?.some((c) =>
|
|
371
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value.includes('en'))
|
|
372
|
-
);
|
|
373
|
-
expect(hasEnglish).toBe(true);
|
|
374
|
-
|
|
375
|
-
const hasSpanish = greetingResource?.candidates?.some((c) =>
|
|
376
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value.includes('es'))
|
|
377
|
-
);
|
|
378
|
-
expect(hasSpanish).toBe(false);
|
|
379
|
-
|
|
380
|
-
return true;
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
// Verify metadata
|
|
384
|
-
expect(outputData.metadata.totalCandidates).toBe(6); // All 6 candidates (added one more to error.notfound)
|
|
385
|
-
expect(outputData.metadata.filteredCandidates).toBe(4); // greeting.hello[en], button.ok[en], error.notfound[en], app.name[default]
|
|
386
|
-
expect(outputData.metadata.totalResources).toBe(4); // 4 unique resource IDs
|
|
387
|
-
expect(outputData.metadata.filteredResources).toBe(4); // All 4 resources have English or default candidates
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test.skip('context filtering with no matches produces empty output', async () => {
|
|
391
|
-
const options: ICompileOptions = {
|
|
392
|
-
input: inputFile,
|
|
393
|
-
output: outputFile,
|
|
394
|
-
contextFilter: 'language=fr', // French - not in our test data
|
|
395
|
-
format: 'source',
|
|
396
|
-
debug: false,
|
|
397
|
-
verbose: false,
|
|
398
|
-
quiet: false,
|
|
399
|
-
validate: true,
|
|
400
|
-
includeMetadata: true
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
const compiler = new ResourceCompiler(options);
|
|
404
|
-
const result = await compiler.compile();
|
|
405
|
-
expect(result).toSucceed();
|
|
406
|
-
|
|
407
|
-
const outputContent = await fs.readFile(outputFile, 'utf-8');
|
|
408
|
-
const outputData = JSON.parse(outputContent);
|
|
409
|
-
|
|
410
|
-
// Should have no resources in output using ResourceCollectionDecl format
|
|
411
|
-
expect(
|
|
412
|
-
TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(outputData.resources)
|
|
413
|
-
).toSucceedAndSatisfy((resources) => {
|
|
414
|
-
expect(resources?.resources?.length || 0).toBe(0);
|
|
415
|
-
return true;
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// Metadata should show filtering occurred
|
|
419
|
-
expect(outputData.metadata.totalCandidates).toBe(2); // Both en and es in input
|
|
420
|
-
expect(outputData.metadata.filteredCandidates).toBe(0); // None match French
|
|
421
|
-
expect(outputData.metadata.totalResources).toBe(1); // 1 resource ID in input
|
|
422
|
-
expect(outputData.metadata.filteredResources).toBe(0); // No resources match
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
test('context filtering with language variants uses fuzzy matching', async () => {
|
|
426
|
-
// Create test data with various English variants and other languages
|
|
427
|
-
const variantTestFile = path.join(tempDir, 'variant-input.json');
|
|
428
|
-
const variantTestResources = {
|
|
429
|
-
resources: [
|
|
430
|
-
{
|
|
431
|
-
id: 'msg.hello',
|
|
432
|
-
resourceTypeName: 'json',
|
|
433
|
-
candidates: [
|
|
434
|
-
{
|
|
435
|
-
json: { text: 'Hello (US)' },
|
|
436
|
-
conditions: { language: 'en-US' }
|
|
437
|
-
},
|
|
438
|
-
{
|
|
439
|
-
json: { text: 'Hello (CA)' },
|
|
440
|
-
conditions: { language: 'en-CA' }
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
json: { text: 'Hello (AU)' },
|
|
444
|
-
conditions: { language: 'en-AU' }
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
json: { text: 'Hello (generic)' },
|
|
448
|
-
conditions: { language: 'en' }
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
json: { text: 'Bonjour (FR)' },
|
|
452
|
-
conditions: { language: 'fr-FR' }
|
|
453
|
-
},
|
|
454
|
-
{
|
|
455
|
-
json: { text: 'Bonjour (CA)' },
|
|
456
|
-
conditions: { language: 'fr-CA' }
|
|
457
|
-
},
|
|
458
|
-
{
|
|
459
|
-
json: { text: 'Bonjour (generic)' },
|
|
460
|
-
conditions: { language: 'fr' }
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
json: { text: 'Hola (ES)' },
|
|
464
|
-
conditions: { language: 'es-ES' }
|
|
465
|
-
},
|
|
466
|
-
{
|
|
467
|
-
json: { text: 'Hola (MX)' },
|
|
468
|
-
conditions: { language: 'es-MX' }
|
|
469
|
-
}
|
|
470
|
-
]
|
|
471
|
-
},
|
|
472
|
-
{
|
|
473
|
-
id: 'btn.save',
|
|
474
|
-
resourceTypeName: 'json',
|
|
475
|
-
candidates: [
|
|
476
|
-
{
|
|
477
|
-
json: { label: 'Save (US)' },
|
|
478
|
-
conditions: { language: 'en-US' }
|
|
479
|
-
},
|
|
480
|
-
{
|
|
481
|
-
json: { label: 'Save (FR)' },
|
|
482
|
-
conditions: { language: 'fr-FR' }
|
|
483
|
-
}
|
|
484
|
-
]
|
|
485
|
-
}
|
|
486
|
-
]
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
await fs.writeFile(variantTestFile, JSON.stringify(variantTestResources, null, 2));
|
|
490
|
-
|
|
491
|
-
const variantOutputFile = path.join(tempDir, 'variant-output.json');
|
|
492
|
-
const options: ICompileOptions = {
|
|
493
|
-
input: variantTestFile,
|
|
494
|
-
output: variantOutputFile,
|
|
495
|
-
contextFilter: 'language=en-GB', // British English - should match other English variants
|
|
496
|
-
format: 'source',
|
|
497
|
-
debug: false,
|
|
498
|
-
verbose: false,
|
|
499
|
-
quiet: false,
|
|
500
|
-
validate: true,
|
|
501
|
-
includeMetadata: true
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
const compiler = new ResourceCompiler(options);
|
|
505
|
-
const result = await compiler.compile();
|
|
506
|
-
expect(result).toSucceed();
|
|
507
|
-
|
|
508
|
-
const outputContent = await fs.readFile(variantOutputFile, 'utf-8');
|
|
509
|
-
const outputData = JSON.parse(outputContent);
|
|
510
|
-
|
|
511
|
-
// Verify all resources that should match are included using ResourceCollectionDecl format
|
|
512
|
-
expect(
|
|
513
|
-
TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(outputData.resources)
|
|
514
|
-
).toSucceedAndSatisfy((resources) => {
|
|
515
|
-
const resourceIds = resources?.resources?.map((r) => r.id) || [];
|
|
516
|
-
expect(resourceIds).toContain('variant-input.msg.hello');
|
|
517
|
-
expect(resourceIds).toContain('variant-input.btn.save');
|
|
518
|
-
|
|
519
|
-
// Verify msg.hello contains English variants but not French/Spanish
|
|
520
|
-
const helloResource = resources?.resources?.find((r) => r.id === 'variant-input.msg.hello');
|
|
521
|
-
expect(helloResource).toBeDefined();
|
|
522
|
-
|
|
523
|
-
// Should include English variants (fuzzy matching)
|
|
524
|
-
const hasEnUS = helloResource?.candidates?.some((c) =>
|
|
525
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value === 'en-US')
|
|
526
|
-
);
|
|
527
|
-
expect(hasEnUS).toBe(true);
|
|
528
|
-
|
|
529
|
-
const hasEnCA = helloResource?.candidates?.some((c) =>
|
|
530
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value === 'en-CA')
|
|
531
|
-
);
|
|
532
|
-
expect(hasEnCA).toBe(true);
|
|
533
|
-
|
|
534
|
-
const hasEnAU = helloResource?.candidates?.some((c) =>
|
|
535
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value === 'en-AU')
|
|
536
|
-
);
|
|
537
|
-
expect(hasEnAU).toBe(true);
|
|
538
|
-
|
|
539
|
-
const hasGenericEn = helloResource?.candidates?.some((c) =>
|
|
540
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value === 'en')
|
|
541
|
-
);
|
|
542
|
-
expect(hasGenericEn).toBe(true);
|
|
543
|
-
|
|
544
|
-
// Should NOT include French or Spanish variants
|
|
545
|
-
const hasFrench = helloResource?.candidates?.some((c) =>
|
|
546
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value.includes('fr'))
|
|
547
|
-
);
|
|
548
|
-
expect(hasFrench).toBe(false);
|
|
549
|
-
|
|
550
|
-
const hasSpanish = helloResource?.candidates?.some((c) =>
|
|
551
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value.includes('es'))
|
|
552
|
-
);
|
|
553
|
-
expect(hasSpanish).toBe(false);
|
|
554
|
-
|
|
555
|
-
// Verify btn.save only has English variants
|
|
556
|
-
const saveResource = resources?.resources?.find((r) => r.id === 'variant-input.btn.save');
|
|
557
|
-
expect(saveResource).toBeDefined();
|
|
558
|
-
|
|
559
|
-
const saveHasEnUS = saveResource?.candidates?.some((c) =>
|
|
560
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value === 'en-US')
|
|
561
|
-
);
|
|
562
|
-
expect(saveHasEnUS).toBe(true);
|
|
563
|
-
|
|
564
|
-
const saveHasFrench = saveResource?.candidates?.some((c) =>
|
|
565
|
-
c.conditions?.some((cond) => cond.qualifierName === 'language' && cond.value.includes('fr'))
|
|
566
|
-
);
|
|
567
|
-
expect(saveHasFrench).toBe(false);
|
|
568
|
-
|
|
569
|
-
return true;
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// Verify metadata shows appropriate filtering
|
|
573
|
-
expect(outputData.metadata.totalCandidates).toBe(11); // All input candidates
|
|
574
|
-
// Should filter to: 4 English msg.hello + 1 English btn.save = 5
|
|
575
|
-
expect(outputData.metadata.filteredCandidates).toBe(5);
|
|
576
|
-
expect(outputData.metadata.totalResources).toBe(2); // 2 unique resource IDs
|
|
577
|
-
expect(outputData.metadata.filteredResources).toBe(2); // All 2 resources have matching candidates
|
|
578
|
-
});
|
|
579
|
-
test('compiles with metadata included', async () => {
|
|
580
|
-
const options: ICompileOptions = {
|
|
581
|
-
input: inputFile,
|
|
582
|
-
output: outputFile,
|
|
583
|
-
format: 'source',
|
|
584
|
-
debug: false,
|
|
585
|
-
verbose: false,
|
|
586
|
-
quiet: false,
|
|
587
|
-
validate: true,
|
|
588
|
-
includeMetadata: true
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
const compiler = new ResourceCompiler(options);
|
|
592
|
-
const result = await compiler.compile();
|
|
593
|
-
|
|
594
|
-
expect(result).toSucceed();
|
|
595
|
-
|
|
596
|
-
const outputContent = await fs.readFile(outputFile, 'utf-8');
|
|
597
|
-
const outputData = JSON.parse(outputContent);
|
|
598
|
-
|
|
599
|
-
expect(outputData).toHaveProperty('metadata');
|
|
600
|
-
expect(outputData.metadata).toHaveProperty('totalResources');
|
|
601
|
-
expect(outputData.metadata).toHaveProperty('totalCandidates');
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
test('compiles to JavaScript format', async () => {
|
|
605
|
-
const jsOutputFile = path.join(tempDir, 'output.js');
|
|
606
|
-
const options: ICompileOptions = {
|
|
607
|
-
input: inputFile,
|
|
608
|
-
output: jsOutputFile,
|
|
609
|
-
format: 'js',
|
|
610
|
-
debug: false,
|
|
611
|
-
verbose: false,
|
|
612
|
-
quiet: false,
|
|
613
|
-
validate: true,
|
|
614
|
-
includeMetadata: false
|
|
615
|
-
};
|
|
616
|
-
|
|
617
|
-
const compiler = new ResourceCompiler(options);
|
|
618
|
-
const result = await compiler.compile();
|
|
619
|
-
|
|
620
|
-
expect(result).toSucceed();
|
|
621
|
-
|
|
622
|
-
const outputContent = await fs.readFile(jsOutputFile, 'utf-8');
|
|
623
|
-
expect(outputContent).toMatch(/^module\.exports = /);
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
test('compiles to TypeScript format', async () => {
|
|
627
|
-
const tsOutputFile = path.join(tempDir, 'output.ts');
|
|
628
|
-
const options: ICompileOptions = {
|
|
629
|
-
input: inputFile,
|
|
630
|
-
output: tsOutputFile,
|
|
631
|
-
format: 'ts',
|
|
632
|
-
|
|
633
|
-
debug: false,
|
|
634
|
-
verbose: false,
|
|
635
|
-
quiet: false,
|
|
636
|
-
validate: true,
|
|
637
|
-
includeMetadata: false
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
const compiler = new ResourceCompiler(options);
|
|
641
|
-
const result = await compiler.compile();
|
|
642
|
-
|
|
643
|
-
expect(result).toSucceed();
|
|
644
|
-
|
|
645
|
-
const outputContent = await fs.readFile(tsOutputFile, 'utf-8');
|
|
646
|
-
expect(outputContent).toMatch(/^export const resources = /);
|
|
647
|
-
expect(outputContent).toMatch(/ as const;$/);
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
test('compiled format can be reconstructed as runtime ICompiledResourceCollection', async () => {
|
|
651
|
-
const options: ICompileOptions = {
|
|
652
|
-
input: inputFile,
|
|
653
|
-
output: outputFile,
|
|
654
|
-
format: 'compiled',
|
|
655
|
-
debug: false,
|
|
656
|
-
verbose: false,
|
|
657
|
-
quiet: false,
|
|
658
|
-
validate: true,
|
|
659
|
-
includeMetadata: false
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
const compiler = new ResourceCompiler(options);
|
|
663
|
-
const result = await compiler.compile();
|
|
664
|
-
expect(result).toSucceed();
|
|
665
|
-
|
|
666
|
-
const outputContent = await fs.readFile(outputFile, 'utf-8');
|
|
667
|
-
const compiledCollectionData = JSON.parse(outputContent);
|
|
668
|
-
|
|
669
|
-
// Validate the structure using the converter
|
|
670
|
-
expect(
|
|
671
|
-
TsRes.ResourceJson.Compiled.Convert.compiledResourceCollection.convert(compiledCollectionData)
|
|
672
|
-
).toSucceed();
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
test('compiled format produces different output than source format', async () => {
|
|
676
|
-
const compiledOutputFile = path.join(tempDir, 'compiled.json');
|
|
677
|
-
const sourceOutputFile = path.join(tempDir, 'source.json');
|
|
678
|
-
|
|
679
|
-
const baseOptions: ICompileOptions = {
|
|
680
|
-
input: inputFile,
|
|
681
|
-
output: '',
|
|
682
|
-
format: 'compiled',
|
|
683
|
-
debug: false,
|
|
684
|
-
verbose: false,
|
|
685
|
-
quiet: false,
|
|
686
|
-
validate: true,
|
|
687
|
-
includeMetadata: false
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
// Compile to compiled format
|
|
691
|
-
const compiledCompiler = new ResourceCompiler({
|
|
692
|
-
...baseOptions,
|
|
693
|
-
output: compiledOutputFile,
|
|
694
|
-
format: 'compiled'
|
|
695
|
-
});
|
|
696
|
-
const compiledResult = await compiledCompiler.compile();
|
|
697
|
-
expect(compiledResult).toSucceed();
|
|
698
|
-
|
|
699
|
-
// Compile to source format
|
|
700
|
-
const sourceCompiler = new ResourceCompiler({
|
|
701
|
-
...baseOptions,
|
|
702
|
-
output: sourceOutputFile,
|
|
703
|
-
format: 'source'
|
|
704
|
-
});
|
|
705
|
-
const sourceResult = await sourceCompiler.compile();
|
|
706
|
-
expect(sourceResult).toSucceed();
|
|
707
|
-
|
|
708
|
-
// Read both outputs
|
|
709
|
-
const compiledContent = await fs.readFile(compiledOutputFile, 'utf-8');
|
|
710
|
-
const sourceContent = await fs.readFile(sourceOutputFile, 'utf-8');
|
|
711
|
-
|
|
712
|
-
const compiledData = JSON.parse(compiledContent);
|
|
713
|
-
const sourceData = JSON.parse(sourceContent);
|
|
714
|
-
|
|
715
|
-
// They should be different formats
|
|
716
|
-
expect(compiledData).not.toEqual(sourceData);
|
|
717
|
-
|
|
718
|
-
// Validate compiled format using ts-res converter
|
|
719
|
-
const compiledValidation =
|
|
720
|
-
TsRes.ResourceJson.Compiled.Convert.compiledResourceCollection.convert(compiledData);
|
|
721
|
-
expect(compiledValidation).toSucceedAndSatisfy((validCompiledCollection) => {
|
|
722
|
-
expect(validCompiledCollection.qualifierTypes).toBeDefined();
|
|
723
|
-
expect(validCompiledCollection.qualifiers).toBeDefined();
|
|
724
|
-
expect(validCompiledCollection.resourceTypes).toBeDefined();
|
|
725
|
-
expect(validCompiledCollection.conditions).toBeDefined();
|
|
726
|
-
expect(validCompiledCollection.conditionSets).toBeDefined();
|
|
727
|
-
expect(validCompiledCollection.decisions).toBeDefined();
|
|
728
|
-
expect(validCompiledCollection.resources).toBeDefined();
|
|
729
|
-
expect(validCompiledCollection.resources.length).toBeGreaterThan(0);
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
// Source format should have legacy blob structure
|
|
733
|
-
expect(sourceData).toHaveProperty('resources');
|
|
734
|
-
expect(sourceData).not.toHaveProperty('qualifierTypes');
|
|
735
|
-
expect(sourceData).not.toHaveProperty('conditions');
|
|
736
|
-
expect(sourceData).not.toHaveProperty('conditionSets');
|
|
737
|
-
expect(sourceData).not.toHaveProperty('decisions');
|
|
738
|
-
|
|
739
|
-
// Source format resources should be in ResourceCollectionDecl format
|
|
740
|
-
expect(
|
|
741
|
-
TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(sourceData.resources)
|
|
742
|
-
).toSucceedAndSatisfy((resources) => {
|
|
743
|
-
const messageResource = resources?.resources?.find((r) => r.id === 'input.test.message');
|
|
744
|
-
expect(messageResource).toBeDefined();
|
|
745
|
-
return true;
|
|
746
|
-
});
|
|
747
|
-
});
|
|
748
|
-
test('fails with invalid input file', async () => {
|
|
749
|
-
const options: ICompileOptions = {
|
|
750
|
-
input: '/nonexistent/file.json',
|
|
751
|
-
output: outputFile,
|
|
752
|
-
format: 'source',
|
|
753
|
-
debug: false,
|
|
754
|
-
verbose: false,
|
|
755
|
-
quiet: false,
|
|
756
|
-
validate: true,
|
|
757
|
-
includeMetadata: false
|
|
758
|
-
};
|
|
759
|
-
|
|
760
|
-
const compiler = new ResourceCompiler(options);
|
|
761
|
-
const result = await compiler.compile();
|
|
762
|
-
|
|
763
|
-
expect(result).toFailWith(/Failed to load resources/);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
test('handles directory input', async () => {
|
|
767
|
-
const inputDir = path.join(tempDir, 'resources');
|
|
768
|
-
await fs.mkdir(inputDir);
|
|
769
|
-
|
|
770
|
-
const resource1 = path.join(inputDir, 'messages.json');
|
|
771
|
-
const resource2 = path.join(inputDir, 'buttons.json');
|
|
772
|
-
|
|
773
|
-
await fs.writeFile(
|
|
774
|
-
resource1,
|
|
775
|
-
JSON.stringify({
|
|
776
|
-
resources: [
|
|
777
|
-
{
|
|
778
|
-
id: 'msg.hello',
|
|
779
|
-
resourceTypeName: 'json',
|
|
780
|
-
candidates: [
|
|
781
|
-
{
|
|
782
|
-
json: { text: 'Hello' },
|
|
783
|
-
conditions: { language: 'en' }
|
|
784
|
-
}
|
|
785
|
-
]
|
|
786
|
-
}
|
|
787
|
-
]
|
|
788
|
-
})
|
|
789
|
-
);
|
|
790
|
-
|
|
791
|
-
await fs.writeFile(
|
|
792
|
-
resource2,
|
|
793
|
-
JSON.stringify({
|
|
794
|
-
resources: [
|
|
795
|
-
{
|
|
796
|
-
id: 'btn.ok',
|
|
797
|
-
resourceTypeName: 'json',
|
|
798
|
-
candidates: [
|
|
799
|
-
{
|
|
800
|
-
json: { label: 'OK' },
|
|
801
|
-
conditions: { language: 'en' }
|
|
802
|
-
}
|
|
803
|
-
]
|
|
804
|
-
}
|
|
805
|
-
]
|
|
806
|
-
})
|
|
807
|
-
);
|
|
808
|
-
|
|
809
|
-
const options: ICompileOptions = {
|
|
810
|
-
input: inputDir,
|
|
811
|
-
output: outputFile,
|
|
812
|
-
format: 'source',
|
|
813
|
-
debug: false,
|
|
814
|
-
verbose: false,
|
|
815
|
-
quiet: false,
|
|
816
|
-
validate: true,
|
|
817
|
-
includeMetadata: false
|
|
818
|
-
};
|
|
819
|
-
|
|
820
|
-
const compiler = new ResourceCompiler(options);
|
|
821
|
-
const result = await compiler.compile();
|
|
822
|
-
|
|
823
|
-
expect(result).toSucceed();
|
|
824
|
-
|
|
825
|
-
const outputContent = await fs.readFile(outputFile, 'utf-8');
|
|
826
|
-
const outputData = JSON.parse(outputContent);
|
|
827
|
-
|
|
828
|
-
expect(
|
|
829
|
-
TsRes.ResourceJson.Convert.resourceCollectionDecl.convert(outputData.resources)
|
|
830
|
-
).toSucceedAndSatisfy((resources) => {
|
|
831
|
-
const resourceIds = resources?.resources?.map((r) => r.id) || [];
|
|
832
|
-
expect(resourceIds).toContain('resources.messages.msg.hello');
|
|
833
|
-
expect(resourceIds).toContain('resources.buttons.btn.ok');
|
|
834
|
-
return true;
|
|
835
|
-
});
|
|
836
|
-
});
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
describe('idempotency and stability tests', () => {
|
|
840
|
-
test('produces identical output when compiled multiple times', async () => {
|
|
841
|
-
const options: ICompileOptions = {
|
|
842
|
-
input: inputFile,
|
|
843
|
-
output: outputFile,
|
|
844
|
-
format: 'source',
|
|
845
|
-
debug: false,
|
|
846
|
-
verbose: false,
|
|
847
|
-
quiet: false,
|
|
848
|
-
validate: true,
|
|
849
|
-
includeMetadata: true
|
|
850
|
-
};
|
|
851
|
-
|
|
852
|
-
// First compilation
|
|
853
|
-
const compiler1 = new ResourceCompiler(options);
|
|
854
|
-
const result1 = await compiler1.compile();
|
|
855
|
-
expect(result1).toSucceed();
|
|
856
|
-
|
|
857
|
-
const output1 = await fs.readFile(outputFile, 'utf-8');
|
|
858
|
-
const data1 = JSON.parse(output1);
|
|
859
|
-
|
|
860
|
-
// Second compilation with same input
|
|
861
|
-
const outputFile2 = path.join(tempDir, 'output2.json');
|
|
862
|
-
const options2 = { ...options, output: outputFile2 };
|
|
863
|
-
const compiler2 = new ResourceCompiler(options2);
|
|
864
|
-
const result2 = await compiler2.compile();
|
|
865
|
-
expect(result2).toSucceed();
|
|
866
|
-
|
|
867
|
-
const output2 = await fs.readFile(outputFile2, 'utf-8');
|
|
868
|
-
const data2 = JSON.parse(output2);
|
|
869
|
-
|
|
870
|
-
// Outputs should be identical
|
|
871
|
-
expect(data1).toEqual(data2);
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
test('produces deterministic output regardless of compilation order', async () => {
|
|
875
|
-
// Test with a directory containing multiple files to ensure
|
|
876
|
-
// file processing order doesn't affect output
|
|
877
|
-
const testDir = path.join(tempDir, 'multi-files');
|
|
878
|
-
await fs.mkdir(testDir, { recursive: true });
|
|
879
|
-
|
|
880
|
-
// Create multiple resource files
|
|
881
|
-
const file1 = path.join(testDir, 'first.json');
|
|
882
|
-
const file2 = path.join(testDir, 'second.json');
|
|
883
|
-
const file3 = path.join(testDir, 'third.json');
|
|
884
|
-
|
|
885
|
-
await fs.writeFile(
|
|
886
|
-
file1,
|
|
887
|
-
JSON.stringify({
|
|
888
|
-
resources: [
|
|
889
|
-
{
|
|
890
|
-
id: 'msg.hello',
|
|
891
|
-
resourceTypeName: 'json',
|
|
892
|
-
candidates: [
|
|
893
|
-
{
|
|
894
|
-
json: { text: 'Hello' },
|
|
895
|
-
conditions: { language: 'en' }
|
|
896
|
-
}
|
|
897
|
-
]
|
|
898
|
-
}
|
|
899
|
-
]
|
|
900
|
-
})
|
|
901
|
-
);
|
|
902
|
-
await fs.writeFile(
|
|
903
|
-
file2,
|
|
904
|
-
JSON.stringify({
|
|
905
|
-
resources: [
|
|
906
|
-
{
|
|
907
|
-
id: 'msg.goodbye',
|
|
908
|
-
resourceTypeName: 'json',
|
|
909
|
-
candidates: [
|
|
910
|
-
{
|
|
911
|
-
json: { text: 'Goodbye' },
|
|
912
|
-
conditions: { language: 'en' }
|
|
913
|
-
}
|
|
914
|
-
]
|
|
915
|
-
}
|
|
916
|
-
]
|
|
917
|
-
})
|
|
918
|
-
);
|
|
919
|
-
await fs.writeFile(
|
|
920
|
-
file3,
|
|
921
|
-
JSON.stringify({
|
|
922
|
-
resources: [
|
|
923
|
-
{
|
|
924
|
-
id: 'msg.welcome',
|
|
925
|
-
resourceTypeName: 'json',
|
|
926
|
-
candidates: [
|
|
927
|
-
{
|
|
928
|
-
json: { text: 'Welcome' },
|
|
929
|
-
conditions: { language: 'en' }
|
|
930
|
-
}
|
|
931
|
-
]
|
|
932
|
-
}
|
|
933
|
-
]
|
|
934
|
-
})
|
|
935
|
-
);
|
|
936
|
-
|
|
937
|
-
const options: ICompileOptions = {
|
|
938
|
-
input: testDir,
|
|
939
|
-
output: path.join(tempDir, 'deterministic-output.json'),
|
|
940
|
-
format: 'source',
|
|
941
|
-
debug: false,
|
|
942
|
-
verbose: false,
|
|
943
|
-
quiet: false,
|
|
944
|
-
validate: true,
|
|
945
|
-
includeMetadata: false
|
|
946
|
-
};
|
|
947
|
-
|
|
948
|
-
// Compile multiple times
|
|
949
|
-
const outputs: string[] = [];
|
|
950
|
-
for (let i = 0; i < 3; i++) {
|
|
951
|
-
const outputFile = path.join(tempDir, `deterministic-${i}.json`);
|
|
952
|
-
const compiler = new ResourceCompiler({ ...options, output: outputFile });
|
|
953
|
-
const result = await compiler.compile();
|
|
954
|
-
expect(result).toSucceed();
|
|
955
|
-
|
|
956
|
-
const output = await fs.readFile(outputFile, 'utf-8');
|
|
957
|
-
outputs.push(output);
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// All outputs should be identical
|
|
961
|
-
expect(outputs[0]).toBe(outputs[1]);
|
|
962
|
-
expect(outputs[1]).toBe(outputs[2]);
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
test('produces identical output across different format conversions', async () => {
|
|
966
|
-
const baseOptions: ICompileOptions = {
|
|
967
|
-
input: inputFile,
|
|
968
|
-
output: '', // Will be set per format
|
|
969
|
-
format: 'source',
|
|
970
|
-
debug: false,
|
|
971
|
-
verbose: false,
|
|
972
|
-
quiet: false,
|
|
973
|
-
validate: true,
|
|
974
|
-
includeMetadata: false
|
|
975
|
-
};
|
|
976
|
-
|
|
977
|
-
// Test source format
|
|
978
|
-
const sourceOutput = path.join(tempDir, 'format-test.json');
|
|
979
|
-
const sourceCompiler = new ResourceCompiler({ ...baseOptions, output: sourceOutput, format: 'source' });
|
|
980
|
-
const sourceResult = await sourceCompiler.compile();
|
|
981
|
-
expect(sourceResult).toSucceed();
|
|
982
|
-
|
|
983
|
-
// Test JS format
|
|
984
|
-
const jsOutput = path.join(tempDir, 'format-test.js');
|
|
985
|
-
const jsCompiler = new ResourceCompiler({ ...baseOptions, output: jsOutput, format: 'js' });
|
|
986
|
-
const jsResult = await jsCompiler.compile();
|
|
987
|
-
expect(jsResult).toSucceed();
|
|
988
|
-
|
|
989
|
-
// Test TS format
|
|
990
|
-
const tsOutput = path.join(tempDir, 'format-test.ts');
|
|
991
|
-
const tsCompiler = new ResourceCompiler({ ...baseOptions, output: tsOutput, format: 'ts' });
|
|
992
|
-
const tsResult = await tsCompiler.compile();
|
|
993
|
-
expect(tsResult).toSucceed();
|
|
994
|
-
|
|
995
|
-
// Parse the source output as baseline
|
|
996
|
-
const sourceContent = await fs.readFile(sourceOutput, 'utf-8');
|
|
997
|
-
const sourceData = JSON.parse(sourceContent);
|
|
998
|
-
|
|
999
|
-
// Verify JS format contains same data
|
|
1000
|
-
const jsContent = await fs.readFile(jsOutput, 'utf-8');
|
|
1001
|
-
expect(jsContent).toMatch(/^module\.exports = /);
|
|
1002
|
-
// Extract the data part from "module.exports = {...};"
|
|
1003
|
-
const jsDataStr = jsContent.replace(/^module\.exports = /, '').replace(/;$/, '');
|
|
1004
|
-
const jsData = JSON.parse(jsDataStr);
|
|
1005
|
-
expect(jsData).toEqual(sourceData);
|
|
1006
|
-
|
|
1007
|
-
// Verify TS format contains same data
|
|
1008
|
-
const tsContent = await fs.readFile(tsOutput, 'utf-8');
|
|
1009
|
-
expect(tsContent).toMatch(/^export const resources = /);
|
|
1010
|
-
// Extract the data part from "export const resources = {...} as const;"
|
|
1011
|
-
const tsDataStr = tsContent.replace(/^export const resources = /, '').replace(/ as const;$/, '');
|
|
1012
|
-
const tsData = JSON.parse(tsDataStr);
|
|
1013
|
-
expect(tsData).toEqual(sourceData);
|
|
1014
|
-
});
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
describe('validate method', () => {
|
|
1018
|
-
test('validates resources successfully', async () => {
|
|
1019
|
-
const options: ICompileOptions = {
|
|
1020
|
-
input: inputFile,
|
|
1021
|
-
output: outputFile,
|
|
1022
|
-
format: 'source',
|
|
1023
|
-
debug: false,
|
|
1024
|
-
verbose: false,
|
|
1025
|
-
quiet: false,
|
|
1026
|
-
validate: true,
|
|
1027
|
-
includeMetadata: false
|
|
1028
|
-
};
|
|
1029
|
-
|
|
1030
|
-
const compiler = new ResourceCompiler(options);
|
|
1031
|
-
const result = await compiler.validate();
|
|
1032
|
-
|
|
1033
|
-
expect(result).toSucceed();
|
|
1034
|
-
});
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
describe('getInfo method', () => {
|
|
1038
|
-
test('returns resource information', async () => {
|
|
1039
|
-
const options: ICompileOptions = {
|
|
1040
|
-
input: inputFile,
|
|
1041
|
-
output: outputFile,
|
|
1042
|
-
format: 'source',
|
|
1043
|
-
debug: false,
|
|
1044
|
-
verbose: false,
|
|
1045
|
-
quiet: false,
|
|
1046
|
-
validate: true,
|
|
1047
|
-
includeMetadata: true
|
|
1048
|
-
};
|
|
1049
|
-
|
|
1050
|
-
const compiler = new ResourceCompiler(options);
|
|
1051
|
-
const result = await compiler.getInfo();
|
|
1052
|
-
|
|
1053
|
-
expect(result).toSucceedAndSatisfy((info) => {
|
|
1054
|
-
expect(info.totalResources).toBeGreaterThan(0);
|
|
1055
|
-
expect(info.totalCandidates).toBeGreaterThan(0);
|
|
1056
|
-
expect(info.resourceTypes).toContain('json');
|
|
1057
|
-
expect(info.qualifiers).toContain('language');
|
|
1058
|
-
});
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
test('returns filtered resource information', async () => {
|
|
1062
|
-
const options: ICompileOptions = {
|
|
1063
|
-
input: inputFile,
|
|
1064
|
-
output: outputFile,
|
|
1065
|
-
contextFilter: 'language=en',
|
|
1066
|
-
format: 'source',
|
|
1067
|
-
debug: false,
|
|
1068
|
-
verbose: false,
|
|
1069
|
-
quiet: false,
|
|
1070
|
-
validate: true,
|
|
1071
|
-
includeMetadata: true
|
|
1072
|
-
};
|
|
1073
|
-
|
|
1074
|
-
const compiler = new ResourceCompiler(options);
|
|
1075
|
-
const result = await compiler.getInfo();
|
|
1076
|
-
|
|
1077
|
-
expect(result).toSucceedAndSatisfy((info) => {
|
|
1078
|
-
expect(info.totalResources).toBeGreaterThan(0);
|
|
1079
|
-
expect(info.filteredResources).toBeGreaterThan(0);
|
|
1080
|
-
expect(info.context).toEqual({ language: 'en' });
|
|
1081
|
-
});
|
|
1082
|
-
});
|
|
1083
|
-
});
|
|
1084
|
-
});
|