@esportsplus/typescript 0.29.6 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +11 -3
  2. package/bin/tsc-alias +1 -1
  3. package/build/cli/diagnostics.d.ts +4 -0
  4. package/build/cli/diagnostics.js +90 -0
  5. package/build/cli/tsc.d.ts +10 -2
  6. package/build/cli/tsc.js +352 -51
  7. package/build/compiler/ast.d.ts +7 -5
  8. package/build/compiler/ast.js +6 -6
  9. package/build/compiler/code.d.ts +5 -4
  10. package/build/compiler/code.js +12 -1
  11. package/build/compiler/coordinator.d.ts +17 -7
  12. package/build/compiler/coordinator.js +54 -31
  13. package/build/compiler/imports.d.ts +6 -3
  14. package/build/compiler/imports.js +26 -23
  15. package/build/compiler/index.d.ts +3 -0
  16. package/build/compiler/index.js +1 -0
  17. package/build/compiler/language-service.d.ts +24 -5
  18. package/build/compiler/language-service.js +215 -53
  19. package/build/compiler/plugins/index.d.ts +4 -2
  20. package/build/compiler/plugins/tsc.d.ts +1 -1
  21. package/build/compiler/plugins/vite.d.ts +6 -3
  22. package/build/compiler/plugins/vite.js +12 -7
  23. package/build/compiler/sourcemap.d.ts +50 -0
  24. package/build/compiler/sourcemap.js +247 -0
  25. package/build/compiler/types.d.ts +7 -6
  26. package/build/compiler/uid.d.ts +5 -2
  27. package/build/compiler/uid.js +33 -4
  28. package/build/index.d.ts +1 -1
  29. package/build/index.js +1 -1
  30. package/build/ts.d.ts +3 -0
  31. package/build/ts.js +3 -0
  32. package/package.json +22 -8
  33. package/tsconfig.dev.json +13 -0
  34. package/tsconfig.package.json +4 -1
  35. package/.claude/skills/code-audit/registry-typescript.json +0 -326
  36. package/.editorconfig +0 -9
  37. package/.gitattributes +0 -2
  38. package/.github/dependabot.yml +0 -25
  39. package/.github/workflows/bump.yml +0 -27
  40. package/.github/workflows/dependabot.yml +0 -58
  41. package/.github/workflows/publish.yml +0 -42
  42. package/.github/workflows/templates/bump.yml +0 -9
  43. package/.github/workflows/templates/dependabot.yml +0 -12
  44. package/.github/workflows/templates/publish.yml +0 -16
  45. package/pnpm-workspace.yaml +0 -6
  46. package/src/cli/tsc.ts +0 -235
  47. package/src/compiler/ast.ts +0 -60
  48. package/src/compiler/code.ts +0 -27
  49. package/src/compiler/coordinator.ts +0 -284
  50. package/src/compiler/imports.ts +0 -185
  51. package/src/compiler/index.ts +0 -7
  52. package/src/compiler/language-service.ts +0 -121
  53. package/src/compiler/plugins/index.ts +0 -5
  54. package/src/compiler/plugins/tsc.ts +0 -6
  55. package/src/compiler/plugins/vite.ts +0 -91
  56. package/src/compiler/types.ts +0 -83
  57. package/src/compiler/uid.ts +0 -10
  58. package/src/constants.ts +0 -4
  59. package/src/index.ts +0 -1
  60. package/tests/cli/tsc.test.ts +0 -155
  61. package/tests/compiler/ast.test.ts +0 -131
  62. package/tests/compiler/code.test.ts +0 -73
  63. package/tests/compiler/coordinator.bench.ts +0 -101
  64. package/tests/compiler/coordinator.test.ts +0 -872
  65. package/tests/compiler/imports.test.ts +0 -167
  66. package/tests/compiler/language-service.test.ts +0 -90
  67. package/tests/compiler/plugins.test.ts +0 -172
  68. package/tests/compiler/uid.test.ts +0 -70
  69. package/tsconfig.json +0 -3
  70. package/vitest.config.ts +0 -14
