@esportsplus/typescript 0.29.0 → 0.29.5

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,855 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import ts from 'typescript';
3
+
4
+ import type { ImportIntent, Plugin, ReplacementIntent, SharedContext, TransformContext } from '~/compiler/types';
5
+
6
+ import coordinator from '~/compiler/coordinator';
7
+
8
+
9
+ vi.mock('~/compiler/language-service', () => ({
10
+ default: {
11
+ invalidate: vi.fn(),
12
+ update: vi.fn((_root: string, fileName: string, content: string) => {
13
+ let file = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
14
+
15
+ return {
16
+ getSourceFile: () => file,
17
+ getTypeChecker: () => ({} as ts.TypeChecker)
18
+ } as unknown as ts.Program;
19
+ })
20
+ }
21
+ }));
22
+
23
+
24
+ function parse(code: string, fileName = 'test.ts'): ts.SourceFile {
25
+ return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
26
+ }
27
+
28
+ function makeProgram(file: ts.SourceFile): ts.Program {
29
+ return {
30
+ getSourceFile: () => file,
31
+ getTypeChecker: () => ({} as ts.TypeChecker)
32
+ } as unknown as ts.Program;
33
+ }
34
+
35
+ function makePlugin(transformFn: (ctx: TransformContext) => ReturnType<Plugin['transform']>): Plugin {
36
+ return { transform: transformFn };
37
+ }
38
+
39
+
40
+ describe('coordinator.transform', () => {
41
+ it('returns unchanged when no plugins', () => {
42
+ let code = 'let x = 1;',
43
+ file = parse(code),
44
+ program = makeProgram(file),
45
+ result = coordinator.transform([], code, file, program, '/root', new Map());
46
+
47
+ expect(result.changed).toBe(false);
48
+ expect(result.code).toBe(code);
49
+ });
50
+
51
+ it('applies replacement intents', () => {
52
+ let code = 'let x = OLD;',
53
+ file = parse(code),
54
+ program = makeProgram(file),
55
+ plugin = makePlugin((ctx) => {
56
+ let node: ts.Node | undefined;
57
+
58
+ ts.forEachChild(ctx.sourceFile, function visit(n) {
59
+ if (ts.isIdentifier(n) && n.text === 'OLD') {
60
+ node = n;
61
+ }
62
+
63
+ ts.forEachChild(n, visit);
64
+ });
65
+
66
+ if (!node) {
67
+ return {};
68
+ }
69
+
70
+ let intents: ReplacementIntent[] = [{
71
+ generate: () => 'NEW',
72
+ node
73
+ }];
74
+
75
+ return { replacements: intents };
76
+ }),
77
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
78
+
79
+ expect(result.changed).toBe(true);
80
+ expect(result.code).toContain('NEW');
81
+ expect(result.code).not.toContain('OLD');
82
+ });
83
+
84
+ it('applies prepend after imports', () => {
85
+ let code = "import { a } from 'pkg';\nlet x = 1;",
86
+ file = parse(code),
87
+ program = makeProgram(file),
88
+ plugin = makePlugin(() => ({
89
+ prepend: ['const GENERATED = true;']
90
+ })),
91
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
92
+
93
+ expect(result.changed).toBe(true);
94
+ expect(result.code).toContain('const GENERATED = true;');
95
+
96
+ let importIdx = result.code.indexOf("import { a } from 'pkg';"),
97
+ generatedIdx = result.code.indexOf('const GENERATED = true;'),
98
+ letIdx = result.code.indexOf('let x = 1;');
99
+
100
+ expect(importIdx).toBeLessThan(generatedIdx);
101
+ expect(generatedIdx).toBeLessThan(letIdx);
102
+ });
103
+
104
+ it('applies import intents', () => {
105
+ let code = 'let x = 1;',
106
+ file = parse(code),
107
+ program = makeProgram(file),
108
+ intents: ImportIntent[] = [{
109
+ add: ['foo'],
110
+ package: 'my-pkg'
111
+ }],
112
+ plugin = makePlugin(() => ({ imports: intents })),
113
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
114
+
115
+ expect(result.changed).toBe(true);
116
+ expect(result.code).toContain("import { foo } from 'my-pkg';");
117
+ });
118
+
119
+ it('skips plugin when patterns do not match', () => {
120
+ let code = 'let x = 1;',
121
+ file = parse(code),
122
+ program = makeProgram(file),
123
+ plugin: Plugin = {
124
+ patterns: ['MAGIC_TOKEN'],
125
+ transform: () => ({ prepend: ['SHOULD_NOT_APPEAR'] })
126
+ },
127
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
128
+
129
+ expect(result.changed).toBe(false);
130
+ expect(result.code).not.toContain('SHOULD_NOT_APPEAR');
131
+ });
132
+
133
+ it('runs plugin when patterns match', () => {
134
+ let code = 'let MAGIC_TOKEN = 1;',
135
+ file = parse(code),
136
+ program = makeProgram(file),
137
+ plugin: Plugin = {
138
+ patterns: ['MAGIC_TOKEN'],
139
+ transform: () => ({ prepend: ['const FOUND = true;'] })
140
+ },
141
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
142
+
143
+ expect(result.changed).toBe(true);
144
+ expect(result.code).toContain('const FOUND = true;');
145
+ });
146
+
147
+ it('re-parses AST between replacements and prepend (F-001 fix)', () => {
148
+ let code = "import { a } from 'pkg';\nlet OLD = 1;\nlet y = 2;",
149
+ file = parse(code),
150
+ program = makeProgram(file),
151
+ plugin = makePlugin((ctx) => {
152
+ let node: ts.Node | undefined;
153
+
154
+ ts.forEachChild(ctx.sourceFile, function visit(n) {
155
+ if (ts.isIdentifier(n) && n.text === 'OLD') {
156
+ node = n;
157
+ }
158
+
159
+ ts.forEachChild(n, visit);
160
+ });
161
+
162
+ if (!node) {
163
+ return {};
164
+ }
165
+
166
+ return {
167
+ prepend: ['const PREPENDED = true;'],
168
+ replacements: [{
169
+ generate: () => 'REPLACED_LONGER_NAME',
170
+ node
171
+ }]
172
+ };
173
+ }),
174
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
175
+
176
+ expect(result.changed).toBe(true);
177
+ expect(result.code).toContain('REPLACED_LONGER_NAME');
178
+ expect(result.code).toContain('const PREPENDED = true;');
179
+
180
+ // Prepend should be after imports, not corrupted by replacement
181
+ let importEnd = result.code.indexOf("';") + 2,
182
+ prependIdx = result.code.indexOf('const PREPENDED = true;');
183
+
184
+ expect(prependIdx).toBeGreaterThan(importEnd);
185
+ });
186
+
187
+ it('chains multiple plugins', () => {
188
+ let code = 'let x = 1;',
189
+ file = parse(code),
190
+ program = makeProgram(file),
191
+ plugin1 = makePlugin(() => ({ prepend: ['const A = 1;'] })),
192
+ plugin2 = makePlugin(() => ({ prepend: ['const B = 2;'] })),
193
+ result = coordinator.transform([plugin1, plugin2], code, file, program, '/root', new Map());
194
+
195
+ expect(result.changed).toBe(true);
196
+ expect(result.code).toContain('const A = 1;');
197
+ expect(result.code).toContain('const B = 2;');
198
+ });
199
+
200
+ it('shares context between plugins', () => {
201
+ let code = 'let x = 1;',
202
+ file = parse(code),
203
+ program = makeProgram(file),
204
+ shared: SharedContext = new Map(),
205
+ plugin1 = makePlugin((ctx) => {
206
+ ctx.shared.set('key', 'value');
207
+ return { prepend: ['const A = 1;'] };
208
+ }),
209
+ plugin2 = makePlugin((ctx) => {
210
+ let val = ctx.shared.get('key');
211
+
212
+ return { prepend: [`const B = '${val}';`] };
213
+ }),
214
+ result = coordinator.transform([plugin1, plugin2], code, file, program, '/root', shared);
215
+
216
+ expect(result.changed).toBe(true);
217
+ expect(result.code).toContain("const B = 'value';");
218
+ });
219
+
220
+ it('applies replacements + imports together with AST re-parse', () => {
221
+ let code = "let OLD = 1;",
222
+ file = parse(code),
223
+ program = makeProgram(file),
224
+ plugin = makePlugin((ctx) => {
225
+ let node: ts.Node | undefined;
226
+
227
+ ts.forEachChild(ctx.sourceFile, function visit(n) {
228
+ if (ts.isIdentifier(n) && n.text === 'OLD') {
229
+ node = n;
230
+ }
231
+
232
+ ts.forEachChild(n, visit);
233
+ });
234
+
235
+ if (!node) {
236
+ return {};
237
+ }
238
+
239
+ return {
240
+ imports: [{ add: ['helper'], package: 'utils' }],
241
+ replacements: [{
242
+ generate: () => 'REPLACED',
243
+ node
244
+ }]
245
+ };
246
+ }),
247
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
248
+
249
+ expect(result.changed).toBe(true);
250
+ expect(result.code).toContain('REPLACED');
251
+ expect(result.code).toContain("import { helper } from 'utils';");
252
+ });
253
+
254
+ // F-TEST-001: Coordinator integration tests with real-world patterns
255
+
256
+ it('plugin producing replacements + prepend + imports simultaneously', () => {
257
+ let code = "import { reactive } from 'my-pkg';\nlet x = reactive(1);",
258
+ file = parse(code),
259
+ program = makeProgram(file),
260
+ plugin = makePlugin((ctx) => {
261
+ let node: ts.Node | undefined;
262
+
263
+ ts.forEachChild(ctx.sourceFile, function visit(n) {
264
+ if (ts.isIdentifier(n) && n.text === 'reactive') {
265
+ node = n;
266
+ }
267
+
268
+ ts.forEachChild(n, visit);
269
+ });
270
+
271
+ if (!node) {
272
+ return {};
273
+ }
274
+
275
+ return {
276
+ imports: [{ namespace: 'NS', package: 'my-pkg', remove: ['reactive'] }],
277
+ prepend: ['class ReactiveState {}'],
278
+ replacements: [{
279
+ generate: () => 'NS.reactive',
280
+ node
281
+ }]
282
+ };
283
+ }),
284
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
285
+
286
+ expect(result.changed).toBe(true);
287
+ expect(result.code).toContain('NS.reactive');
288
+ expect(result.code).toContain('class ReactiveState {}');
289
+ expect(result.code).toContain("import * as NS from 'my-pkg';");
290
+ expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
291
+
292
+ let nsImportIdx = result.code.indexOf("import * as NS from 'my-pkg';"),
293
+ classIdx = result.code.indexOf('class ReactiveState {}'),
294
+ bodyIdx = result.code.indexOf('NS.reactive');
295
+
296
+ expect(nsImportIdx).toBeLessThan(classIdx);
297
+ expect(classIdx).toBeLessThan(bodyIdx);
298
+ });
299
+
300
+ it('plugin with generate() closures capturing scope variables', () => {
301
+ let code = 'let myVar = 1;',
302
+ file = parse(code),
303
+ program = makeProgram(file),
304
+ plugin = makePlugin((ctx) => {
305
+ let node: ts.Node | undefined,
306
+ varname = '';
307
+
308
+ ts.forEachChild(ctx.sourceFile, function visit(n) {
309
+ if (ts.isIdentifier(n) && n.text === 'myVar') {
310
+ node = n;
311
+ varname = n.text;
312
+ }
313
+
314
+ ts.forEachChild(n, visit);
315
+ });
316
+
317
+ if (!node) {
318
+ return {};
319
+ }
320
+
321
+ return {
322
+ replacements: [{
323
+ generate: () => `NS.write(${varname}, ${varname}.value)`,
324
+ node
325
+ }]
326
+ };
327
+ }),
328
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
329
+
330
+ expect(result.changed).toBe(true);
331
+ expect(result.code).toContain('NS.write(myVar, myVar.value)');
332
+ });
333
+
334
+ it('file with existing imports, plugin adds namespace + removes specifier', () => {
335
+ let code = "import { other, reactive } from 'my-pkg';\nlet x = 1;",
336
+ file = parse(code),
337
+ program = makeProgram(file),
338
+ plugin = makePlugin(() => ({
339
+ imports: [{ namespace: 'NS', package: 'my-pkg', remove: ['reactive'] }]
340
+ })),
341
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
342
+
343
+ expect(result.changed).toBe(true);
344
+ expect(result.code).toContain("import * as NS from 'my-pkg';");
345
+ expect(result.code).toContain('other');
346
+ expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
347
+ });
348
+
349
+ // F-TEST-003: Import manipulation integration tests
350
+
351
+ it('adds specifiers to existing import', () => {
352
+ let code = "import { a } from 'pkg';\nlet x = 1;",
353
+ file = parse(code),
354
+ program = makeProgram(file),
355
+ plugin = makePlugin(() => ({
356
+ imports: [{ add: ['b', 'c'], package: 'pkg' }]
357
+ })),
358
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
359
+
360
+ expect(result.changed).toBe(true);
361
+ expect(result.code).toContain('a');
362
+ expect(result.code).toContain('b');
363
+ expect(result.code).toContain('c');
364
+ expect(result.code).toMatch(/import\s*\{[^}]*a[^}]*b[^}]*c[^}]*\}\s*from\s*'pkg'/);
365
+ });
366
+
367
+ it('removes specifier from import keeping others', () => {
368
+ let code = "import { a, b, reactive } from 'my-pkg';\nlet x = 1;",
369
+ file = parse(code),
370
+ program = makeProgram(file),
371
+ plugin = makePlugin(() => ({
372
+ imports: [{ package: 'my-pkg', remove: ['reactive'] }]
373
+ })),
374
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
375
+
376
+ expect(result.changed).toBe(true);
377
+ expect(result.code).toContain('a');
378
+ expect(result.code).toContain('b');
379
+ expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
380
+ });
381
+
382
+ it('adds namespace import to file without package imports', () => {
383
+ let code = 'let x = 1;',
384
+ file = parse(code),
385
+ program = makeProgram(file),
386
+ plugin = makePlugin(() => ({
387
+ imports: [{ namespace: 'NS', package: 'my-pkg' }]
388
+ })),
389
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
390
+
391
+ expect(result.changed).toBe(true);
392
+ expect(result.code).toContain("import * as NS from 'my-pkg';");
393
+ });
394
+
395
+ it('merges duplicate import statements', () => {
396
+ let code = "import { a } from 'pkg';\nimport { b } from 'pkg';\nlet x = 1;",
397
+ file = parse(code),
398
+ program = makeProgram(file),
399
+ plugin = makePlugin(() => ({
400
+ imports: [{ add: ['c'], package: 'pkg' }]
401
+ })),
402
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
403
+
404
+ expect(result.changed).toBe(true);
405
+
406
+ let importMatches = result.code.match(/import\s*\{[^}]+\}\s*from\s*'pkg'/g);
407
+
408
+ expect(importMatches).toHaveLength(1);
409
+ expect(result.code).toContain('a');
410
+ expect(result.code).toContain('b');
411
+ expect(result.code).toContain('c');
412
+ });
413
+
414
+ it('namespace + remove specifier combined', () => {
415
+ let code = "import { reactive } from 'my-pkg';\nlet x = 1;",
416
+ file = parse(code),
417
+ program = makeProgram(file),
418
+ plugin = makePlugin(() => ({
419
+ imports: [{ namespace: 'NS', package: 'my-pkg', remove: ['reactive'] }]
420
+ })),
421
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
422
+
423
+ expect(result.changed).toBe(true);
424
+ expect(result.code).toContain("import * as NS from 'my-pkg';");
425
+ expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
426
+ });
427
+
428
+ it('adds import to file with different package imports', () => {
429
+ let code = "import { x } from 'other';\nlet a = 1;",
430
+ file = parse(code),
431
+ program = makeProgram(file),
432
+ plugin = makePlugin(() => ({
433
+ imports: [{ add: ['y'], package: 'new-pkg' }]
434
+ })),
435
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
436
+
437
+ expect(result.changed).toBe(true);
438
+ expect(result.code).toContain("import { x } from 'other';");
439
+ expect(result.code).toContain("import { y } from 'new-pkg';");
440
+ });
441
+
442
+ // F-TEST-004: replaceReverse edge cases
443
+
444
+ it('multiple non-overlapping replacements', () => {
445
+ let code = 'let OLD1 = 1; let OLD2 = 2;',
446
+ file = parse(code),
447
+ program = makeProgram(file),
448
+ plugin = makePlugin((ctx) => {
449
+ let nodes: ts.Node[] = [];
450
+
451
+ ts.forEachChild(ctx.sourceFile, function visit(n) {
452
+ if (ts.isIdentifier(n) && (n.text === 'OLD1' || n.text === 'OLD2')) {
453
+ nodes.push(n);
454
+ }
455
+
456
+ ts.forEachChild(n, visit);
457
+ });
458
+
459
+ return {
460
+ replacements: nodes.map(node => ({
461
+ generate: () => node.getText(ctx.sourceFile) === 'OLD1' ? 'NEW1' : 'NEW2',
462
+ node
463
+ }))
464
+ };
465
+ }),
466
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
467
+
468
+ expect(result.changed).toBe(true);
469
+ expect(result.code).toContain('NEW1');
470
+ expect(result.code).toContain('NEW2');
471
+ expect(result.code).not.toContain('OLD1');
472
+ expect(result.code).not.toContain('OLD2');
473
+ });
474
+
475
+ it('replacement with empty string deletes a node', () => {
476
+ let code = 'let DELETEME = 1;',
477
+ file = parse(code),
478
+ program = makeProgram(file),
479
+ plugin = makePlugin((ctx) => {
480
+ let node: ts.Node | undefined;
481
+
482
+ ts.forEachChild(ctx.sourceFile, function visit(n) {
483
+ if (ts.isIdentifier(n) && n.text === 'DELETEME') {
484
+ node = n;
485
+ }
486
+
487
+ ts.forEachChild(n, visit);
488
+ });
489
+
490
+ if (!node) {
491
+ return {};
492
+ }
493
+
494
+ return {
495
+ replacements: [{
496
+ generate: () => '',
497
+ node
498
+ }]
499
+ };
500
+ }),
501
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
502
+
503
+ expect(result.changed).toBe(true);
504
+ expect(result.code).not.toContain('DELETEME');
505
+ });
506
+
507
+ it('replacement at file start', () => {
508
+ let code = 'FIRST_TOKEN;',
509
+ file = parse(code),
510
+ program = makeProgram(file),
511
+ plugin = makePlugin((ctx) => {
512
+ let node: ts.Node | undefined;
513
+
514
+ ts.forEachChild(ctx.sourceFile, function visit(n) {
515
+ if (ts.isIdentifier(n) && n.text === 'FIRST_TOKEN') {
516
+ node = n;
517
+ }
518
+
519
+ ts.forEachChild(n, visit);
520
+ });
521
+
522
+ if (!node) {
523
+ return {};
524
+ }
525
+
526
+ return {
527
+ replacements: [{
528
+ generate: () => 'REPLACED_FIRST',
529
+ node
530
+ }]
531
+ };
532
+ }),
533
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
534
+
535
+ expect(result.changed).toBe(true);
536
+ expect(result.code).toContain('REPLACED_FIRST');
537
+ expect(result.code.indexOf('REPLACED_FIRST')).toBe(0);
538
+ });
539
+
540
+ // F-TEST-006: Multi-plugin pipeline
541
+
542
+ it('first plugin modifies code, second receives updated code', () => {
543
+ let code = 'let OLD = 1;',
544
+ file = parse(code),
545
+ program = makeProgram(file),
546
+ plugin1 = makePlugin((ctx) => {
547
+ let node: ts.Node | undefined;
548
+
549
+ ts.forEachChild(ctx.sourceFile, function visit(n) {
550
+ if (ts.isIdentifier(n) && n.text === 'OLD') {
551
+ node = n;
552
+ }
553
+
554
+ ts.forEachChild(n, visit);
555
+ });
556
+
557
+ if (!node) {
558
+ return {};
559
+ }
560
+
561
+ return {
562
+ replacements: [{
563
+ generate: () => 'TRANSFORMED',
564
+ node
565
+ }]
566
+ };
567
+ }),
568
+ plugin2 = makePlugin((ctx) => {
569
+ if (ctx.code.includes('TRANSFORMED')) {
570
+ return { prepend: ['const SEEN_BY_PLUGIN2 = true;'] };
571
+ }
572
+
573
+ return {};
574
+ }),
575
+ result = coordinator.transform([plugin1, plugin2], code, file, program, '/root', new Map());
576
+
577
+ expect(result.changed).toBe(true);
578
+ expect(result.code).toContain('TRANSFORMED');
579
+ expect(result.code).toContain('const SEEN_BY_PLUGIN2 = true;');
580
+ });
581
+
582
+ it('three plugins — first skipped, second and third run', () => {
583
+ let code = 'let x = 1;',
584
+ file = parse(code),
585
+ program = makeProgram(file),
586
+ plugin1: Plugin = {
587
+ patterns: ['MISSING'],
588
+ transform: () => ({ prepend: ['const SHOULD_NOT_APPEAR = true;'] })
589
+ },
590
+ plugin2 = makePlugin(() => ({ prepend: ['const A = 1;'] })),
591
+ plugin3 = makePlugin(() => ({ prepend: ['const B = 2;'] })),
592
+ result = coordinator.transform([plugin1, plugin2, plugin3], code, file, program, '/root', new Map());
593
+
594
+ expect(result.changed).toBe(true);
595
+ expect(result.code).not.toContain('SHOULD_NOT_APPEAR');
596
+ expect(result.code).toContain('const A = 1;');
597
+ expect(result.code).toContain('const B = 2;');
598
+ });
599
+
600
+ // F-TEST-009: generate() sourceFile correctness
601
+
602
+ it('generate() receives correct sourceFile after prior replacement', () => {
603
+ let code = "let TARGET = 'hello';",
604
+ file = parse(code),
605
+ program = makeProgram(file),
606
+ plugin = makePlugin((ctx) => {
607
+ let node: ts.Node | undefined;
608
+
609
+ ts.forEachChild(ctx.sourceFile, function visit(n) {
610
+ if (ts.isIdentifier(n) && n.text === 'TARGET') {
611
+ node = n;
612
+ }
613
+
614
+ ts.forEachChild(n, visit);
615
+ });
616
+
617
+ if (!node) {
618
+ return {};
619
+ }
620
+
621
+ return {
622
+ replacements: [{
623
+ generate: (sf) => `REPLACED_IN_${sf.fileName}`,
624
+ node
625
+ }]
626
+ };
627
+ }),
628
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
629
+
630
+ expect(result.changed).toBe(true);
631
+ expect(result.code).toContain('REPLACED_IN_test.ts');
632
+ });
633
+
634
+ // F-TEST-005: Pattern filtering edge cases
635
+
636
+ it('pattern in string literal still matches', () => {
637
+ let code = 'let x = "reactive(";',
638
+ file = parse(code),
639
+ program = makeProgram(file),
640
+ plugin: Plugin = {
641
+ patterns: ['reactive('],
642
+ transform: () => ({ prepend: ['const MATCHED = true;'] })
643
+ },
644
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
645
+
646
+ expect(result.changed).toBe(true);
647
+ expect(result.code).toContain('const MATCHED = true;');
648
+ });
649
+
650
+ it('multiple patterns, only one matches', () => {
651
+ let code = 'let PRESENT = 1;',
652
+ file = parse(code),
653
+ program = makeProgram(file),
654
+ plugin: Plugin = {
655
+ patterns: ['MISSING', 'PRESENT'],
656
+ transform: () => ({ prepend: ['const FOUND = true;'] })
657
+ },
658
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
659
+
660
+ expect(result.changed).toBe(true);
661
+ expect(result.code).toContain('const FOUND = true;');
662
+ });
663
+
664
+ it('no patterns property — plugin always runs', () => {
665
+ let code = 'let x = 1;',
666
+ file = parse(code),
667
+ program = makeProgram(file),
668
+ plugin = makePlugin(() => ({ prepend: ['const ALWAYS = true;'] })),
669
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
670
+
671
+ expect(result.changed).toBe(true);
672
+ expect(result.code).toContain('const ALWAYS = true;');
673
+ });
674
+
675
+ // F-TEST-010: applyPrepend edge cases
676
+
677
+ it('no imports — prepend goes to start', () => {
678
+ let code = 'let x = 1;',
679
+ file = parse(code),
680
+ program = makeProgram(file),
681
+ plugin = makePlugin(() => ({ prepend: ['const A = 1;'] })),
682
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
683
+
684
+ expect(result.changed).toBe(true);
685
+
686
+ let prependIdx = result.code.indexOf('const A = 1;'),
687
+ letIdx = result.code.indexOf('let x = 1;');
688
+
689
+ expect(prependIdx).toBeLessThan(letIdx);
690
+ });
691
+
692
+ it('multiple prepend strings appear in order', () => {
693
+ let code = 'let x = 1;',
694
+ file = parse(code),
695
+ program = makeProgram(file),
696
+ plugin = makePlugin(() => ({ prepend: ['const A = 1;', 'const B = 2;'] })),
697
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
698
+
699
+ expect(result.changed).toBe(true);
700
+ expect(result.code).toContain('const A = 1;');
701
+ expect(result.code).toContain('const B = 2;');
702
+
703
+ let aIdx = result.code.indexOf('const A = 1;'),
704
+ bIdx = result.code.indexOf('const B = 2;');
705
+
706
+ expect(aIdx).toBeLessThan(bIdx);
707
+ });
708
+
709
+ // F-TEST-011: applyImports multi-intent
710
+
711
+ it('two ImportIntents for different packages', () => {
712
+ let code = 'let x = 1;',
713
+ file = parse(code),
714
+ program = makeProgram(file),
715
+ plugin = makePlugin(() => ({
716
+ imports: [
717
+ { add: ['a'], package: 'pkg-1' },
718
+ { add: ['b'], package: 'pkg-2' }
719
+ ]
720
+ })),
721
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
722
+
723
+ expect(result.changed).toBe(true);
724
+ expect(result.code).toContain("import { a } from 'pkg-1';");
725
+ expect(result.code).toContain("import { b } from 'pkg-2';");
726
+ });
727
+
728
+ it('ImportIntent with only remove', () => {
729
+ let code = "import { a, b } from 'pkg';\nlet x = 1;",
730
+ file = parse(code),
731
+ program = makeProgram(file),
732
+ plugin = makePlugin(() => ({
733
+ imports: [{ package: 'pkg', remove: ['a'] }]
734
+ })),
735
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
736
+
737
+ expect(result.changed).toBe(true);
738
+ expect(result.code).toContain('b');
739
+ expect(result.code).not.toMatch(/import\s*\{[^}]*a[^}]*\}\s*from\s*'pkg'/);
740
+ expect(result.code).toContain("import { b } from 'pkg';");
741
+ });
742
+
743
+ it('propagates plugin.transform() exception', () => {
744
+ let code = 'let x = 1;',
745
+ file = parse(code),
746
+ program = makeProgram(file),
747
+ plugin = makePlugin(() => { throw new Error('plugin crashed'); });
748
+
749
+ expect(() => coordinator.transform([plugin], code, file, program, '/root', new Map())).toThrow('plugin crashed');
750
+ });
751
+
752
+ it('falls back to createSourceFile when getSourceFile returns undefined', async () => {
753
+ let code = 'let x = 1;',
754
+ file = parse(code),
755
+ program = makeProgram(file);
756
+
757
+ let languageService = await import('~/compiler/language-service');
758
+
759
+ vi.mocked(languageService.default.update).mockReturnValueOnce({
760
+ getSourceFile: () => undefined,
761
+ getTypeChecker: () => ({} as ts.TypeChecker)
762
+ } as unknown as ts.Program);
763
+
764
+ let plugin1 = makePlugin(() => ({ prepend: ['const A = 1;'] })),
765
+ plugin2 = makePlugin((ctx) => {
766
+ if (ctx.code.includes('const A = 1;')) {
767
+ return { prepend: ['const B = 2;'] };
768
+ }
769
+
770
+ return {};
771
+ }),
772
+ result = coordinator.transform([plugin1, plugin2], code, file, program, '/root', new Map());
773
+
774
+ expect(result.changed).toBe(true);
775
+ expect(result.code).toContain('const A = 1;');
776
+ expect(result.code).toContain('const B = 2;');
777
+ });
778
+
779
+ // F-003: applyImports batching
780
+
781
+ describe('applyImports batching', () => {
782
+ it('batches multiple intents for the same package into one modify call', () => {
783
+ let code = 'let x = 1;',
784
+ file = parse(code),
785
+ program = makeProgram(file),
786
+ plugin = makePlugin(() => ({
787
+ imports: [
788
+ { add: ['foo'], package: '@pkg/a' },
789
+ { add: ['bar'], package: '@pkg/a' },
790
+ { add: ['baz'], package: '@pkg/a' }
791
+ ]
792
+ })),
793
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
794
+
795
+ expect(result.changed).toBe(true);
796
+ expect(result.code).toContain("import { bar, baz, foo } from '@pkg/a';");
797
+
798
+ let importMatches = result.code.match(/import\s*\{[^}]+\}\s*from\s*'@pkg\/a'/g);
799
+
800
+ expect(importMatches).toHaveLength(1);
801
+ });
802
+
803
+ it('re-parses only between distinct packages', () => {
804
+ let code = 'let x = 1;',
805
+ file = parse(code),
806
+ program = makeProgram(file),
807
+ plugin = makePlugin(() => ({
808
+ imports: [
809
+ { add: ['foo'], package: '@pkg/a' },
810
+ { add: ['bar'], package: '@pkg/a' },
811
+ { add: ['qux'], package: '@pkg/b' }
812
+ ]
813
+ })),
814
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
815
+
816
+ expect(result.changed).toBe(true);
817
+ expect(result.code).toContain("import { bar, foo } from '@pkg/a';");
818
+ expect(result.code).toContain("import { qux } from '@pkg/b';");
819
+ });
820
+
821
+ it('merges add and remove for same package', () => {
822
+ let code = "import { bar, foo } from '@pkg/a';\nlet x = 1;",
823
+ file = parse(code),
824
+ program = makeProgram(file),
825
+ plugin = makePlugin(() => ({
826
+ imports: [
827
+ { add: ['baz'], package: '@pkg/a' },
828
+ { package: '@pkg/a', remove: ['bar'] }
829
+ ]
830
+ })),
831
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
832
+
833
+ expect(result.changed).toBe(true);
834
+ expect(result.code).toContain("import { baz, foo } from '@pkg/a';");
835
+ expect(result.code).not.toMatch(/import\s*\{[^}]*bar[^}]*\}\s*from\s*'@pkg\/a'/);
836
+ });
837
+
838
+ it('preserves namespace across merged intents', () => {
839
+ let code = 'let x = 1;',
840
+ file = parse(code),
841
+ program = makeProgram(file),
842
+ plugin = makePlugin(() => ({
843
+ imports: [
844
+ { namespace: 'utils', package: '@pkg/a' },
845
+ { add: ['foo'], package: '@pkg/a' }
846
+ ]
847
+ })),
848
+ result = coordinator.transform([plugin], code, file, program, '/root', new Map());
849
+
850
+ expect(result.changed).toBe(true);
851
+ expect(result.code).toContain("import * as utils from '@pkg/a';");
852
+ expect(result.code).toContain("import { foo } from '@pkg/a';");
853
+ });
854
+ });
855
+ });