slicejs-cli 3.3.0 → 3.4.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.
Files changed (46) hide show
  1. package/AGENTS.md +247 -0
  2. package/LICENSE +21 -21
  3. package/client.js +663 -626
  4. package/commands/Print.js +163 -167
  5. package/commands/Validations.js +92 -103
  6. package/commands/build/build.js +40 -40
  7. package/commands/buildProduction/buildProduction.js +576 -579
  8. package/commands/bundle/bundle.js +234 -235
  9. package/commands/createComponent/VisualComponentTemplate.js +55 -55
  10. package/commands/createComponent/createComponent.js +124 -126
  11. package/commands/deleteComponent/deleteComponent.js +77 -77
  12. package/commands/doctor/doctor.js +366 -369
  13. package/commands/getComponent/getComponent.js +684 -747
  14. package/commands/init/init.js +269 -261
  15. package/commands/listComponents/listComponents.js +172 -175
  16. package/commands/startServer/startServer.js +261 -264
  17. package/commands/startServer/watchServer.js +79 -79
  18. package/commands/types/types.js +69 -27
  19. package/commands/utils/LocalCliDelegation.js +53 -53
  20. package/commands/utils/PathHelper.js +75 -68
  21. package/commands/utils/VersionChecker.js +167 -167
  22. package/commands/utils/bundling/BundleGenerator.js +2292 -2292
  23. package/commands/utils/bundling/DependencyAnalyzer.js +925 -933
  24. package/commands/utils/loadConfig.js +31 -0
  25. package/commands/utils/updateManager.js +452 -453
  26. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +105 -105
  27. package/package.json +58 -46
  28. package/post.js +66 -65
  29. package/tests/bundle-generator.test.js +691 -708
  30. package/tests/bundle-v2-register-output.test.js +470 -470
  31. package/tests/client-launcher-contract.test.js +211 -211
  32. package/tests/client-update-flow-contract.test.js +272 -272
  33. package/tests/component-registry-parse.test.js +34 -0
  34. package/tests/dependency-analyzer.test.js +24 -24
  35. package/tests/fixtures/components.js +8 -0
  36. package/tests/fixtures/sliceConfig.json +74 -0
  37. package/tests/getcomponent.test.js +407 -0
  38. package/tests/helpers/setup.js +97 -0
  39. package/tests/init-command-contract.test.js +46 -0
  40. package/tests/local-cli-delegation.test.js +81 -79
  41. package/tests/path-helper.test.js +206 -0
  42. package/tests/types-breakage.test.js +491 -0
  43. package/tests/types-generator-errors.test.js +361 -0
  44. package/tests/types-generator.test.js +172 -184
  45. package/tests/update-manager-notifications.test.js +88 -88
  46. package/.github/workflows/docs-render-cicd.yml +0 -65
