Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@esportsplus/typescript

Package Overview
Dependencies
Maintainers
1
Versions
117
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@esportsplus/typescript - npm Package Compare versions

Comparing version
0.29.0
to
0.29.1
+326
.claude/skills/code-audit/registry-typescript.json
{
"project": "@esportsplus/typescript",
"version": "0.29.0",
"target": "src/ tests/",
"runs": [
{
"date": "2026-04-11",
"commit": "e0657fe",
"mode": "full",
"files_audited": 13,
"files_skipped": 0,
"findings_new": 2,
"findings_recurring": 0,
"sqale_rating": "A",
"agent_results": {
"correctness": { "status": "ok", "symbols": 26, "sub_targets": 1, "findings": 1 },
"security": { "status": "ok", "symbols": 14, "sub_targets": 1, "findings": 0 },
"performance": { "status": "ok", "symbols": 15, "sub_targets": 1, "findings": 1 },
"architecture": { "status": "ok", "symbols": 32, "sub_targets": 1, "findings": 0 },
"testing": { "status": "ok", "symbols": 16, "sub_targets": 1, "findings": 1 }
},
"agent_overlap": {},
"estimated_remaining": null
},
{
"date": "2026-04-11",
"commit": "e0657fe",
"mode": "incremental",
"files_audited": 13,
"files_skipped": 0,
"findings_new": 0,
"findings_recurring": 1,
"sqale_rating": "A",
"agent_results": {
"correctness": { "status": "ok", "symbols": 26, "sub_targets": 1, "findings": 0 },
"security": { "status": "ok", "symbols": 14, "sub_targets": 1, "findings": 0 },
"performance": { "status": "ok", "symbols": 15, "sub_targets": 1, "findings": 0 },
"architecture": { "status": "ok", "symbols": 32, "sub_targets": 1, "findings": 0 },
"testing": { "status": "ok", "symbols": 16, "sub_targets": 1, "findings": 1 }
},
"agent_overlap": {},
"estimated_remaining": null
},
{
"date": "2026-04-11",
"commit": "e0657fe",
"mode": "incremental",
"files_audited": 13,
"files_skipped": 0,
"findings_new": 0,
"findings_recurring": 0,
"sqale_rating": "A",
"agent_results": {
"correctness": { "status": "ok", "symbols": 26, "sub_targets": 1, "findings": 0 },
"security": { "status": "ok", "symbols": 14, "sub_targets": 1, "findings": 0 },
"performance": { "status": "ok", "symbols": 15, "sub_targets": 1, "findings": 0 },
"architecture": { "status": "ok", "symbols": 32, "sub_targets": 1, "findings": 0 },
"testing": { "status": "ok", "symbols": 16, "sub_targets": 1, "findings": 0 }
},
"agent_overlap": {},
"estimated_remaining": null
},
{
"date": "2026-04-11",
"commit": "59ea7a3",
"mode": "incremental",
"files_audited": 19,
"files_skipped": 0,
"findings_new": 9,
"findings_recurring": 1,
"sqale_rating": "B",
"agent_results": {
"correctness": { "status": "ok", "symbols": 26, "sub_targets": 1, "findings": 0 },
"security": { "status": "ok", "symbols": 14, "sub_targets": 1, "findings": 0 },
"performance": { "status": "ok", "symbols": 15, "sub_targets": 1, "findings": 0 },
"architecture": { "status": "ok", "symbols": 32, "sub_targets": 1, "findings": 2 },
"testing": { "status": "ok", "symbols": 35, "sub_targets": 1, "findings": 7 }
},
"agent_overlap": {},
"estimated_remaining": null
},
{
"date": "2026-04-11",
"commit": "59ea7a3",
"mode": "incremental",
"files_audited": 21,
"files_skipped": 0,
"findings_new": 0,
"findings_recurring": 0,
"sqale_rating": "A",
"agent_results": {
"correctness": { "status": "ok", "symbols": 24, "sub_targets": 1, "findings": 0 },
"security": { "status": "ok", "symbols": 14, "sub_targets": 1, "findings": 0 },
"performance": { "status": "ok", "symbols": 13, "sub_targets": 1, "findings": 0 },
"architecture": { "status": "ok", "symbols": 30, "sub_targets": 1, "findings": 0 },
"testing": { "status": "ok", "symbols": 35, "sub_targets": 1, "findings": 0 }
},
"agent_overlap": {},
"estimated_remaining": null
},
{
"date": "2026-04-11",
"commit": "59ea7a3",
"mode": "incremental",
"files_audited": 21,
"files_skipped": 0,
"findings_new": 0,
"findings_recurring": 0,
"sqale_rating": "A",
"agent_results": {
"correctness": { "status": "ok", "symbols": 24, "sub_targets": 1, "findings": 0 },
"security": { "status": "ok", "symbols": 14, "sub_targets": 1, "findings": 0 },
"performance": { "status": "ok", "symbols": 13, "sub_targets": 1, "findings": 0 },
"architecture": { "status": "ok", "symbols": 30, "sub_targets": 1, "findings": 0 },
"testing": { "status": "ok", "symbols": 35, "sub_targets": 1, "findings": 0 }
},
"agent_overlap": {},
"estimated_remaining": null
}
],
"findings": {
"F-001": {
"status": "implemented",
"file": "src/compiler/coordinator.ts",
"symbol": "transform",
"category": "correctness",
"title": "AST Position Staleness After Code Modification",
"content_hash": "ast-position-staleness-coordinator-transform",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["correctness"],
"priority_score": 66
},
"F-002": {
"status": "deferred",
"file": "src/compiler/coordinator.ts",
"symbol": "applyImports",
"category": "optimize",
"title": "Redundant SourceFile Recreation in applyImports Loop",
"content_hash": "redundant-sourcefile-recreation-applyimports",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["performance"],
"priority_score": 44
},
"F-003": {
"status": "implemented",
"file": "src/",
"symbol": "*",
"category": "coverage",
"title": "Complete Absence of Test Infrastructure",
"content_hash": "missing-test-infrastructure",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["testing"],
"priority_score": 114
},
"F-004": {
"status": "implemented",
"file": "src/compiler/language-service.ts",
"symbol": "get",
"category": "loc",
"title": "languageService.get() is dead code",
"content_hash": "dead-code-language-service-get",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["architecture"],
"priority_score": 45
},
"F-005": {
"status": "implemented",
"file": "src/compiler/language-service.ts",
"symbol": "delete",
"category": "loc",
"title": "languageService.delete() is dead code",
"content_hash": "dead-code-language-service-delete",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["architecture"],
"priority_score": 45
},
"F-006": {
"status": "implemented",
"file": "src/compiler/language-service.ts",
"symbol": "*",
"category": "coverage",
"title": "language-service.ts has 0 test coverage",
"content_hash": "no-test-coverage-language-service",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["testing"],
"priority_score": 50
},
"F-007": {
"status": "open",
"file": "src/cli/tsc.ts",
"symbol": "*",
"category": "coverage",
"title": "cli/tsc.ts has 0 test coverage",
"content_hash": "no-test-coverage-cli-tsc",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["testing"],
"priority_score": 35
},
"F-008": {
"status": "implemented",
"file": "src/compiler/coordinator.ts",
"symbol": "transform",
"category": "test-quality",
"title": "coordinator plugin.transform() throw path untested",
"content_hash": "test-quality-coordinator-throw-path",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["testing"],
"priority_score": 48
},
"F-009": {
"status": "implemented",
"file": "src/compiler/coordinator.ts",
"symbol": "transform",
"category": "test-quality",
"title": "coordinator null sourceFile fallback path untested",
"content_hash": "test-quality-coordinator-null-sourcefile",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["testing"],
"priority_score": 48
},
"F-010": {
"status": "implemented",
"file": "src/compiler/plugins/vite.ts",
"symbol": "transform",
"category": "test-quality",
"title": "vite plugin error catch path untested",
"content_hash": "test-quality-vite-error-catch",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["testing"],
"priority_score": 48
},
"F-011": {
"status": "implemented",
"file": "src/compiler/plugins/vite.ts",
"symbol": "transform",
"category": "test-quality",
"title": "vite plugin sourceFile undefined fallback untested",
"content_hash": "test-quality-vite-sourcefile-fallback",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["testing"],
"priority_score": 45
},
"F-012": {
"status": "implemented",
"file": "src/compiler/imports.ts",
"symbol": "includes",
"category": "test-quality",
"title": "imports.includes() getAliasedSymbol catch block untested",
"content_hash": "test-quality-imports-aliased-catch",
"file_hash": "",
"first_seen": "2026-04-11",
"last_seen": "2026-04-11",
"found_by": ["testing"],
"priority_score": 42
}
},
"clean_symbols": {},
"convergence": {
"per_category": {
"correctness": {
"findings_per_run": [1, 0, 0, 0, 0, 0],
"successful_runs": 6,
"consecutive_zero": 5,
"last_agent_success": true,
"status": "CONVERGED",
"reason": "0 new findings on 5 consecutive successful runs"
},
"security": {
"findings_per_run": [0, 0, 0, 0, 0, 0],
"successful_runs": 6,
"consecutive_zero": 6,
"last_agent_success": true,
"status": "CONVERGED",
"reason": "0 new findings on 6 consecutive successful runs"
},
"performance": {
"findings_per_run": [1, 0, 0, 0, 0, 0],
"successful_runs": 6,
"consecutive_zero": 5,
"last_agent_success": true,
"status": "CONVERGED",
"reason": "0 new findings on 5 consecutive successful runs"
},
"architecture": {
"findings_per_run": [0, 0, 0, 2, 0, 0],
"successful_runs": 6,
"consecutive_zero": 2,
"last_agent_success": true,
"status": "CONVERGED",
"reason": "0 new findings on 2 consecutive runs after fixes"
},
"testing": {
"findings_per_run": [1, 1, 0, 7, 0, 0],
"successful_runs": 6,
"consecutive_zero": 2,
"last_agent_success": true,
"status": "CONVERGED",
"reason": "0 new findings on 2 consecutive runs after fixes"
}
},
"overall": "CONVERGED",
"reason": "all 5 categories converged with 0 findings on 2+ consecutive runs"
}
}
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { isPlugin, loadPlugins, normalizePath, runTscAlias } from '~/cli/tsc';
describe('isPlugin', () => {
it('returns true for valid plugin', () => {
expect(isPlugin({ transform: () => {} })).toBe(true);
});
it('returns false for null', () => {
expect(isPlugin(null)).toBe(false);
});
it('returns false for empty object', () => {
expect(isPlugin({})).toBe(false);
});
it('returns false when transform is not a function', () => {
expect(isPlugin({ transform: 'not-fn' })).toBe(false);
});
it('returns false for primitives', () => {
expect(isPlugin(42)).toBe(false);
expect(isPlugin('str')).toBe(false);
expect(isPlugin(undefined)).toBe(false);
});
});
describe('normalizePath', () => {
it('converts backslashes to forward slashes', () => {
let result = normalizePath('C:\\foo\\bar.ts');
expect(result).not.toContain('\\');
expect(result).toContain('/foo/bar');
});
it('resolves to absolute path', () => {
let result = normalizePath('relative/file.ts');
expect(path.isAbsolute(result)).toBe(true);
});
});
describe('loadPlugins', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tsc-test-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('loads a valid plugin with transform export', async () => {
let pluginFile = 'plugin.mjs';
fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default { transform: () => ({}) };');
let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
expect(plugins).toHaveLength(1);
expect(typeof plugins[0].transform).toBe('function');
});
it('loads a factory function that returns a plugin', async () => {
let pluginFile = 'factory.mjs';
fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default function() { return { transform: () => ({}) }; };');
let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
expect(plugins).toHaveLength(1);
expect(typeof plugins[0].transform).toBe('function');
});
it('loads array of plugins', async () => {
let pluginFile = 'array.mjs';
fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default [{ transform: () => ({}) }, { transform: () => ({}) }];');
let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
expect(plugins).toHaveLength(2);
});
it('skips invalid plugin format with error', async () => {
let pluginFile = 'invalid.mjs';
fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default { notTransform: true };');
let spy = vi.spyOn(console, 'error').mockImplementation(() => {});
let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
expect(plugins).toHaveLength(0);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
it('skips invalid array element with error', async () => {
let pluginFile = 'mixed.mjs';
fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default [{ transform: () => ({}) }, { bad: true }];');
let spy = vi.spyOn(console, 'error').mockImplementation(() => {});
let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
expect(plugins).toHaveLength(1);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
it('resolves relative paths from root', async () => {
let pluginFile = 'relative.mjs';
fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default { transform: () => ({}) };');
let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
expect(plugins).toHaveLength(1);
});
});
describe('runTscAlias', () => {
it('returns 0 for --noEmit flag', async () => {
let code = await runTscAlias(['--noEmit']);
expect(code).toBe(0);
});
it('returns 0 for --help flag', async () => {
let code = await runTscAlias(['--help']);
expect(code).toBe(0);
});
it('returns 0 for --version flag', async () => {
let code = await runTscAlias(['--version']);
expect(code).toBe(0);
});
it('returns 0 for -v flag', async () => {
let code = await runTscAlias(['-v']);
expect(code).toBe(0);
});
});
import { describe, expect, it } from 'vitest';
import ts from 'typescript';
import ast from '~/compiler/ast';
function parse(code: string): ts.SourceFile {
return ts.createSourceFile('test.ts', code, ts.ScriptTarget.Latest, true);
}
function findFirst(file: ts.SourceFile, predicate: (n: ts.Node) => boolean): ts.Node | undefined {
let result: ts.Node | undefined;
function visit(node: ts.Node) {
if (result) {
return;
}
if (predicate(node)) {
result = node;
return;
}
ts.forEachChild(node, visit);
}
ts.forEachChild(file, visit);
return result;
}
describe('ast.expression.name', () => {
it('returns text for identifiers', () => {
let file = parse('let x = foo;'),
node = findFirst(file, ts.isIdentifier);
expect(node).toBeDefined();
expect(ast.expression.name(node as ts.Expression)).toBe('x');
});
it('returns dotted path for property access', () => {
let file = parse('a.b.c;'),
node = findFirst(file, ts.isPropertyAccessExpression);
expect(node).toBeDefined();
expect(ast.expression.name(node as ts.Expression)).toBe('a.b.c');
});
it('returns null for unsupported expressions', () => {
let file = parse('foo();'),
node = findFirst(file, ts.isCallExpression);
expect(node).toBeDefined();
expect(ast.expression.name(node as ts.Expression)).toBeNull();
});
});
describe('ast.inRange', () => {
it('returns true when node is within range', () => {
expect(ast.inRange([{ start: 0, end: 100 }], 10, 50)).toBe(true);
});
it('returns true at exact boundaries', () => {
expect(ast.inRange([{ start: 10, end: 50 }], 10, 50)).toBe(true);
});
it('returns false when node is outside range', () => {
expect(ast.inRange([{ start: 10, end: 50 }], 0, 9)).toBe(false);
});
it('returns false when node partially overlaps', () => {
expect(ast.inRange([{ start: 10, end: 50 }], 5, 30)).toBe(false);
});
it('returns false for empty ranges', () => {
expect(ast.inRange([], 0, 10)).toBe(false);
});
it('checks multiple ranges', () => {
let ranges = [{ start: 0, end: 10 }, { start: 20, end: 30 }];
expect(ast.inRange(ranges, 0, 10)).toBe(true);
expect(ast.inRange(ranges, 25, 28)).toBe(true);
expect(ast.inRange(ranges, 11, 19)).toBe(false);
});
});
describe('ast.property.path', () => {
it('returns dotted path for nested access', () => {
let file = parse('a.b.c.d;'),
node = findFirst(file, (n) => ts.isPropertyAccessExpression(n) && !ts.isPropertyAccessExpression(n.parent));
expect(node).toBeDefined();
expect(ast.property.path(node as ts.Expression)).toBe('a.b.c.d');
});
it('returns null for non-identifier base', () => {
let file = parse('foo().bar;'),
node = findFirst(file, ts.isPropertyAccessExpression);
expect(node).toBeDefined();
expect(ast.property.path(node as ts.Expression)).toBeNull();
});
});
describe('ast.test', () => {
it('returns true when predicate matches root', () => {
let file = parse('let x = 1;'),
found = ast.test(file, (n) => ts.isVariableStatement(n));
expect(found).toBe(true);
});
it('returns true when predicate matches deep child', () => {
let file = parse('function f() { return { a: 1 }; }'),
found = ast.test(file, (n) => ts.isObjectLiteralExpression(n));
expect(found).toBe(true);
});
it('returns false when predicate never matches', () => {
let file = parse('let x = 1;'),
found = ast.test(file, (n) => ts.isClassDeclaration(n));
expect(found).toBe(false);
});
});
import { describe, expect, it } from 'vitest';
import code from '~/compiler/code';
describe('code', () => {
it('interpolates values into template', () => {
let result = code`let x = ${'hello'};`;
expect(result).toBe("let x = hello;");
});
it('handles multiple interpolations', () => {
let result = code`${'a'} + ${'b'} = ${'c'}`;
expect(result).toBe('a + b = c');
});
it('collapses null to empty string', () => {
let result = code`x${null}y`;
expect(result).toBe('xy');
});
it('collapses undefined to empty string', () => {
let result = code`x${undefined}y`;
expect(result).toBe('xy');
});
it('collapses false to empty string', () => {
let result = code`x${false}y`;
expect(result).toBe('xy');
});
it('preserves zero', () => {
let result = code`x${0}y`;
expect(result).toBe('x0y');
});
it('preserves empty string', () => {
let result = code`x${''}y`;
expect(result).toBe('xy');
});
it('handles no interpolations', () => {
let result = code`just plain text`;
expect(result).toBe('just plain text');
});
});
describe('code.escape', () => {
it('escapes single quotes', () => {
expect(code.escape("it's")).toBe("it\\'s");
});
it('escapes multiple quotes', () => {
expect(code.escape("a'b'c")).toBe("a\\'b\\'c");
});
it('returns unchanged string without quotes', () => {
expect(code.escape('hello')).toBe('hello');
});
it('handles empty string', () => {
expect(code.escape('')).toBe('');
});
});
import { bench, describe, vi } from 'vitest';
import ts from 'typescript';
import type { Plugin, TransformContext } from '~/compiler/types';
import coordinator from '~/compiler/coordinator';
vi.mock('~/compiler/language-service', () => ({
default: {
invalidate: vi.fn(),
update: vi.fn((_root: string, fileName: string, content: string) => {
let file = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
return {
getSourceFile: () => file,
getTypeChecker: () => ({} as ts.TypeChecker)
} as unknown as ts.Program;
})
}
}));
function parse(code: string, fileName = 'bench.ts'): ts.SourceFile {
return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
}
function makeProgram(file: ts.SourceFile): ts.Program {
return {
getSourceFile: () => file,
getTypeChecker: () => ({} as ts.TypeChecker)
} as unknown as ts.Program;
}
function makePlugin(transformFn: (ctx: TransformContext) => ReturnType<Plugin['transform']>): Plugin {
return { transform: transformFn };
}
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file);
describe('applyImports batching', () => {
bench('10 intents, 1 package', () => {
let plugin = makePlugin(() => ({
imports: [
{ add: ['a'], package: '@pkg/a' },
{ add: ['b'], package: '@pkg/a' },
{ add: ['c'], package: '@pkg/a' },
{ add: ['d'], package: '@pkg/a' },
{ add: ['e'], package: '@pkg/a' },
{ add: ['f'], package: '@pkg/a' },
{ add: ['g'], package: '@pkg/a' },
{ add: ['h'], package: '@pkg/a' },
{ add: ['i'], package: '@pkg/a' },
{ add: ['j'], package: '@pkg/a' }
]
}));
coordinator.transform([plugin], code, file, program, '/root', new Map());
});
bench('10 intents, 3 packages', () => {
let plugin = makePlugin(() => ({
imports: [
{ add: ['a'], package: '@pkg/a' },
{ add: ['b'], package: '@pkg/a' },
{ add: ['c'], package: '@pkg/a' },
{ add: ['d'], package: '@pkg/b' },
{ add: ['e'], package: '@pkg/b' },
{ add: ['f'], package: '@pkg/b' },
{ add: ['g'], package: '@pkg/b' },
{ add: ['h'], package: '@pkg/c' },
{ add: ['i'], package: '@pkg/c' },
{ add: ['j'], package: '@pkg/c' }
]
}));
coordinator.transform([plugin], code, file, program, '/root', new Map());
});
bench('10 intents, 10 packages', () => {
let plugin = makePlugin(() => ({
imports: [
{ add: ['a'], package: '@pkg/a' },
{ add: ['b'], package: '@pkg/b' },
{ add: ['c'], package: '@pkg/c' },
{ add: ['d'], package: '@pkg/d' },
{ add: ['e'], package: '@pkg/e' },
{ add: ['f'], package: '@pkg/f' },
{ add: ['g'], package: '@pkg/g' },
{ add: ['h'], package: '@pkg/h' },
{ add: ['i'], package: '@pkg/i' },
{ add: ['j'], package: '@pkg/j' }
]
}));
coordinator.transform([plugin], code, file, program, '/root', new Map());
});
});
import { describe, expect, it, vi } from 'vitest';
import ts from 'typescript';
import type { ImportIntent, Plugin, ReplacementIntent, SharedContext, TransformContext } from '~/compiler/types';
import coordinator from '~/compiler/coordinator';
vi.mock('~/compiler/language-service', () => ({
default: {
invalidate: vi.fn(),
update: vi.fn((_root: string, fileName: string, content: string) => {
let file = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
return {
getSourceFile: () => file,
getTypeChecker: () => ({} as ts.TypeChecker)
} as unknown as ts.Program;
})
}
}));
function parse(code: string, fileName = 'test.ts'): ts.SourceFile {
return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
}
function makeProgram(file: ts.SourceFile): ts.Program {
return {
getSourceFile: () => file,
getTypeChecker: () => ({} as ts.TypeChecker)
} as unknown as ts.Program;
}
function makePlugin(transformFn: (ctx: TransformContext) => ReturnType<Plugin['transform']>): Plugin {
return { transform: transformFn };
}
describe('coordinator.transform', () => {
it('returns unchanged when no plugins', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
result = coordinator.transform([], code, file, program, '/root', new Map());
expect(result.changed).toBe(false);
expect(result.code).toBe(code);
});
it('applies replacement intents', () => {
let code = 'let x = OLD;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin((ctx) => {
let node: ts.Node | undefined;
ts.forEachChild(ctx.sourceFile, function visit(n) {
if (ts.isIdentifier(n) && n.text === 'OLD') {
node = n;
}
ts.forEachChild(n, visit);
});
if (!node) {
return {};
}
let intents: ReplacementIntent[] = [{
generate: () => 'NEW',
node
}];
return { replacements: intents };
}),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('NEW');
expect(result.code).not.toContain('OLD');
});
it('applies prepend after imports', () => {
let code = "import { a } from 'pkg';\nlet x = 1;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
prepend: ['const GENERATED = true;']
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('const GENERATED = true;');
let importIdx = result.code.indexOf("import { a } from 'pkg';"),
generatedIdx = result.code.indexOf('const GENERATED = true;'),
letIdx = result.code.indexOf('let x = 1;');
expect(importIdx).toBeLessThan(generatedIdx);
expect(generatedIdx).toBeLessThan(letIdx);
});
it('applies import intents', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
intents: ImportIntent[] = [{
add: ['foo'],
package: 'my-pkg'
}],
plugin = makePlugin(() => ({ imports: intents })),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain("import { foo } from 'my-pkg';");
});
it('skips plugin when patterns do not match', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin: Plugin = {
patterns: ['MAGIC_TOKEN'],
transform: () => ({ prepend: ['SHOULD_NOT_APPEAR'] })
},
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(false);
expect(result.code).not.toContain('SHOULD_NOT_APPEAR');
});
it('runs plugin when patterns match', () => {
let code = 'let MAGIC_TOKEN = 1;',
file = parse(code),
program = makeProgram(file),
plugin: Plugin = {
patterns: ['MAGIC_TOKEN'],
transform: () => ({ prepend: ['const FOUND = true;'] })
},
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('const FOUND = true;');
});
it('re-parses AST between replacements and prepend (F-001 fix)', () => {
let code = "import { a } from 'pkg';\nlet OLD = 1;\nlet y = 2;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin((ctx) => {
let node: ts.Node | undefined;
ts.forEachChild(ctx.sourceFile, function visit(n) {
if (ts.isIdentifier(n) && n.text === 'OLD') {
node = n;
}
ts.forEachChild(n, visit);
});
if (!node) {
return {};
}
return {
prepend: ['const PREPENDED = true;'],
replacements: [{
generate: () => 'REPLACED_LONGER_NAME',
node
}]
};
}),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('REPLACED_LONGER_NAME');
expect(result.code).toContain('const PREPENDED = true;');
// Prepend should be after imports, not corrupted by replacement
let importEnd = result.code.indexOf("';") + 2,
prependIdx = result.code.indexOf('const PREPENDED = true;');
expect(prependIdx).toBeGreaterThan(importEnd);
});
it('chains multiple plugins', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin1 = makePlugin(() => ({ prepend: ['const A = 1;'] })),
plugin2 = makePlugin(() => ({ prepend: ['const B = 2;'] })),
result = coordinator.transform([plugin1, plugin2], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('const A = 1;');
expect(result.code).toContain('const B = 2;');
});
it('shares context between plugins', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
shared: SharedContext = new Map(),
plugin1 = makePlugin((ctx) => {
ctx.shared.set('key', 'value');
return { prepend: ['const A = 1;'] };
}),
plugin2 = makePlugin((ctx) => {
let val = ctx.shared.get('key');
return { prepend: [`const B = '${val}';`] };
}),
result = coordinator.transform([plugin1, plugin2], code, file, program, '/root', shared);
expect(result.changed).toBe(true);
expect(result.code).toContain("const B = 'value';");
});
it('applies replacements + imports together with AST re-parse', () => {
let code = "let OLD = 1;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin((ctx) => {
let node: ts.Node | undefined;
ts.forEachChild(ctx.sourceFile, function visit(n) {
if (ts.isIdentifier(n) && n.text === 'OLD') {
node = n;
}
ts.forEachChild(n, visit);
});
if (!node) {
return {};
}
return {
imports: [{ add: ['helper'], package: 'utils' }],
replacements: [{
generate: () => 'REPLACED',
node
}]
};
}),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('REPLACED');
expect(result.code).toContain("import { helper } from 'utils';");
});
// F-TEST-001: Coordinator integration tests with real-world patterns
it('plugin producing replacements + prepend + imports simultaneously', () => {
let code = "import { reactive } from 'my-pkg';\nlet x = reactive(1);",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin((ctx) => {
let node: ts.Node | undefined;
ts.forEachChild(ctx.sourceFile, function visit(n) {
if (ts.isIdentifier(n) && n.text === 'reactive') {
node = n;
}
ts.forEachChild(n, visit);
});
if (!node) {
return {};
}
return {
imports: [{ namespace: 'NS', package: 'my-pkg', remove: ['reactive'] }],
prepend: ['class ReactiveState {}'],
replacements: [{
generate: () => 'NS.reactive',
node
}]
};
}),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('NS.reactive');
expect(result.code).toContain('class ReactiveState {}');
expect(result.code).toContain("import * as NS from 'my-pkg';");
expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
let nsImportIdx = result.code.indexOf("import * as NS from 'my-pkg';"),
classIdx = result.code.indexOf('class ReactiveState {}'),
bodyIdx = result.code.indexOf('NS.reactive');
expect(nsImportIdx).toBeLessThan(classIdx);
expect(classIdx).toBeLessThan(bodyIdx);
});
it('plugin with generate() closures capturing scope variables', () => {
let code = 'let myVar = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin((ctx) => {
let node: ts.Node | undefined,
varname = '';
ts.forEachChild(ctx.sourceFile, function visit(n) {
if (ts.isIdentifier(n) && n.text === 'myVar') {
node = n;
varname = n.text;
}
ts.forEachChild(n, visit);
});
if (!node) {
return {};
}
return {
replacements: [{
generate: () => `NS.write(${varname}, ${varname}.value)`,
node
}]
};
}),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('NS.write(myVar, myVar.value)');
});
it('file with existing imports, plugin adds namespace + removes specifier', () => {
let code = "import { other, reactive } from 'my-pkg';\nlet x = 1;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [{ namespace: 'NS', package: 'my-pkg', remove: ['reactive'] }]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain("import * as NS from 'my-pkg';");
expect(result.code).toContain('other');
expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
});
// F-TEST-003: Import manipulation integration tests
it('adds specifiers to existing import', () => {
let code = "import { a } from 'pkg';\nlet x = 1;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [{ add: ['b', 'c'], package: 'pkg' }]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('a');
expect(result.code).toContain('b');
expect(result.code).toContain('c');
expect(result.code).toMatch(/import\s*\{[^}]*a[^}]*b[^}]*c[^}]*\}\s*from\s*'pkg'/);
});
it('removes specifier from import keeping others', () => {
let code = "import { a, b, reactive } from 'my-pkg';\nlet x = 1;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [{ package: 'my-pkg', remove: ['reactive'] }]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('a');
expect(result.code).toContain('b');
expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
});
it('adds namespace import to file without package imports', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [{ namespace: 'NS', package: 'my-pkg' }]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain("import * as NS from 'my-pkg';");
});
it('merges duplicate import statements', () => {
let code = "import { a } from 'pkg';\nimport { b } from 'pkg';\nlet x = 1;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [{ add: ['c'], package: 'pkg' }]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
let importMatches = result.code.match(/import\s*\{[^}]+\}\s*from\s*'pkg'/g);
expect(importMatches).toHaveLength(1);
expect(result.code).toContain('a');
expect(result.code).toContain('b');
expect(result.code).toContain('c');
});
it('namespace + remove specifier combined', () => {
let code = "import { reactive } from 'my-pkg';\nlet x = 1;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [{ namespace: 'NS', package: 'my-pkg', remove: ['reactive'] }]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain("import * as NS from 'my-pkg';");
expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
});
it('adds import to file with different package imports', () => {
let code = "import { x } from 'other';\nlet a = 1;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [{ add: ['y'], package: 'new-pkg' }]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain("import { x } from 'other';");
expect(result.code).toContain("import { y } from 'new-pkg';");
});
// F-TEST-004: replaceReverse edge cases
it('multiple non-overlapping replacements', () => {
let code = 'let OLD1 = 1; let OLD2 = 2;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin((ctx) => {
let nodes: ts.Node[] = [];
ts.forEachChild(ctx.sourceFile, function visit(n) {
if (ts.isIdentifier(n) && (n.text === 'OLD1' || n.text === 'OLD2')) {
nodes.push(n);
}
ts.forEachChild(n, visit);
});
return {
replacements: nodes.map(node => ({
generate: () => node.getText(ctx.sourceFile) === 'OLD1' ? 'NEW1' : 'NEW2',
node
}))
};
}),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('NEW1');
expect(result.code).toContain('NEW2');
expect(result.code).not.toContain('OLD1');
expect(result.code).not.toContain('OLD2');
});
it('replacement with empty string deletes a node', () => {
let code = 'let DELETEME = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin((ctx) => {
let node: ts.Node | undefined;
ts.forEachChild(ctx.sourceFile, function visit(n) {
if (ts.isIdentifier(n) && n.text === 'DELETEME') {
node = n;
}
ts.forEachChild(n, visit);
});
if (!node) {
return {};
}
return {
replacements: [{
generate: () => '',
node
}]
};
}),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).not.toContain('DELETEME');
});
it('replacement at file start', () => {
let code = 'FIRST_TOKEN;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin((ctx) => {
let node: ts.Node | undefined;
ts.forEachChild(ctx.sourceFile, function visit(n) {
if (ts.isIdentifier(n) && n.text === 'FIRST_TOKEN') {
node = n;
}
ts.forEachChild(n, visit);
});
if (!node) {
return {};
}
return {
replacements: [{
generate: () => 'REPLACED_FIRST',
node
}]
};
}),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('REPLACED_FIRST');
expect(result.code.indexOf('REPLACED_FIRST')).toBe(0);
});
// F-TEST-006: Multi-plugin pipeline
it('first plugin modifies code, second receives updated code', () => {
let code = 'let OLD = 1;',
file = parse(code),
program = makeProgram(file),
plugin1 = makePlugin((ctx) => {
let node: ts.Node | undefined;
ts.forEachChild(ctx.sourceFile, function visit(n) {
if (ts.isIdentifier(n) && n.text === 'OLD') {
node = n;
}
ts.forEachChild(n, visit);
});
if (!node) {
return {};
}
return {
replacements: [{
generate: () => 'TRANSFORMED',
node
}]
};
}),
plugin2 = makePlugin((ctx) => {
if (ctx.code.includes('TRANSFORMED')) {
return { prepend: ['const SEEN_BY_PLUGIN2 = true;'] };
}
return {};
}),
result = coordinator.transform([plugin1, plugin2], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('TRANSFORMED');
expect(result.code).toContain('const SEEN_BY_PLUGIN2 = true;');
});
it('three plugins — first skipped, second and third run', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin1: Plugin = {
patterns: ['MISSING'],
transform: () => ({ prepend: ['const SHOULD_NOT_APPEAR = true;'] })
},
plugin2 = makePlugin(() => ({ prepend: ['const A = 1;'] })),
plugin3 = makePlugin(() => ({ prepend: ['const B = 2;'] })),
result = coordinator.transform([plugin1, plugin2, plugin3], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).not.toContain('SHOULD_NOT_APPEAR');
expect(result.code).toContain('const A = 1;');
expect(result.code).toContain('const B = 2;');
});
// F-TEST-009: generate() sourceFile correctness
it('generate() receives correct sourceFile after prior replacement', () => {
let code = "let TARGET = 'hello';",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin((ctx) => {
let node: ts.Node | undefined;
ts.forEachChild(ctx.sourceFile, function visit(n) {
if (ts.isIdentifier(n) && n.text === 'TARGET') {
node = n;
}
ts.forEachChild(n, visit);
});
if (!node) {
return {};
}
return {
replacements: [{
generate: (sf) => `REPLACED_IN_${sf.fileName}`,
node
}]
};
}),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('REPLACED_IN_test.ts');
});
// F-TEST-005: Pattern filtering edge cases
it('pattern in string literal still matches', () => {
let code = 'let x = "reactive(";',
file = parse(code),
program = makeProgram(file),
plugin: Plugin = {
patterns: ['reactive('],
transform: () => ({ prepend: ['const MATCHED = true;'] })
},
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('const MATCHED = true;');
});
it('multiple patterns, only one matches', () => {
let code = 'let PRESENT = 1;',
file = parse(code),
program = makeProgram(file),
plugin: Plugin = {
patterns: ['MISSING', 'PRESENT'],
transform: () => ({ prepend: ['const FOUND = true;'] })
},
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('const FOUND = true;');
});
it('no patterns property — plugin always runs', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({ prepend: ['const ALWAYS = true;'] })),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('const ALWAYS = true;');
});
// F-TEST-010: applyPrepend edge cases
it('no imports — prepend goes to start', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({ prepend: ['const A = 1;'] })),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
let prependIdx = result.code.indexOf('const A = 1;'),
letIdx = result.code.indexOf('let x = 1;');
expect(prependIdx).toBeLessThan(letIdx);
});
it('multiple prepend strings appear in order', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({ prepend: ['const A = 1;', 'const B = 2;'] })),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('const A = 1;');
expect(result.code).toContain('const B = 2;');
let aIdx = result.code.indexOf('const A = 1;'),
bIdx = result.code.indexOf('const B = 2;');
expect(aIdx).toBeLessThan(bIdx);
});
// F-TEST-011: applyImports multi-intent
it('two ImportIntents for different packages', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [
{ add: ['a'], package: 'pkg-1' },
{ add: ['b'], package: 'pkg-2' }
]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain("import { a } from 'pkg-1';");
expect(result.code).toContain("import { b } from 'pkg-2';");
});
it('ImportIntent with only remove', () => {
let code = "import { a, b } from 'pkg';\nlet x = 1;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [{ package: 'pkg', remove: ['a'] }]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('b');
expect(result.code).not.toMatch(/import\s*\{[^}]*a[^}]*\}\s*from\s*'pkg'/);
expect(result.code).toContain("import { b } from 'pkg';");
});
it('propagates plugin.transform() exception', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => { throw new Error('plugin crashed'); });
expect(() => coordinator.transform([plugin], code, file, program, '/root', new Map())).toThrow('plugin crashed');
});
it('falls back to createSourceFile when getSourceFile returns undefined', async () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file);
let languageService = await import('~/compiler/language-service');
vi.mocked(languageService.default.update).mockReturnValueOnce({
getSourceFile: () => undefined,
getTypeChecker: () => ({} as ts.TypeChecker)
} as unknown as ts.Program);
let plugin1 = makePlugin(() => ({ prepend: ['const A = 1;'] })),
plugin2 = makePlugin((ctx) => {
if (ctx.code.includes('const A = 1;')) {
return { prepend: ['const B = 2;'] };
}
return {};
}),
result = coordinator.transform([plugin1, plugin2], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain('const A = 1;');
expect(result.code).toContain('const B = 2;');
});
// F-003: applyImports batching
describe('applyImports batching', () => {
it('batches multiple intents for the same package into one modify call', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [
{ add: ['foo'], package: '@pkg/a' },
{ add: ['bar'], package: '@pkg/a' },
{ add: ['baz'], package: '@pkg/a' }
]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain("import { bar, baz, foo } from '@pkg/a';");
let importMatches = result.code.match(/import\s*\{[^}]+\}\s*from\s*'@pkg\/a'/g);
expect(importMatches).toHaveLength(1);
});
it('re-parses only between distinct packages', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [
{ add: ['foo'], package: '@pkg/a' },
{ add: ['bar'], package: '@pkg/a' },
{ add: ['qux'], package: '@pkg/b' }
]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain("import { bar, foo } from '@pkg/a';");
expect(result.code).toContain("import { qux } from '@pkg/b';");
});
it('merges add and remove for same package', () => {
let code = "import { bar, foo } from '@pkg/a';\nlet x = 1;",
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [
{ add: ['baz'], package: '@pkg/a' },
{ package: '@pkg/a', remove: ['bar'] }
]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain("import { baz, foo } from '@pkg/a';");
expect(result.code).not.toMatch(/import\s*\{[^}]*bar[^}]*\}\s*from\s*'@pkg\/a'/);
});
it('preserves namespace across merged intents', () => {
let code = 'let x = 1;',
file = parse(code),
program = makeProgram(file),
plugin = makePlugin(() => ({
imports: [
{ namespace: 'utils', package: '@pkg/a' },
{ add: ['foo'], package: '@pkg/a' }
]
})),
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
expect(result.changed).toBe(true);
expect(result.code).toContain("import * as utils from '@pkg/a';");
expect(result.code).toContain("import { foo } from '@pkg/a';");
});
});
});
import { describe, expect, it } from 'vitest';
import ts from 'typescript';
import imports from '~/compiler/imports';
function parse(code: string, fileName = 'test.ts'): ts.SourceFile {
return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
}
describe('imports.all', () => {
it('finds named imports from a package', () => {
let file = parse("import { foo, bar } from 'my-pkg';"),
result = imports.all(file, 'my-pkg');
expect(result).toHaveLength(1);
expect(result[0].specifiers.get('foo')).toBe('foo');
expect(result[0].specifiers.get('bar')).toBe('bar');
});
it('handles aliased imports', () => {
let file = parse("import { foo as f } from 'my-pkg';"),
result = imports.all(file, 'my-pkg');
expect(result).toHaveLength(1);
expect(result[0].specifiers.get('foo')).toBe('f');
});
it('returns empty for non-matching package', () => {
let file = parse("import { foo } from 'other-pkg';"),
result = imports.all(file, 'my-pkg');
expect(result).toHaveLength(0);
});
it('returns empty when no imports', () => {
let file = parse('let x = 1;'),
result = imports.all(file, 'my-pkg');
expect(result).toHaveLength(0);
});
it('finds multiple import statements for same package', () => {
let file = parse("import { a } from 'pkg';\nimport { b } from 'pkg';"),
result = imports.all(file, 'pkg');
expect(result).toHaveLength(2);
});
it('tracks start and end positions', () => {
let file = parse("import { foo } from 'my-pkg';"),
result = imports.all(file, 'my-pkg');
expect(result[0].start).toBe(0);
expect(result[0].end).toBeGreaterThan(0);
});
it('handles default import (no named bindings)', () => {
let file = parse("import pkg from 'my-pkg';"),
result = imports.all(file, 'my-pkg');
expect(result).toHaveLength(1);
expect(result[0].specifiers.size).toBe(0);
});
it('handles namespace import', () => {
let file = parse("import * as pkg from 'my-pkg';"),
result = imports.all(file, 'my-pkg');
expect(result).toHaveLength(1);
expect(result[0].specifiers.size).toBe(0);
});
});
describe('imports.includes', () => {
let mockChecker = { getSymbolAtLocation: () => null } as unknown as ts.TypeChecker;
function findIdentifier(file: ts.SourceFile, name: string): ts.Identifier | undefined {
let found: ts.Identifier | undefined;
ts.forEachChild(file, function visit(n) {
if (ts.isIdentifier(n) && n.text === name && !found) {
let parent = n.parent;
if (!ts.isImportSpecifier(parent) && !ts.isImportClause(parent) && !ts.isNamespaceImport(parent)) {
found = n;
}
}
ts.forEachChild(n, visit);
});
return found;
}
it('direct named import matches', () => {
let file = parse("import { reactive } from 'my-pkg';\nreactive(x);"),
node = findIdentifier(file, 'reactive');
expect(node).toBeDefined();
expect(imports.includes(mockChecker, node!, 'my-pkg', 'reactive')).toBe(true);
});
it('aliased import matches', () => {
let file = parse("import { foo as f } from 'my-pkg';\nf();"),
node = findIdentifier(file, 'f');
expect(node).toBeDefined();
expect(imports.includes(mockChecker, node!, 'my-pkg')).toBe(true);
});
it('non-matching package returns false', () => {
let file = parse("import { foo } from 'other-pkg';\nfoo();"),
node = findIdentifier(file, 'foo');
expect(node).toBeDefined();
expect(imports.includes(mockChecker, node!, 'my-pkg')).toBe(false);
});
it('non-matching symbol name returns false', () => {
let file = parse("import { foo } from 'my-pkg';\nfoo();"),
node = findIdentifier(file, 'foo');
expect(node).toBeDefined();
expect(imports.includes(mockChecker, node!, 'my-pkg', 'bar')).toBe(false);
});
it('cache returns consistent results', () => {
let file = parse("import { reactive } from 'my-pkg';\nreactive(1);"),
node = findIdentifier(file, 'reactive');
expect(node).toBeDefined();
let first = imports.includes(mockChecker, node!, 'my-pkg', 'reactive'),
second = imports.includes(mockChecker, node!, 'my-pkg', 'reactive');
expect(first).toBe(true);
expect(second).toBe(true);
expect(first).toBe(second);
});
it('no imports at all returns false', () => {
let file = parse('let x = 1;\nx;'),
node = findIdentifier(file, 'x');
expect(node).toBeDefined();
expect(imports.includes(mockChecker, node!, 'my-pkg')).toBe(false);
});
it('returns false when getAliasedSymbol throws', () => {
let file = parse("import { foo } from 'my-pkg';\nbar();"),
node = findIdentifier(file, 'bar');
expect(node).toBeDefined();
let checker = {
getSymbolAtLocation: () => ({
getDeclarations: () => []
}),
getAliasedSymbol: () => { throw new Error('not an alias'); }
} as unknown as ts.TypeChecker;
expect(imports.includes(checker, node!, 'my-pkg')).toBe(false);
});
});
import { describe, expect, it } from 'vitest';
import languageService from '~/compiler/language-service';
describe('language-service', () => {
describe('update', () => {
it('returns a Program when given valid root + fileName + content', () => {
let root = process.cwd().replace(/\\/g, '/'),
fileName = root + '/test-virtual-update.ts',
content = 'let x: number = 42;',
program = languageService.update(root, fileName, content);
expect(program).toBeDefined();
expect(program.getTypeChecker).toBeDefined();
});
it('updated content is reflected in the program SourceFile', () => {
let root = process.cwd().replace(/\\/g, '/'),
fileName = root + '/test-virtual-reflect.ts',
content = 'let hello = "world";',
program = languageService.update(root, fileName, content),
sourceFile = program.getSourceFile(fileName);
expect(sourceFile).toBeDefined();
expect(sourceFile!.text).toBe(content);
});
it('increments version for updated files', () => {
let root = process.cwd().replace(/\\/g, '/'),
fileName = root + '/test-virtual-version.ts';
languageService.update(root, fileName, 'let a = 1;');
let program = languageService.update(root, fileName, 'let a = 2;'),
sourceFile = program.getSourceFile(fileName);
expect(sourceFile).toBeDefined();
expect(sourceFile!.text).toBe('let a = 2;');
});
it('adds new files to rootFiles', () => {
let root = process.cwd().replace(/\\/g, '/'),
fileName = root + '/test-virtual-new-root.ts',
content = 'export const value = 1;',
program = languageService.update(root, fileName, content),
sourceFile = program.getSourceFile(fileName);
expect(sourceFile).toBeDefined();
expect(sourceFile!.text).toBe(content);
});
});
describe('invalidate', () => {
it('removes content so next getProgram reads from disk', () => {
let root = process.cwd().replace(/\\/g, '/'),
fileName = root + '/test-virtual-invalidate.ts',
content = 'let val = 99;';
languageService.update(root, fileName, content);
languageService.invalidate(root, fileName);
let program = languageService.update(root, fileName, 'let val = 100;'),
sourceFile = program.getSourceFile(fileName);
expect(sourceFile).toBeDefined();
expect(sourceFile!.text).toBe('let val = 100;');
});
it('increments version for invalidated files', () => {
let root = process.cwd().replace(/\\/g, '/'),
fileName = root + '/test-virtual-inv-version.ts';
languageService.update(root, fileName, 'let a = 1;');
languageService.invalidate(root, fileName);
let program = languageService.update(root, fileName, 'let a = 3;'),
sourceFile = program.getSourceFile(fileName);
expect(sourceFile).toBeDefined();
expect(sourceFile!.text).toBe('let a = 3;');
});
it('no-op when root does not exist in cache', () => {
expect(() => {
languageService.invalidate('/nonexistent/root', 'file.ts');
}).not.toThrow();
});
});
});
import { beforeEach, describe, expect, it, vi } from 'vitest';
import ts from 'typescript';
import type { Plugin } from '~/compiler/types';
import tsc from '~/compiler/plugins/tsc';
import vite from '~/compiler/plugins/vite';
vi.mock('~/compiler/language-service', () => ({
default: {
invalidate: vi.fn(),
update: vi.fn((_root: string, fileName: string, content: string) => {
let file = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
return {
getSourceFile: () => file,
getTypeChecker: () => ({} as ts.TypeChecker)
} as unknown as ts.Program;
})
}
}));
vi.mock('~/compiler/coordinator', () => ({
default: {
transform: vi.fn((_plugins: Plugin[], code: string, _file: ts.SourceFile, _prog: ts.Program, _root: string, _ctx: Map<string, unknown>) => ({
changed: false,
code,
sourceFile: {} as ts.SourceFile
}))
}
}));
import coordinator from '~/compiler/coordinator';
import languageService from '~/compiler/language-service';
describe('plugin.tsc', () => {
it('returns a function that returns the plugins array', () => {
let p1: Plugin = { transform: () => ({}) },
p2: Plugin = { transform: () => ({}) },
factory = tsc([p1, p2]),
result = factory();
expect(result).toEqual([p1, p2]);
});
it('returns empty array for empty input', () => {
let result = tsc([])();
expect(result).toEqual([]);
});
});
describe('plugin.vite', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('creates VitePlugin with correct shape', () => {
let factory = vite({ name: 'test-pkg', plugins: [] }),
plugin = factory();
expect(plugin).toHaveProperty('configResolved');
expect(plugin).toHaveProperty('enforce');
expect(plugin).toHaveProperty('name');
expect(plugin).toHaveProperty('transform');
expect(plugin).toHaveProperty('watchChange');
});
it('name includes package name', () => {
let plugin = vite({ name: 'test-pkg', plugins: [] })();
expect(plugin.name).toBe('test-pkg/compiler/vite');
});
it('filters non-ts files', () => {
let plugin = vite({ name: 'test-pkg', plugins: [] })();
expect(plugin.transform('code', 'file.css')).toBeNull();
});
it('filters node_modules', () => {
let plugin = vite({ name: 'test-pkg', plugins: [] })();
expect(plugin.transform('code', 'node_modules/pkg/index.ts')).toBeNull();
});
it('processes .ts files — returns null when unchanged', () => {
let plugin = vite({ name: 'test-pkg', plugins: [] })();
let result = plugin.transform('let x = 1;', 'src/app.ts');
expect(result).toBeNull();
expect(coordinator.transform).toHaveBeenCalled();
});
it('returns transformed code when changed', () => {
vi.mocked(coordinator.transform).mockReturnValueOnce({
changed: true,
code: 'TRANSFORMED',
sourceFile: {} as ts.SourceFile
});
let plugin = vite({ name: 'test-pkg', plugins: [] })();
let result = plugin.transform('let x = 1;', 'src/app.ts');
expect(result).toEqual({ code: 'TRANSFORMED', map: null });
});
it('watchChange calls onWatchChange and invalidate', () => {
let onWatchChange = vi.fn(),
plugin = vite({ name: 'test-pkg', onWatchChange, plugins: [] })();
plugin.watchChange('src/app.ts');
expect(onWatchChange).toHaveBeenCalled();
expect(languageService.invalidate).toHaveBeenCalledWith('', 'src/app.ts');
});
it('watchChange ignores non-ts files', () => {
let onWatchChange = vi.fn(),
plugin = vite({ name: 'test-pkg', onWatchChange, plugins: [] })();
plugin.watchChange('style.css');
expect(onWatchChange).not.toHaveBeenCalled();
});
it('configResolved sets root', () => {
let plugin = vite({ name: 'test-pkg', plugins: [] })();
plugin.configResolved({ root: '/my/root' });
plugin.transform('let x = 1;', 'src/app.ts');
expect(languageService.update).toHaveBeenCalledWith('/my/root', expect.any(String), expect.any(String));
});
it('catches coordinator.transform() error and returns null', () => {
vi.mocked(coordinator.transform).mockImplementationOnce(() => {
throw new Error('transform failed');
});
let consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}),
plugin = vite({ name: 'test-pkg', plugins: [] })();
let result = plugin.transform('let x = 1;', 'src/app.ts');
expect(result).toBeNull();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('test-pkg'),
expect.any(Error)
);
consoleSpy.mockRestore();
});
it('falls back to createSourceFile when getSourceFile returns undefined', () => {
vi.mocked(languageService.update).mockReturnValueOnce({
getSourceFile: () => undefined,
getTypeChecker: () => ({} as ts.TypeChecker)
} as unknown as ts.Program);
let plugin = vite({ name: 'test-pkg', plugins: [] })();
let result = plugin.transform('let x = 1;', 'src/app.ts');
expect(result).toBeNull();
expect(coordinator.transform).toHaveBeenCalled();
});
});
import { describe, expect, it } from 'vitest';
import uid from '~/compiler/uid';
describe('uid', () => {
it('generates unique ids', () => {
let a = uid('test'),
b = uid('test');
expect(a).not.toBe(b);
});
it('prefixes with given name', () => {
let result = uid('myPrefix');
expect(result.startsWith('myPrefix_')).toBe(true);
});
it('contains only alphanumeric characters after prefix', () => {
let result = uid('x'),
suffix = result.slice(2); // after 'x_'
expect(suffix).toMatch(/^[A-Za-z0-9]+$/);
});
// F-TEST-008: uid sequential guarantees
it('sequential calls produce different suffixes (5 calls)', () => {
let results = new Set<string>();
for (let i = 0; i < 5; i++) {
results.add(uid('x'));
}
expect(results.size).toBe(5);
});
it('different prefixes share same namespace', () => {
let a1 = uid('a'),
a2 = uid('a'),
b1 = uid('b'),
suffixA1 = a1.slice(2), // after 'a_'
suffixA2 = a2.slice(2),
suffixB1 = b1.slice(2); // after 'b_'
// Find common prefix between two 'a' calls — that's the namespace
let common = '';
for (let i = 0, n = Math.min(suffixA1.length, suffixA2.length); i < n; i++) {
if (suffixA1[i] !== suffixA2[i]) {
break;
}
common += suffixA1[i];
}
expect(common.length).toBeGreaterThan(0);
expect(suffixB1.startsWith(common)).toBe(true);
});
it('suffix contains valid base-36 characters', () => {
let result = uid('z'),
parts = result.slice(2), // after 'z_'
base36Suffix = parts.match(/[0-9a-z]+$/);
expect(base36Suffix).not.toBeNull();
expect(base36Suffix![0]).toMatch(/^[0-9a-z]+$/);
});
});
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'~': path.resolve(__dirname, 'src')
}
},
test: {
include: ['tests/**/*.test.ts']
}
});
+1
-1