@@ -1,872 +0,0 @@
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
- it('preserves a type-only import when rewriting a package\'s imports', () => {
350
- let code = "import { html } from 'my-pkg';\nimport type { Renderable } from 'my-pkg';\nlet x = 1;",
351
- file = parse(code),
352
- program = makeProgram(file),
353
- plugin = makePlugin(() => ({
354
- imports: [{ namespace: 'NS', package: 'my-pkg', remove: ['html'] }]
355
- })),
356
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
357
-
358
- expect(result.changed).toBe(true);
359
- expect(result.code).toContain("import * as NS from 'my-pkg';");
360
- // Renderable must stay type-only (import type { ... } or inline `type Renderable`) so it is
361
- // erased at runtime — a runtime `import { Renderable }` fails when it has no runtime export.
362
- expect(result.code).toMatch(/import\s+type\s*\{[^}]*Renderable|import\s*\{[^}]*type\s+Renderable/);
363
- expect(result.code).not.toMatch(/import\s*\{\s*Renderable\s*\}\s*from\s*'my-pkg'/);
364
- });
365
-
366
- // F-TEST-003: Import manipulation integration tests
367
-
368
- it('adds specifiers to existing import', () => {
369
- let code = "import { a } from 'pkg';\nlet x = 1;",
370
- file = parse(code),
371
- program = makeProgram(file),
372
- plugin = makePlugin(() => ({
373
- imports: [{ add: ['b', 'c'], package: 'pkg' }]
374
- })),
375
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
376
-
377
- expect(result.changed).toBe(true);
378
- expect(result.code).toContain('a');
379
- expect(result.code).toContain('b');
380
- expect(result.code).toContain('c');
381
- expect(result.code).toMatch(/import\s*\{[^}]*a[^}]*b[^}]*c[^}]*\}\s*from\s*'pkg'/);
382
- });
383
-
384
- it('removes specifier from import keeping others', () => {
385
- let code = "import { a, b, reactive } from 'my-pkg';\nlet x = 1;",
386
- file = parse(code),
387
- program = makeProgram(file),
388
- plugin = makePlugin(() => ({
389
- imports: [{ package: 'my-pkg', remove: ['reactive'] }]
390
- })),
391
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
392
-
393
- expect(result.changed).toBe(true);
394
- expect(result.code).toContain('a');
395
- expect(result.code).toContain('b');
396
- expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
397
- });
398
-
399
- it('adds namespace import to file without package imports', () => {
400
- let code = 'let x = 1;',
401
- file = parse(code),
402
- program = makeProgram(file),
403
- plugin = makePlugin(() => ({
404
- imports: [{ namespace: 'NS', package: 'my-pkg' }]
405
- })),
406
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
407
-
408
- expect(result.changed).toBe(true);
409
- expect(result.code).toContain("import * as NS from 'my-pkg';");
410
- });
411
-
412
- it('merges duplicate import statements', () => {
413
- let code = "import { a } from 'pkg';\nimport { b } from 'pkg';\nlet x = 1;",
414
- file = parse(code),
415
- program = makeProgram(file),
416
- plugin = makePlugin(() => ({
417
- imports: [{ add: ['c'], package: 'pkg' }]
418
- })),
419
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
420
-
421
- expect(result.changed).toBe(true);
422
-
423
- let importMatches = result.code.match(/import\s*\{[^}]+\}\s*from\s*'pkg'/g);
424
-
425
- expect(importMatches).toHaveLength(1);
426
- expect(result.code).toContain('a');
427
- expect(result.code).toContain('b');
428
- expect(result.code).toContain('c');
429
- });
430
-
431
- it('namespace + remove specifier combined', () => {
432
- let code = "import { reactive } from 'my-pkg';\nlet x = 1;",
433
- file = parse(code),
434
- program = makeProgram(file),
435
- plugin = makePlugin(() => ({
436
- imports: [{ namespace: 'NS', package: 'my-pkg', remove: ['reactive'] }]
437
- })),
438
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
439
-
440
- expect(result.changed).toBe(true);
441
- expect(result.code).toContain("import * as NS from 'my-pkg';");
442
- expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
443
- });
444
-
445
- it('adds import to file with different package imports', () => {
446
- let code = "import { x } from 'other';\nlet a = 1;",
447
- file = parse(code),
448
- program = makeProgram(file),
449
- plugin = makePlugin(() => ({
450
- imports: [{ add: ['y'], package: 'new-pkg' }]
451
- })),
452
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
453
-
454
- expect(result.changed).toBe(true);
455
- expect(result.code).toContain("import { x } from 'other';");
456
- expect(result.code).toContain("import { y } from 'new-pkg';");
457
- });
458
-
459
- // F-TEST-004: replaceReverse edge cases
460
-
461
- it('multiple non-overlapping replacements', () => {
462
- let code = 'let OLD1 = 1; let OLD2 = 2;',
463
- file = parse(code),
464
- program = makeProgram(file),
465
- plugin = makePlugin((ctx) => {
466
- let nodes: ts.Node[] = [];
467
-
468
- ts.forEachChild(ctx.sourceFile, function visit(n) {
469
- if (ts.isIdentifier(n) && (n.text === 'OLD1' || n.text === 'OLD2')) {
470
- nodes.push(n);
471
- }
472
-
473
- ts.forEachChild(n, visit);
474
- });
475
-
476
- return {
477
- replacements: nodes.map(node => ({
478
- generate: () => node.getText(ctx.sourceFile) === 'OLD1' ? 'NEW1' : 'NEW2',
479
- node
480
- }))
481
- };
482
- }),
483
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
484
-
485
- expect(result.changed).toBe(true);
486
- expect(result.code).toContain('NEW1');
487
- expect(result.code).toContain('NEW2');
488
- expect(result.code).not.toContain('OLD1');
489
- expect(result.code).not.toContain('OLD2');
490
- });
491
-
492
- it('replacement with empty string deletes a node', () => {
493
- let code = 'let DELETEME = 1;',
494
- file = parse(code),
495
- program = makeProgram(file),
496
- plugin = makePlugin((ctx) => {
497
- let node: ts.Node | undefined;
498
-
499
- ts.forEachChild(ctx.sourceFile, function visit(n) {
500
- if (ts.isIdentifier(n) && n.text === 'DELETEME') {
501
- node = n;
502
- }
503
-
504
- ts.forEachChild(n, visit);
505
- });
506
-
507
- if (!node) {
508
- return {};
509
- }
510
-
511
- return {
512
- replacements: [{
513
- generate: () => '',
514
- node
515
- }]
516
- };
517
- }),
518
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
519
-
520
- expect(result.changed).toBe(true);
521
- expect(result.code).not.toContain('DELETEME');
522
- });
523
-
524
- it('replacement at file start', () => {
525
- let code = 'FIRST_TOKEN;',
526
- file = parse(code),
527
- program = makeProgram(file),
528
- plugin = makePlugin((ctx) => {
529
- let node: ts.Node | undefined;
530
-
531
- ts.forEachChild(ctx.sourceFile, function visit(n) {
532
- if (ts.isIdentifier(n) && n.text === 'FIRST_TOKEN') {
533
- node = n;
534
- }
535
-
536
- ts.forEachChild(n, visit);
537
- });
538
-
539
- if (!node) {
540
- return {};
541
- }
542
-
543
- return {
544
- replacements: [{
545
- generate: () => 'REPLACED_FIRST',
546
- node
547
- }]
548
- };
549
- }),
550
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
551
-
552
- expect(result.changed).toBe(true);
553
- expect(result.code).toContain('REPLACED_FIRST');
554
- expect(result.code.indexOf('REPLACED_FIRST')).toBe(0);
555
- });
556
-
557
- // F-TEST-006: Multi-plugin pipeline
558
-
559
- it('first plugin modifies code, second receives updated code', () => {
560
- let code = 'let OLD = 1;',
561
- file = parse(code),
562
- program = makeProgram(file),
563
- plugin1 = makePlugin((ctx) => {
564
- let node: ts.Node | undefined;
565
-
566
- ts.forEachChild(ctx.sourceFile, function visit(n) {
567
- if (ts.isIdentifier(n) && n.text === 'OLD') {
568
- node = n;
569
- }
570
-
571
- ts.forEachChild(n, visit);
572
- });
573
-
574
- if (!node) {
575
- return {};
576
- }
577
-
578
- return {
579
- replacements: [{
580
- generate: () => 'TRANSFORMED',
581
- node
582
- }]
583
- };
584
- }),
585
- plugin2 = makePlugin((ctx) => {
586
- if (ctx.code.includes('TRANSFORMED')) {
587
- return { prepend: ['const SEEN_BY_PLUGIN2 = true;'] };
588
- }
589
-
590
- return {};
591
- }),
592
- result = coordinator.transform([plugin1, plugin2], code, file, program, '/root', new Map());
593
-
594
- expect(result.changed).toBe(true);
595
- expect(result.code).toContain('TRANSFORMED');
596
- expect(result.code).toContain('const SEEN_BY_PLUGIN2 = true;');
597
- });
598
-
599
- it('three plugins — first skipped, second and third run', () => {
600
- let code = 'let x = 1;',
601
- file = parse(code),
602
- program = makeProgram(file),
603
- plugin1: Plugin = {
604
- patterns: ['MISSING'],
605
- transform: () => ({ prepend: ['const SHOULD_NOT_APPEAR = true;'] })
606
- },
607
- plugin2 = makePlugin(() => ({ prepend: ['const A = 1;'] })),
608
- plugin3 = makePlugin(() => ({ prepend: ['const B = 2;'] })),
609
- result = coordinator.transform([plugin1, plugin2, plugin3], code, file, program, '/root', new Map());
610
-
611
- expect(result.changed).toBe(true);
612
- expect(result.code).not.toContain('SHOULD_NOT_APPEAR');
613
- expect(result.code).toContain('const A = 1;');
614
- expect(result.code).toContain('const B = 2;');
615
- });
616
-
617
- // F-TEST-009: generate() sourceFile correctness
618
-
619
- it('generate() receives correct sourceFile after prior replacement', () => {
620
- let code = "let TARGET = 'hello';",
621
- file = parse(code),
622
- program = makeProgram(file),
623
- plugin = makePlugin((ctx) => {
624
- let node: ts.Node | undefined;
625
-
626
- ts.forEachChild(ctx.sourceFile, function visit(n) {
627
- if (ts.isIdentifier(n) && n.text === 'TARGET') {
628
- node = n;
629
- }
630
-
631
- ts.forEachChild(n, visit);
632
- });
633
-
634
- if (!node) {
635
- return {};
636
- }
637
-
638
- return {
639
- replacements: [{
640
- generate: (sf) => `REPLACED_IN_${sf.fileName}`,
641
- node
642
- }]
643
- };
644
- }),
645
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
646
-
647
- expect(result.changed).toBe(true);
648
- expect(result.code).toContain('REPLACED_IN_test.ts');
649
- });
650
-
651
- // F-TEST-005: Pattern filtering edge cases
652
-
653
- it('pattern in string literal still matches', () => {
654
- let code = 'let x = "reactive(";',
655
- file = parse(code),
656
- program = makeProgram(file),
657
- plugin: Plugin = {
658
- patterns: ['reactive('],
659
- transform: () => ({ prepend: ['const MATCHED = true;'] })
660
- },
661
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
662
-
663
- expect(result.changed).toBe(true);
664
- expect(result.code).toContain('const MATCHED = true;');
665
- });
666
-
667
- it('multiple patterns, only one matches', () => {
668
- let code = 'let PRESENT = 1;',
669
- file = parse(code),
670
- program = makeProgram(file),
671
- plugin: Plugin = {
672
- patterns: ['MISSING', 'PRESENT'],
673
- transform: () => ({ prepend: ['const FOUND = true;'] })
674
- },
675
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
676
-
677
- expect(result.changed).toBe(true);
678
- expect(result.code).toContain('const FOUND = true;');
679
- });
680
-
681
- it('no patterns property — plugin always runs', () => {
682
- let code = 'let x = 1;',
683
- file = parse(code),
684
- program = makeProgram(file),
685
- plugin = makePlugin(() => ({ prepend: ['const ALWAYS = true;'] })),
686
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
687
-
688
- expect(result.changed).toBe(true);
689
- expect(result.code).toContain('const ALWAYS = true;');
690
- });
691
-
692
- // F-TEST-010: applyPrepend edge cases
693
-
694
- it('no imports — prepend goes to start', () => {
695
- let code = 'let x = 1;',
696
- file = parse(code),
697
- program = makeProgram(file),
698
- plugin = makePlugin(() => ({ prepend: ['const A = 1;'] })),
699
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
700
-
701
- expect(result.changed).toBe(true);
702
-
703
- let prependIdx = result.code.indexOf('const A = 1;'),
704
- letIdx = result.code.indexOf('let x = 1;');
705
-
706
- expect(prependIdx).toBeLessThan(letIdx);
707
- });
708
-
709
- it('multiple prepend strings appear in order', () => {
710
- let code = 'let x = 1;',
711
- file = parse(code),
712
- program = makeProgram(file),
713
- plugin = makePlugin(() => ({ prepend: ['const A = 1;', 'const B = 2;'] })),
714
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
715
-
716
- expect(result.changed).toBe(true);
717
- expect(result.code).toContain('const A = 1;');
718
- expect(result.code).toContain('const B = 2;');
719
-
720
- let aIdx = result.code.indexOf('const A = 1;'),
721
- bIdx = result.code.indexOf('const B = 2;');
722
-
723
- expect(aIdx).toBeLessThan(bIdx);
724
- });
725
-
726
- // F-TEST-011: applyImports multi-intent
727
-
728
- it('two ImportIntents for different packages', () => {
729
- let code = 'let x = 1;',
730
- file = parse(code),
731
- program = makeProgram(file),
732
- plugin = makePlugin(() => ({
733
- imports: [
734
- { add: ['a'], package: 'pkg-1' },
735
- { add: ['b'], package: 'pkg-2' }
736
- ]
737
- })),
738
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
739
-
740
- expect(result.changed).toBe(true);
741
- expect(result.code).toContain("import { a } from 'pkg-1';");
742
- expect(result.code).toContain("import { b } from 'pkg-2';");
743
- });
744
-
745
- it('ImportIntent with only remove', () => {
746
- let code = "import { a, b } from 'pkg';\nlet x = 1;",
747
- file = parse(code),
748
- program = makeProgram(file),
749
- plugin = makePlugin(() => ({
750
- imports: [{ package: 'pkg', remove: ['a'] }]
751
- })),
752
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
753
-
754
- expect(result.changed).toBe(true);
755
- expect(result.code).toContain('b');
756
- expect(result.code).not.toMatch(/import\s*\{[^}]*a[^}]*\}\s*from\s*'pkg'/);
757
- expect(result.code).toContain("import { b } from 'pkg';");
758
- });
759
-
760
- it('propagates plugin.transform() exception', () => {
761
- let code = 'let x = 1;',
762
- file = parse(code),
763
- program = makeProgram(file),
764
- plugin = makePlugin(() => { throw new Error('plugin crashed'); });
765
-
766
- expect(() => coordinator.transform([plugin], code, file, program, '/root', new Map())).toThrow('plugin crashed');
767
- });
768
-
769
- it('falls back to createSourceFile when getSourceFile returns undefined', async () => {
770
- let code = 'let x = 1;',
771
- file = parse(code),
772
- program = makeProgram(file);
773
-
774
- let languageService = await import('~/compiler/language-service');
775
-
776
- vi.mocked(languageService.default.update).mockReturnValueOnce({
777
- getSourceFile: () => undefined,
778
- getTypeChecker: () => ({} as ts.TypeChecker)
779
- } as unknown as ts.Program);
780
-
781
- let plugin1 = makePlugin(() => ({ prepend: ['const A = 1;'] })),
782
- plugin2 = makePlugin((ctx) => {
783
- if (ctx.code.includes('const A = 1;')) {
784
- return { prepend: ['const B = 2;'] };
785
- }
786
-
787
- return {};
788
- }),
789
- result = coordinator.transform([plugin1, plugin2], code, file, program, '/root', new Map());
790
-
791
- expect(result.changed).toBe(true);
792
- expect(result.code).toContain('const A = 1;');
793
- expect(result.code).toContain('const B = 2;');
794
- });
795
-
796
- // F-003: applyImports batching
797
-
798
- describe('applyImports batching', () => {
799
- it('batches multiple intents for the same package into one modify call', () => {
800
- let code = 'let x = 1;',
801
- file = parse(code),
802
- program = makeProgram(file),
803
- plugin = makePlugin(() => ({
804
- imports: [
805
- { add: ['foo'], package: '@pkg/a' },
806
- { add: ['bar'], package: '@pkg/a' },
807
- { add: ['baz'], package: '@pkg/a' }
808
- ]
809
- })),
810
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
811
-
812
- expect(result.changed).toBe(true);
813
- expect(result.code).toContain("import { bar, baz, foo } from '@pkg/a';");
814
-
815
- let importMatches = result.code.match(/import\s*\{[^}]+\}\s*from\s*'@pkg\/a'/g);
816
-
817
- expect(importMatches).toHaveLength(1);
818
- });
819
-
820
- it('re-parses only between distinct packages', () => {
821
- let code = 'let x = 1;',
822
- file = parse(code),
823
- program = makeProgram(file),
824
- plugin = makePlugin(() => ({
825
- imports: [
826
- { add: ['foo'], package: '@pkg/a' },
827
- { add: ['bar'], package: '@pkg/a' },
828
- { add: ['qux'], package: '@pkg/b' }
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 { bar, foo } from '@pkg/a';");
835
- expect(result.code).toContain("import { qux } from '@pkg/b';");
836
- });
837
-
838
- it('merges add and remove for same package', () => {
839
- let code = "import { bar, foo } from '@pkg/a';\nlet x = 1;",
840
- file = parse(code),
841
- program = makeProgram(file),
842
- plugin = makePlugin(() => ({
843
- imports: [
844
- { add: ['baz'], package: '@pkg/a' },
845
- { package: '@pkg/a', remove: ['bar'] }
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 { baz, foo } from '@pkg/a';");
852
- expect(result.code).not.toMatch(/import\s*\{[^}]*bar[^}]*\}\s*from\s*'@pkg\/a'/);
853
- });
854
-
855
- it('preserves namespace across merged intents', () => {
856
- let code = 'let x = 1;',
857
- file = parse(code),
858
- program = makeProgram(file),
859
- plugin = makePlugin(() => ({
860
- imports: [
861
- { namespace: 'utils', package: '@pkg/a' },
862
- { add: ['foo'], package: '@pkg/a' }
863
- ]
864
- })),
865
- result = coordinator.transform([plugin], code, file, program, '/root', new Map());
866
-
867
- expect(result.changed).toBe(true);
868
- expect(result.code).toContain("import * as utils from '@pkg/a';");
869
- expect(result.code).toContain("import { foo } from '@pkg/a';");
870
- });
871
- });
872
- });