@@ -0,0 +1,491 @@
1
+ import { test, describe, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { createTestProject, cleanupTestProject } from './helpers/setup.js';
6
+ import Print from '../commands/Print.js';
7
+
8
+ import {
9
+ generateTypesFile,
10
+ generateDeclarationContent,
11
+ extractStaticPropsFromSource,
12
+ parseComponentsRegistry,
13
+ ensureEditorConfigForTypes,
14
+ ensureNoCheckInPublicVendorFiles
15
+ } from '../commands/types/types.js';
16
+
17
+ describe('parseComponentsRegistry — breakage', () => {
18
+ test('nested braces in values parse correctly', () => {
19
+ const content = 'const components = {"Button": {"nested": true}};\nexport default components;\n';
20
+ const result = parseComponentsRegistry(content, '/test/components.js');
21
+ assert.deepEqual(result.Button, { nested: true });
22
+ });
23
+
24
+ test('duplicate keys keep last value (no error)', () => {
25
+ const content = 'const components = {"Button": "Visual", "Button": "Service"};\nexport default components;\n';
26
+ const result = parseComponentsRegistry(content, '/test/components.js');
27
+ assert.equal(result.Button, 'Service');
28
+ });
29
+
30
+ test('trailing comma in JSON throws', () => {
31
+ const content = 'const components = {"a": "Visual",};\nexport default components;\n';
32
+ assert.throws(
33
+ () => parseComponentsRegistry(content, '/test/components.js'),
34
+ /Failed to parse components registry/
35
+ );
36
+ });
37
+
38
+ test('no semicolon after object throws invalid format', () => {
39
+ const content = 'const components = {"a": "Visual"}';
40
+ assert.throws(
41
+ () => parseComponentsRegistry(content, '/test/components.js'),
42
+ /Invalid format in/
43
+ );
44
+ });
45
+
46
+ test('literal } inside a string value is valid JSON', () => {
47
+ const content = 'const components = {"a": "foo}"};\nexport default components;\n';
48
+ const result = parseComponentsRegistry(content, '/test/components.js');
49
+ assert.equal(result.a, 'foo}');
50
+ });
51
+
52
+ test('multiple const components declarations — only first is captured', () => {
53
+ const content = 'const components = {"a": "Visual"};\nconst components = {"b": "Service"};';
54
+ const result = parseComponentsRegistry(content, '/test/components.js');
55
+ assert.deepEqual(result, { a: 'Visual' });
56
+ });
57
+
58
+ test('prototype pollution keys are parsed as normal keys', () => {
59
+ const content = 'const components = {"__proto__": {"polluted": true}};\nexport default components;\n';
60
+ const result = parseComponentsRegistry(content, '/test/components.js');
61
+ assert.deepEqual(result.__proto__, { polluted: true });
62
+ });
63
+ });
64
+
65
+ describe('extractStaticPropsFromSource — breakage', () => {
66
+ test('computed property keys are silently dropped', () => {
67
+ const source = `
68
+ const KEY = 'label';
69
+ export default class Button extends HTMLElement {
70
+ static props = {
71
+ [KEY]: { type: 'string' },
72
+ normal: { type: 'number' }
73
+ };
74
+ }
75
+ `;
76
+ const result = extractStaticPropsFromSource(source, '/test/Button.js');
77
+ assert.equal(result.normal.type, 'number');
78
+ assert.equal(result.label, undefined);
79
+ });
80
+
81
+ test('method shorthand in props is skipped', () => {
82
+ const source = `
83
+ export default class Button extends HTMLElement {
84
+ static props = {
85
+ onClick() { console.log('click'); },
86
+ normal: { type: 'string' }
87
+ };
88
+ }
89
+ `;
90
+ const result = extractStaticPropsFromSource(source, '/test/Button.js');
91
+ assert.equal(result.onClick, undefined);
92
+ assert.equal(result.normal.type, 'string');
93
+ });
94
+
95
+ test('spread expressions lose the spread content', () => {
96
+ const source = `
97
+ const BASE = { value: { type: 'string' } };
98
+ export default class Button extends HTMLElement {
99
+ static props = {
100
+ ...BASE,
101
+ extra: { type: 'number' }
102
+ };
103
+ }
104
+ `;
105
+ const result = extractStaticPropsFromSource(source, '/test/Button.js');
106
+ assert.equal(result.value, undefined);
107
+ assert.equal(result.extra.type, 'number');
108
+ });
109
+
110
+ test('shorthand property is silently dropped', () => {
111
+ const source = `
112
+ const value = { type: 'string' };
113
+ export default class Button extends HTMLElement {
114
+ static props = { value };
115
+ }
116
+ `;
117
+ const result = extractStaticPropsFromSource(source, '/test/Button.js');
118
+ assert.equal(result, null);
119
+ });
120
+
121
+ test('null literal as prop value becomes type any', () => {
122
+ const source = `
123
+ export default class Button extends HTMLElement {
124
+ static props = { value: null };
125
+ }
126
+ `;
127
+ const result = extractStaticPropsFromSource(source, '/test/Button.js');
128
+ assert.deepEqual(result.value, { type: 'any', required: false });
129
+ });
130
+
131
+ test('getter in static props is skipped', () => {
132
+ const source = `
133
+ export default class Button extends HTMLElement {
134
+ static props = {
135
+ get dynamicProp() { return { type: 'string' }; },
136
+ normal: { type: 'number' }
137
+ };
138
+ }
139
+ `;
140
+ const result = extractStaticPropsFromSource(source, '/test/Button.js');
141
+ assert.equal(result.dynamicProp, undefined);
142
+ assert.equal(result.normal.type, 'number');
143
+ });
144
+
145
+ test('reserved JS keywords as prop names parse ok', () => {
146
+ const source = `
147
+ export default class Button extends HTMLElement {
148
+ static props = {
149
+ class: { type: 'string' },
150
+ delete: { type: 'boolean' },
151
+ return: { type: 'number' }
152
+ };
153
+ }
154
+ `;
155
+ const result = extractStaticPropsFromSource(source, '/test/Button.js');
156
+ assert.notEqual(result, null);
157
+ assert.equal(result.class.type, 'string');
158
+ assert.equal(result.delete.type, 'boolean');
159
+ assert.equal(result.return.type, 'number');
160
+ });
161
+
162
+ test('async class with static props works', () => {
163
+ const source = `
164
+ export default class DataLoader extends HTMLElement {
165
+ static props = { url: { type: 'string', required: true } };
166
+ async loadData() {}
167
+ }
168
+ `;
169
+ const result = extractStaticPropsFromSource(source, '/test/DataLoader.js');
170
+ assert.deepEqual(result, { url: { type: 'string', required: true } });
171
+ });
172
+ });
173
+
174
+ describe('generateDeclarationContent — breakage', () => {
175
+ test('reserved TS keyword as component name generates invalid TS', () => {
176
+ const content = generateDeclarationContent({
177
+ class: { value: { type: 'string', required: false } },
178
+ delete: { flag: { type: 'boolean', required: false } }
179
+ });
180
+ assert.match(content, /export interface classProps/);
181
+ assert.match(content, /export interface deleteProps/);
182
+ });
183
+
184
+ test('digit-starting component name generates invalid TS', () => {
185
+ const content = generateDeclarationContent({
186
+ '123Comp': { val: { type: 'string', required: false } }
187
+ });
188
+ assert.match(content, /export interface 123CompProps/);
189
+ });
190
+
191
+ test('hyphenated component name generates invalid TS', () => {
192
+ const content = generateDeclarationContent({
193
+ 'my-component': { val: { type: 'string', required: false } }
194
+ });
195
+ assert.match(content, /export interface my-componentProps/);
196
+ });
197
+
198
+ test('__dynamicPropsFallback as real prop is hidden from declaration', () => {
199
+ const content = generateDeclarationContent({
200
+ MyComp: { __dynamicPropsFallback: { type: 'string', required: false } }
201
+ });
202
+ assert.doesNotMatch(content, /__dynamicPropsFallback/);
203
+ assert.match(content, /\[key: string\]: unknown;/);
204
+ });
205
+
206
+ test('empty allowedValues produces string type', () => {
207
+ const content = generateDeclarationContent({
208
+ Input: { val: { type: 'string', required: false, allowedValues: [] } }
209
+ });
210
+ assert.match(content, /val\?: string/);
211
+ });
212
+ });
213
+
214
+ describe('generateTypesFile — file corruption breakage', () => {
215
+ test('components.js with BOM is handled', async () => {
216
+ const tmpRoot = await createTestProject({ visualComponents: ['Button'] });
217
+ try {
218
+ const componentsPath = path.join(tmpRoot, 'src', 'Components', 'components.js');
219
+ fs.writeFileSync(componentsPath, '\uFEFFconst components = {"Button": "Visual"};\nexport default components;\n', 'utf8');
220
+ const outputFile = path.join(tmpRoot, 'src', 'slice-build.generated.d.ts');
221
+ const result = await generateTypesFile({ projectRoot: tmpRoot, outputPath: outputFile });
222
+ assert.equal(result.componentsProcessed, 1);
223
+ } finally {
224
+ await cleanupTestProject(tmpRoot);
225
+ }
226
+ });
227
+
228
+ test('binary content in components.js throws parse error', async () => {
229
+ const tmpRoot = await createTestProject({ visualComponents: ['Button'] });
230
+ try {
231
+ const componentsPath = path.join(tmpRoot, 'src', 'Components', 'components.js');
232
+ fs.writeFileSync(componentsPath, Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]));
233
+ const outputFile = path.join(tmpRoot, 'src', 'slice-build.generated.d.ts');
234
+ await assert.rejects(
235
+ () => generateTypesFile({ projectRoot: tmpRoot, outputPath: outputFile }),
236
+ /Invalid format in/
237
+ );
238
+ } finally {
239
+ await cleanupTestProject(tmpRoot);
240
+ }
241
+ });
242
+
243
+ test('empty components.js file throws invalid format', async () => {
244
+ const tmpRoot = await createTestProject({ visualComponents: ['Button'] });
245
+ try {
246
+ const componentsPath = path.join(tmpRoot, 'src', 'Components', 'components.js');
247
+ fs.writeFileSync(componentsPath, '', 'utf8');
248
+ const outputFile = path.join(tmpRoot, 'src', 'slice-build.generated.d.ts');
249
+ await assert.rejects(
250
+ () => generateTypesFile({ projectRoot: tmpRoot, outputPath: outputFile }),
251
+ /Invalid format in/
252
+ );
253
+ } finally {
254
+ await cleanupTestProject(tmpRoot);
255
+ }
256
+ });
257
+
258
+ test('component JS file with only whitespace', async () => {
259
+ const tmpRoot = await createTestProject();
260
+ const srcDir = path.join(tmpRoot, 'src');
261
+ try {
262
+ const visualDir = path.join(srcDir, 'Components', 'Visual', 'Blank');
263
+ fs.mkdirSync(visualDir, { recursive: true });
264
+ fs.writeFileSync(path.join(visualDir, 'Blank.js'), ' \n \n', 'utf8');
265
+ fs.writeFileSync(
266
+ path.join(srcDir, 'Components', 'components.js'),
267
+ 'const components = {"Blank": "Visual"};\nexport default components;\n',
268
+ 'utf8'
269
+ );
270
+ const outputFile = path.join(srcDir, 'slice-build.generated.d.ts');
271
+ const result = await generateTypesFile({ projectRoot: tmpRoot, outputPath: outputFile });
272
+ assert.equal(result.componentsProcessed, 1);
273
+ const content = fs.readFileSync(result.outputPath, 'utf8');
274
+ assert.match(content, /\[key: string\]: unknown;/);
275
+ } finally {
276
+ await cleanupTestProject(tmpRoot);
277
+ }
278
+ });
279
+
280
+ test('running twice produces identical output', async () => {
281
+ const tmpRoot = await createTestProject({ visualComponents: ['Button'] });
282
+ const srcDir = path.join(tmpRoot, 'src');
283
+ try {
284
+ fs.writeFileSync(
285
+ path.join(srcDir, 'Components', 'Visual', 'Button', 'Button.js'),
286
+ 'export default class Button extends HTMLElement { static props = { label: { type: "string" } }; }',
287
+ 'utf8'
288
+ );
289
+ const outputFile = path.join(srcDir, 'slice-build.generated.d.ts');
290
+ const first = await generateTypesFile({ projectRoot: tmpRoot, outputPath: outputFile });
291
+ const firstContent = fs.readFileSync(first.outputPath, 'utf8');
292
+ const second = await generateTypesFile({ projectRoot: tmpRoot, outputPath: outputFile });
293
+ const secondContent = fs.readFileSync(second.outputPath, 'utf8');
294
+ assert.equal(first.componentsProcessed, 1);
295
+ assert.equal(second.componentsProcessed, 1);
296
+ assert.equal(firstContent, secondContent);
297
+ } finally {
298
+ await cleanupTestProject(tmpRoot);
299
+ }
300
+ });
301
+ });
302
+
303
+ describe('ensureNoCheckInPublicVendorFiles — breakage', () => {
304
+ test('returns zero counts when sliceConfig.json is missing', async () => {
305
+ const tmpRoot = await createTestProject();
306
+ try {
307
+ fs.unlinkSync(path.join(tmpRoot, 'src', 'sliceConfig.json'));
308
+ const result = await ensureNoCheckInPublicVendorFiles(tmpRoot);
309
+ assert.deepEqual(result, { updatedFiles: 0, scannedFiles: 0 });
310
+ } finally {
311
+ await cleanupTestProject(tmpRoot);
312
+ }
313
+ });
314
+
315
+ test('returns zero counts when publicFolders is not an array', async () => {
316
+ const tmpRoot = await createTestProject();
317
+ const srcDir = path.join(tmpRoot, 'src');
318
+ try {
319
+ const configPath = path.join(srcDir, 'sliceConfig.json');
320
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
321
+ config.publicFolders = 'not-an-array';
322
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
323
+ const result = await ensureNoCheckInPublicVendorFiles(tmpRoot);
324
+ assert.deepEqual(result, { updatedFiles: 0, scannedFiles: 0 });
325
+ } finally {
326
+ await cleanupTestProject(tmpRoot);
327
+ }
328
+ });
329
+ });
330
+
331
+ describe('Print log counting during generateTypesFile errors', () => {
332
+ test('missing components.js prints no extra logs beyond the thrown error', async () => {
333
+ const tmpRoot = await createTestProject();
334
+ try {
335
+ const componentsPath = path.join(tmpRoot, 'src', 'Components', 'components.js');
336
+ fs.unlinkSync(componentsPath);
337
+ const outputFile = path.join(tmpRoot, 'src', 'slice-build.generated.d.ts');
338
+ let warningCount = 0;
339
+ const origWarning = Print.warning;
340
+ Print.warning = () => { warningCount++; };
341
+ let errorCount = 0;
342
+ const origError = Print.error;
343
+ Print.error = () => { errorCount++; };
344
+ try {
345
+ await assert.rejects(
346
+ () => generateTypesFile({ projectRoot: tmpRoot, outputPath: outputFile }),
347
+ /Cannot read components registry/
348
+ );
349
+ } finally {
350
+ Print.warning = origWarning;
351
+ Print.error = origError;
352
+ }
353
+ assert.equal(warningCount, 0);
354
+ assert.equal(errorCount, 0);
355
+ } finally {
356
+ await cleanupTestProject(tmpRoot);
357
+ }
358
+ });
359
+
360
+ test('component with Babel parse error triggers Print.warning exactly once', async () => {
361
+ const tmpRoot = await createTestProject();
362
+ const srcDir = path.join(tmpRoot, 'src');
363
+ try {
364
+ const visualDir = path.join(srcDir, 'Components', 'Visual', 'Broken');
365
+ fs.mkdirSync(visualDir, { recursive: true });
366
+ fs.writeFileSync(path.join(visualDir, 'Broken.js'), 'export default class Broken extends HTMLElement {', 'utf8');
367
+ const goodDir = path.join(srcDir, 'Components', 'Visual', 'Good');
368
+ fs.mkdirSync(goodDir, { recursive: true });
369
+ fs.writeFileSync(
370
+ path.join(goodDir, 'Good.js'),
371
+ 'export default class Good extends HTMLElement { static props = { x: { type: "string" } }; }',
372
+ 'utf8'
373
+ );
374
+ fs.writeFileSync(
375
+ path.join(srcDir, 'Components', 'components.js'),
376
+ 'const components = {"Good": "Visual", "Broken": "Visual"};\nexport default components;\n',
377
+ 'utf8'
378
+ );
379
+ const outputFile = path.join(srcDir, 'slice-build.generated.d.ts');
380
+ let warningCount = 0;
381
+ const origWarning = Print.warning;
382
+ Print.warning = () => { warningCount++; };
383
+ try {
384
+ await generateTypesFile({ projectRoot: tmpRoot, outputPath: outputFile });
385
+ } finally {
386
+ Print.warning = origWarning;
387
+ }
388
+ assert.equal(warningCount, 1);
389
+ } finally {
390
+ await cleanupTestProject(tmpRoot);
391
+ }
392
+ });
393
+
394
+ test('missing component file triggers Print.info once for skipped count', async () => {
395
+ const tmpRoot = await createTestProject();
396
+ const srcDir = path.join(tmpRoot, 'src');
397
+ try {
398
+ fs.writeFileSync(
399
+ path.join(srcDir, 'Components', 'components.js'),
400
+ 'const components = {"Existing": "Visual", "Missing": "Visual"};\nexport default components;\n',
401
+ 'utf8'
402
+ );
403
+ const existingDir = path.join(srcDir, 'Components', 'Visual', 'Existing');
404
+ fs.mkdirSync(existingDir, { recursive: true });
405
+ fs.writeFileSync(
406
+ path.join(existingDir, 'Existing.js'),
407
+ 'export default class Existing extends HTMLElement { static props = { x: { type: "string" } }; }',
408
+ 'utf8'
409
+ );
410
+ const outputFile = path.join(srcDir, 'slice-build.generated.d.ts');
411
+ let infoCount = 0;
412
+ const origInfo = Print.info;
413
+ Print.info = () => { infoCount++; };
414
+ try {
415
+ await generateTypesFile({ projectRoot: tmpRoot, outputPath: outputFile });
416
+ } finally {
417
+ Print.info = origInfo;
418
+ }
419
+ assert.equal(infoCount, 1);
420
+ } finally {
421
+ await cleanupTestProject(tmpRoot);
422
+ }
423
+ });
424
+ });
425
+
426
+ describe('ensureEditorConfigForTypes — config breakage', () => {
427
+ test('jsconfig with compilerOptions: null fills defaults', async () => {
428
+ const tmpRoot = await createTestProject();
429
+ const outputFile = path.join(tmpRoot, 'src', 'slice-build.generated.d.ts');
430
+ const jsconfigPath = path.join(tmpRoot, 'jsconfig.json');
431
+ try {
432
+ fs.writeFileSync(outputFile, 'export {};\n', 'utf8');
433
+ fs.writeFileSync(jsconfigPath, JSON.stringify({ compilerOptions: null }), 'utf8');
434
+ const result = await ensureEditorConfigForTypes({ projectRoot: tmpRoot, outputPath: outputFile });
435
+ assert.equal(result.mode, 'updated_jsconfig');
436
+ } finally {
437
+ await cleanupTestProject(tmpRoot);
438
+ }
439
+ });
440
+
441
+ test('jsconfig with non-array include gets rebuilt', async () => {
442
+ const tmpRoot = await createTestProject();
443
+ const outputFile = path.join(tmpRoot, 'src', 'slice-build.generated.d.ts');
444
+ const jsconfigPath = path.join(tmpRoot, 'jsconfig.json');
445
+ try {
446
+ fs.writeFileSync(outputFile, 'export {};\n', 'utf8');
447
+ fs.writeFileSync(
448
+ jsconfigPath,
449
+ JSON.stringify({ include: 'src/**/*.js', compilerOptions: {} }),
450
+ 'utf8'
451
+ );
452
+ const result = await ensureEditorConfigForTypes({ projectRoot: tmpRoot, outputPath: outputFile });
453
+ assert.match(result.mode, /created_jsconfig|updated_jsconfig|reset_jsconfig/);
454
+ const parsed = JSON.parse(fs.readFileSync(jsconfigPath, 'utf8'));
455
+ assert.ok(Array.isArray(parsed.include));
456
+ } finally {
457
+ await cleanupTestProject(tmpRoot);
458
+ }
459
+ });
460
+
461
+ test('outputPath outside project root still works', async () => {
462
+ const tmpRoot = await createTestProject();
463
+ const outsideFile = path.join(tmpRoot, '..', 'outside.d.ts');
464
+ try {
465
+ fs.writeFileSync(outsideFile, 'export {};\n', 'utf8');
466
+ const result = await ensureEditorConfigForTypes({
467
+ projectRoot: tmpRoot,
468
+ outputPath: outsideFile
469
+ });
470
+ assert.match(result.mode, /created_jsconfig|updated_jsconfig/);
471
+ } finally {
472
+ fs.unlinkSync(outsideFile);
473
+ await cleanupTestProject(tmpRoot);
474
+ }
475
+ });
476
+
477
+ test('both tsconfig.json and jsconfig.json exist — tsconfig takes priority', async () => {
478
+ const tmpRoot = await createTestProject();
479
+ const outputFile = path.join(tmpRoot, 'src', 'slice-build.generated.d.ts');
480
+ const jsconfigPath = path.join(tmpRoot, 'jsconfig.json');
481
+ try {
482
+ fs.writeFileSync(outputFile, 'export {};\n', 'utf8');
483
+ fs.writeFileSync(path.join(tmpRoot, 'tsconfig.json'), '{}', 'utf8');
484
+ fs.writeFileSync(jsconfigPath, JSON.stringify({ include: [], compilerOptions: {} }), 'utf8');
485
+ const result = await ensureEditorConfigForTypes({ projectRoot: tmpRoot, outputPath: outputFile });
486
+ assert.equal(result.mode, 'tsconfig_exists');
487
+ } finally {
488
+ await cleanupTestProject(tmpRoot);
489
+ }
490
+ });
491
+ });