@@ -43,2 +43,2 @@ name: publish to npm

- run: pnpm build
- run: pnpm publish --provenance --no-git-checks
- run: pnpm publish --no-git-checks

@@ -1,1 +0,10 @@

export {};
import type { Plugin } from '../compiler/types.js';
type PluginConfig = {
transform: string;
};
declare function build(config: object, tsconfig: string, pluginConfigs: PluginConfig[]): Promise<void>;
declare function isPlugin(value: unknown): value is Plugin;
declare function loadPlugins(configs: PluginConfig[], root: string): Promise<Plugin[]>;
declare function normalizePath(fileName: string): string;
declare function runTscAlias(args: string[]): Promise<number>;
export { build, isPlugin, loadPlugins, normalizePath, runTscAlias };

@@ -1,8 +0,8 @@

import { spawn } from 'child_process';
import { createRequire } from 'module';
import { PACKAGE_NAME } from '../constants.js';
import { pathToFileURL } from 'url';
import { spawn } from 'child_process';
import coordinator from '../compiler/coordinator.js';
import path from 'path';
import ts from 'typescript';
import coordinator from '../compiler/coordinator.js';
import { PACKAGE_NAME } from '../constants.js';
const BACKSLASH_REGEX = /\\/g;

@@ -140,2 +140,5 @@ let require = createRequire(import.meta.url), skipFlags = new Set(['--help', '--init', '--noEmit', '--showConfig', '--version', '-h', '-noEmit', '-v']);

}
main();
if (process.env.VITEST === undefined) {
main();
}
export { build, isPlugin, loadPlugins, normalizePath, runTscAlias };

