@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.
- package/README.md +11 -3
- package/bin/tsc-alias +1 -1
- package/build/cli/diagnostics.d.ts +4 -0
- package/build/cli/diagnostics.js +90 -0
- package/build/cli/tsc.d.ts +10 -2
- package/build/cli/tsc.js +352 -51
- package/build/compiler/ast.d.ts +7 -5
- package/build/compiler/ast.js +6 -6
- package/build/compiler/code.d.ts +5 -4
- package/build/compiler/code.js +12 -1
- package/build/compiler/coordinator.d.ts +17 -7
- package/build/compiler/coordinator.js +54 -31
- package/build/compiler/imports.d.ts +6 -3
- package/build/compiler/imports.js +26 -23
- package/build/compiler/index.d.ts +3 -0
- package/build/compiler/index.js +1 -0
- package/build/compiler/language-service.d.ts +24 -5
- package/build/compiler/language-service.js +215 -53
- package/build/compiler/plugins/index.d.ts +4 -2
- package/build/compiler/plugins/tsc.d.ts +1 -1
- package/build/compiler/plugins/vite.d.ts +6 -3
- package/build/compiler/plugins/vite.js +12 -7
- package/build/compiler/sourcemap.d.ts +50 -0
- package/build/compiler/sourcemap.js +247 -0
- package/build/compiler/types.d.ts +7 -6
- package/build/compiler/uid.d.ts +5 -2
- package/build/compiler/uid.js +33 -4
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/ts.d.ts +3 -0
- package/build/ts.js +3 -0
- package/package.json +22 -8
- package/tsconfig.dev.json +13 -0
- package/tsconfig.package.json +4 -1
- package/.claude/skills/code-audit/registry-typescript.json +0 -326
- package/.editorconfig +0 -9
- package/.gitattributes +0 -2
- package/.github/dependabot.yml +0 -25
- package/.github/workflows/bump.yml +0 -27
- package/.github/workflows/dependabot.yml +0 -58
- package/.github/workflows/publish.yml +0 -42
- package/.github/workflows/templates/bump.yml +0 -9
- package/.github/workflows/templates/dependabot.yml +0 -12
- package/.github/workflows/templates/publish.yml +0 -16
- package/pnpm-workspace.yaml +0 -6
- package/src/cli/tsc.ts +0 -235
- package/src/compiler/ast.ts +0 -60
- package/src/compiler/code.ts +0 -27
- package/src/compiler/coordinator.ts +0 -284
- package/src/compiler/imports.ts +0 -185
- package/src/compiler/index.ts +0 -7
- package/src/compiler/language-service.ts +0 -121
- package/src/compiler/plugins/index.ts +0 -5
- package/src/compiler/plugins/tsc.ts +0 -6
- package/src/compiler/plugins/vite.ts +0 -91
- package/src/compiler/types.ts +0 -83
- package/src/compiler/uid.ts +0 -10
- package/src/constants.ts +0 -4
- package/src/index.ts +0 -1
- package/tests/cli/tsc.test.ts +0 -155
- package/tests/compiler/ast.test.ts +0 -131
- package/tests/compiler/code.test.ts +0 -73
- package/tests/compiler/coordinator.bench.ts +0 -101
- package/tests/compiler/coordinator.test.ts +0 -872
- package/tests/compiler/imports.test.ts +0 -167
- package/tests/compiler/language-service.test.ts +0 -90
- package/tests/compiler/plugins.test.ts +0 -172
- package/tests/compiler/uid.test.ts +0 -70
- package/tsconfig.json +0 -3
- 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
|
-
});
|