@@ -1,6 +0,3 @@

import ts from 'typescript';
type Range = {
end: number;
start: number;
};
import type { Range } from './types.js';
import { ts } from '../index.js';
declare const _default: {

@@ -7,0 +4,0 @@ expression: {

@@ -1,2 +0,2 @@

import ts from 'typescript';
import { ts } from '../index.js';
const expression = {

@@ -3,0 +3,0 @@ name: (node) => {

@@ -5,9 +5,27 @@ import { ts } from '../index.js';

function applyImports(code, file, intents) {
let merged = new Map();
for (let i = 0, n = intents.length; i < n; i++) {
let intent = intents[i];
code = modify(code, file, intent.package, {
add: intent.add,
namespace: intent.namespace,
remove: intent.remove
});
let intent = intents[i], existing = merged.get(intent.package);
if (existing) {
if (intent.add) {
(existing.add ??= []).push(...intent.add);
}
if (intent.namespace) {
existing.namespace = intent.namespace;
}
if (intent.remove) {
(existing.remove ??= []).push(...intent.remove);
}
}
else {
merged.set(intent.package, {
add: intent.add ? [...intent.add] : undefined,
namespace: intent.namespace,
remove: intent.remove ? [...intent.remove] : undefined
});
}
}
let keys = [...merged.keys()];
for (let i = 0, n = keys.length; i < n; i++) {
code = modify(code, file, keys[i], merged.get(keys[i]));
if (i < n - 1) {

@@ -44,5 +62,5 @@ file = ts.createSourceFile(file.fileName, code, file.languageVersion, true);

if (position === 0) {
return prepend.join('\n') + code;
return prepend.join('\n') + '\n' + code;
}
return code.slice(0, position) + prepend.join('\n') + code.slice(position);
return code.slice(0, position) + '\n' + prepend.join('\n') + '\n' + code.slice(position);
}

@@ -110,8 +128,15 @@ function hasPattern(code, patterns) {

replacements.sort((a, b) => b.start - a.start);
let result = code;
let parts = [], pos = code.length;
for (let i = 0, n = replacements.length; i < n; i++) {
let r = replacements[i];
result = result.substring(0, r.start) + r.newText + result.substring(r.end);
if (r.end < pos) {
parts.push(code.substring(r.end, pos));
}
parts.push(r.newText);
pos = r.start;
}
return result;
if (pos > 0) {
parts.push(code.substring(0, pos));
}
return parts.reverse().join('');
}

@@ -140,2 +165,5 @@ const transform = (plugins, code, file, program, root, shared) => {

if (prepend?.length) {
if (pluginChanged) {
currentFile = ts.createSourceFile(fileName, currentCode, file.languageVersion, true);
}
currentCode = applyPrepend(currentCode, currentFile, prepend);

@@ -145,2 +173,5 @@ pluginChanged = true;

if (imports?.length) {
if (pluginChanged) {
currentFile = ts.createSourceFile(fileName, currentCode, file.languageVersion, true);
}
currentCode = applyImports(currentCode, currentFile, imports);

@@ -147,0 +178,0 @@ pluginChanged = true;

import { ts } from '../index.js';
let cache = new WeakMap();
function fileNameMatchesPackage(fileName, pkg) {
let normalized = fileName.replace(/\\/g, '/'), marker = `/node_modules/${pkg}/`;
return normalized.includes(marker);
}
const all = (file, pkg) => {

@@ -63,3 +67,3 @@ let imports = [];

}
if (decl.getSourceFile().fileName.includes(pkg)) {
if (fileNameMatchesPackage(decl.getSourceFile().fileName, pkg)) {
return true;

@@ -80,3 +84,3 @@ }

let decl = declarations[i];
if (decl.getSourceFile().fileName.includes(pkg)) {
if (fileNameMatchesPackage(decl.getSourceFile().fileName, pkg)) {
return true;

@@ -92,3 +96,3 @@ }

for (let i = 0, n = aliasedDecls.length; i < n; i++) {
if (aliasedDecls[i].getSourceFile().fileName.includes(pkg)) {
if (fileNameMatchesPackage(aliasedDecls[i].getSourceFile().fileName, pkg)) {
return true;

@@ -95,0 +99,0 @@ }

@@ -1,8 +0,5 @@

import ts from 'typescript';
declare const get: (root: string) => ts.Program;
import { ts } from '../index.js';
declare const invalidate: (root: string, fileName: string) => void;
declare const update: (root: string, fileName: string, content: string) => ts.Program;
declare const _default: {
delete: (root: string) => void;
get: (root: string) => ts.Program;
invalidate: (root: string, fileName: string) => void;

@@ -12,2 +9,2 @@ update: (root: string, fileName: string, content: string) => ts.Program;

export default _default;
export { get, invalidate, update };
export { invalidate, update };

@@ -0,4 +1,4 @@

import { PACKAGE_NAME } from '../constants.js';
import { ts } from '../index.js';
import path from 'path';
import ts from 'typescript';
import { PACKAGE_NAME } from '../constants.js';
let cache = new Map();

@@ -51,12 +51,2 @@ function create(root) {

}
const del = (root) => {
cache.delete(root);
};
const get = (root) => {
let entry = getEntry(root), program = entry.service.getProgram();
if (!program) {
throw new Error(`${PACKAGE_NAME}: failed to get program from language service`);
}
return program;
};
const invalidate = (root, fileName) => {

@@ -83,3 +73,3 @@ let entry = cache.get(root);

};
export default { delete: del, get, invalidate, update };
export { get, invalidate, update };
export default { invalidate, update };
export { invalidate, update };

@@ -22,5 +22,10 @@ import { ts } from '../../index.js';

if (!sourceFile) {
sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
sourceFile = ts.createSourceFile(normalizedId, code, ts.ScriptTarget.Latest, true);
}
let result = coordinator.transform(plugins, code, sourceFile, prog, root || '', contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || ''));
let key = root || '', ctx = contexts.get(key);
if (!ctx) {
ctx = new Map();
contexts.set(key, ctx);
}
let result = coordinator.transform(plugins, code, sourceFile, prog, key, ctx);
if (!result.changed) {

@@ -27,0 +32,0 @@ return null;

@@ -10,10 +10,11 @@ {

"dependencies": {
"@esportsplus/cli-passthrough": "^0.0.12",
"@esportsplus/utilities": "^0.27.2",
"@types/node": "^25.0.3",
"@esportsplus/cli-passthrough": "^0.0.15",
"@esportsplus/utilities": "^0.27.3",
"@types/node": "^25.6.0",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3"
"typescript": "^6.0.2"
},
"devDependencies": {
"vite": "^6.0.7"
"vite": "^8.0.8",
"vitest": "^4.1.4"
},

@@ -43,7 +44,9 @@ "exports": {

"types": "build/index.d.ts",
"version": "0.29.0",
"version": "0.29.1",
"scripts": {
"bench:run": "vitest bench --run",
"build": "tsc && tsc-alias",
"-": "-"
"-": "-",
"test": "vitest run"
}
}

@@ -1,13 +0,13 @@

import { spawn } from 'child_process';
import type { Plugin, SharedContext } from '~/compiler/types';
import { createRequire } from 'module';
import { PACKAGE_NAME } from '~/constants';
import { pathToFileURL } from 'url';
import { spawn } from 'child_process';
import coordinator from '~/compiler/coordinator';
import path from 'path';
import ts from 'typescript';
import coordinator from '~/compiler/coordinator';
import type { Plugin, SharedContext } from '~/compiler/types';
import { PACKAGE_NAME } from '~/constants';
type PluginConfig = {
after?: boolean;
transform: string;

@@ -230,2 +230,7 @@ };

main();
if (process.env.VITEST === undefined) {
main();
}
export { build, isPlugin, loadPlugins, normalizePath, runTscAlias };

@@ -1,10 +0,5 @@

import ts from 'typescript';
import type { Range } from './types';
import { ts } from '~/index';
type Range = {
end: number;
start: number;
};
const expression = {

@@ -11,0 +6,0 @@ name: (node: ts.Expression): string | null => {

import type { ImportIntent, Plugin, Replacement, ReplacementIntent, SharedContext } from './types';
import type { ModifyOptions } from './imports';
import { ts } from '~/index';
import imports, { ModifyOptions } from './imports';
import imports from './imports';
import languageService from './language-service';

@@ -15,18 +17,35 @@

function applyImports(code: string, file: ts.SourceFile, intents: ImportIntent[]): string {
let merged = new Map<string, { add?: string[]; namespace?: string; remove?: string[] }>();
for (let i = 0, n = intents.length; i < n; i++) {
let intent = intents[i];
let intent = intents[i],
existing = merged.get(intent.package);
code = modify(code, file, intent.package, {
add: intent.add,
namespace: intent.namespace,
remove: intent.remove
});
if (existing) {
if (intent.add) {
(existing.add ??= []).push(...intent.add);
}
if (intent.namespace) {
existing.namespace = intent.namespace;
}
if (intent.remove) {
(existing.remove ??= []).push(...intent.remove);
}
}
else {
merged.set(intent.package, {
add: intent.add ? [...intent.add] : undefined,
namespace: intent.namespace,
remove: intent.remove ? [...intent.remove] : undefined
});
}
}
let keys = [...merged.keys()];
for (let i = 0, n = keys.length; i < n; i++) {
code = modify(code, file, keys[i], merged.get(keys[i])!);
if (i < n - 1) {
file = ts.createSourceFile(
file.fileName,
code,
file.languageVersion,
true
);
file = ts.createSourceFile(file.fileName, code, file.languageVersion, true);
}

@@ -72,6 +91,6 @@ }

if (position === 0) {
return prepend.join('\n') + code;
return prepend.join('\n') + '\n' + code;
}
return code.slice(0, position) + prepend.join('\n') + code.slice(position);
return code.slice(0, position) + '\n' + prepend.join('\n') + '\n' + code.slice(position);
}

@@ -163,3 +182,4 @@

let result = code;
let parts: string[] = [],
pos = code.length;

@@ -169,6 +189,15 @@ for (let i = 0, n = replacements.length; i < n; i++) {

result = result.substring(0, r.start) + r.newText + result.substring(r.end);
if (r.end < pos) {
parts.push(code.substring(r.end, pos));
}
parts.push(r.newText);
pos = r.start;
}
return result;
if (pos > 0) {
parts.push(code.substring(0, pos));
}
return parts.reverse().join('');
}

@@ -218,2 +247,6 @@

if (prepend?.length) {
if (pluginChanged) {
currentFile = ts.createSourceFile(fileName, currentCode, file.languageVersion, true);
}
currentCode = applyPrepend(currentCode, currentFile, prepend);

@@ -224,2 +257,6 @@ pluginChanged = true;

if (imports?.length) {
if (pluginChanged) {
currentFile = ts.createSourceFile(fileName, currentCode, file.languageVersion, true);
}
currentCode = applyImports(currentCode, currentFile, imports);

@@ -226,0 +263,0 @@ pluginChanged = true;

@@ -20,2 +20,10 @@ import { ts } from '~/index';

function fileNameMatchesPackage(fileName: string, pkg: string): boolean {
let normalized = fileName.replace(/\\/g, '/'),
marker = `/node_modules/${pkg}/`;
return normalized.includes(marker);
}
// Find all named imports from a specific package

@@ -112,3 +120,3 @@ const all = (file: ts.SourceFile, pkg: string): ImportInfo[] => {

if (decl.getSourceFile().fileName.includes(pkg)) {
if (fileNameMatchesPackage(decl.getSourceFile().fileName, pkg)) {
return true;

@@ -138,3 +146,3 @@ }

if (decl.getSourceFile().fileName.includes(pkg)) {
if (fileNameMatchesPackage(decl.getSourceFile().fileName, pkg)) {
return true;

@@ -154,3 +162,3 @@ }

for (let i = 0, n = aliasedDecls.length; i < n; i++) {
if (aliasedDecls[i].getSourceFile().fileName.includes(pkg)) {
if (fileNameMatchesPackage(aliasedDecls[i].getSourceFile().fileName, pkg)) {
return true;

@@ -157,0 +165,0 @@ }

@@ -1,6 +0,7 @@

import path from 'path';
import ts from 'typescript';
import { PACKAGE_NAME } from '~/constants';
import { ts } from '~/index';
import path from 'path';
type LanguageServiceEntry = {

@@ -87,17 +88,2 @@ contents: Map<string, string>;

const del = (root: string): void => {
cache.delete(root);
};
const get = (root: string): ts.Program => {
let entry = getEntry(root),
program = entry.service.getProgram();
if (!program) {
throw new Error(`${PACKAGE_NAME}: failed to get program from language service`);
}
return program;
};
const invalidate = (root: string, fileName: string): void => {

@@ -135,3 +121,3 @@ let entry = cache.get(root);

export default { delete: del, get, invalidate, update };
export { get, invalidate, update };
export default { invalidate, update };
export { invalidate, update };

@@ -0,4 +1,5 @@

import type { Plugin, SharedContext } from '../types';
import type { ResolvedConfig } from 'vite';
import type { Plugin, SharedContext } from '../types';
import { ts } from '~/index';
import coordinator from '../coordinator';

@@ -50,5 +51,13 @@ import languageService from '../language-service';

if (!sourceFile) {
sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
sourceFile = ts.createSourceFile(normalizedId, code, ts.ScriptTarget.Latest, true);
}
let key = root || '',
ctx = contexts.get(key);
if (!ctx) {
ctx = new Map();
contexts.set(key, ctx);
}
let result = coordinator.transform(

@@ -59,4 +68,4 @@ plugins,

prog,
root || '',
contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || '')!
key,
ctx
);

@@ -63,0 +72,0 @@

@@ -6,3 +6,2 @@ {

"allowSyntheticDefaultImports": true,
"baseUrl": "${configDir}",
"declaration": false,

@@ -9,0 +8,0 @@ "esModuleInterop": true,