@esportsplus/typescript 0.29.0 → 0.29.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/code-audit/registry-typescript.json +326 -0
- package/.github/workflows/bump.yml +1 -1
- package/.github/workflows/dependabot.yml +4 -4
- package/.github/workflows/publish.yml +4 -5
- package/README.md +146 -0
- package/build/cli/tsc.d.ts +10 -1
- package/build/cli/tsc.js +7 -4
- package/build/compiler/ast.d.ts +2 -5
- package/build/compiler/ast.js +1 -1
- package/build/compiler/coordinator.js +42 -11
- package/build/compiler/imports.js +7 -3
- package/build/compiler/language-service.d.ts +2 -5
- package/build/compiler/language-service.js +4 -14
- package/build/compiler/plugins/vite.js +7 -2
- package/package.json +14 -8
- package/src/cli/tsc.ts +11 -6
- package/src/compiler/ast.ts +2 -7
- package/src/compiler/coordinator.ts +55 -18
- package/src/compiler/imports.ts +11 -3
- package/src/compiler/language-service.ts +5 -19
- package/src/compiler/plugins/vite.ts +13 -4
- package/tests/cli/tsc.test.ts +155 -0
- package/tests/compiler/ast.test.ts +131 -0
- package/tests/compiler/code.test.ts +73 -0
- package/tests/compiler/coordinator.bench.ts +101 -0
- package/tests/compiler/coordinator.test.ts +855 -0
- package/tests/compiler/imports.test.ts +167 -0
- package/tests/compiler/language-service.test.ts +90 -0
- package/tests/compiler/plugins.test.ts +172 -0
- package/tests/compiler/uid.test.ts +70 -0
- package/tsconfig.base.json +0 -1
- package/tsconfig.node.json +3 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
import ast from '~/compiler/ast';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function parse(code: string): ts.SourceFile {
|
|
8
|
+
return ts.createSourceFile('test.ts', code, ts.ScriptTarget.Latest, true);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function findFirst(file: ts.SourceFile, predicate: (n: ts.Node) => boolean): ts.Node | undefined {
|
|
12
|
+
let result: ts.Node | undefined;
|
|
13
|
+
|
|
14
|
+
function visit(node: ts.Node) {
|
|
15
|
+
if (result) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (predicate(node)) {
|
|
20
|
+
result = node;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ts.forEachChild(node, visit);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ts.forEachChild(file, visit);
|
|
28
|
+
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
describe('ast.expression.name', () => {
|
|
34
|
+
it('returns text for identifiers', () => {
|
|
35
|
+
let file = parse('let x = foo;'),
|
|
36
|
+
node = findFirst(file, ts.isIdentifier);
|
|
37
|
+
|
|
38
|
+
expect(node).toBeDefined();
|
|
39
|
+
expect(ast.expression.name(node as ts.Expression)).toBe('x');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns dotted path for property access', () => {
|
|
43
|
+
let file = parse('a.b.c;'),
|
|
44
|
+
node = findFirst(file, ts.isPropertyAccessExpression);
|
|
45
|
+
|
|
46
|
+
expect(node).toBeDefined();
|
|
47
|
+
expect(ast.expression.name(node as ts.Expression)).toBe('a.b.c');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns null for unsupported expressions', () => {
|
|
51
|
+
let file = parse('foo();'),
|
|
52
|
+
node = findFirst(file, ts.isCallExpression);
|
|
53
|
+
|
|
54
|
+
expect(node).toBeDefined();
|
|
55
|
+
expect(ast.expression.name(node as ts.Expression)).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
describe('ast.inRange', () => {
|
|
61
|
+
it('returns true when node is within range', () => {
|
|
62
|
+
expect(ast.inRange([{ start: 0, end: 100 }], 10, 50)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns true at exact boundaries', () => {
|
|
66
|
+
expect(ast.inRange([{ start: 10, end: 50 }], 10, 50)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns false when node is outside range', () => {
|
|
70
|
+
expect(ast.inRange([{ start: 10, end: 50 }], 0, 9)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns false when node partially overlaps', () => {
|
|
74
|
+
expect(ast.inRange([{ start: 10, end: 50 }], 5, 30)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns false for empty ranges', () => {
|
|
78
|
+
expect(ast.inRange([], 0, 10)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('checks multiple ranges', () => {
|
|
82
|
+
let ranges = [{ start: 0, end: 10 }, { start: 20, end: 30 }];
|
|
83
|
+
|
|
84
|
+
expect(ast.inRange(ranges, 0, 10)).toBe(true);
|
|
85
|
+
expect(ast.inRange(ranges, 25, 28)).toBe(true);
|
|
86
|
+
expect(ast.inRange(ranges, 11, 19)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
describe('ast.property.path', () => {
|
|
92
|
+
it('returns dotted path for nested access', () => {
|
|
93
|
+
let file = parse('a.b.c.d;'),
|
|
94
|
+
node = findFirst(file, (n) => ts.isPropertyAccessExpression(n) && !ts.isPropertyAccessExpression(n.parent));
|
|
95
|
+
|
|
96
|
+
expect(node).toBeDefined();
|
|
97
|
+
expect(ast.property.path(node as ts.Expression)).toBe('a.b.c.d');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns null for non-identifier base', () => {
|
|
101
|
+
let file = parse('foo().bar;'),
|
|
102
|
+
node = findFirst(file, ts.isPropertyAccessExpression);
|
|
103
|
+
|
|
104
|
+
expect(node).toBeDefined();
|
|
105
|
+
expect(ast.property.path(node as ts.Expression)).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
describe('ast.test', () => {
|
|
111
|
+
it('returns true when predicate matches root', () => {
|
|
112
|
+
let file = parse('let x = 1;'),
|
|
113
|
+
found = ast.test(file, (n) => ts.isVariableStatement(n));
|
|
114
|
+
|
|
115
|
+
expect(found).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns true when predicate matches deep child', () => {
|
|
119
|
+
let file = parse('function f() { return { a: 1 }; }'),
|
|
120
|
+
found = ast.test(file, (n) => ts.isObjectLiteralExpression(n));
|
|
121
|
+
|
|
122
|
+
expect(found).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('returns false when predicate never matches', () => {
|
|
126
|
+
let file = parse('let x = 1;'),
|
|
127
|
+
found = ast.test(file, (n) => ts.isClassDeclaration(n));
|
|
128
|
+
|
|
129
|
+
expect(found).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import code from '~/compiler/code';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('code', () => {
|
|
7
|
+
it('interpolates values into template', () => {
|
|
8
|
+
let result = code`let x = ${'hello'};`;
|
|
9
|
+
|
|
10
|
+
expect(result).toBe("let x = hello;");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('handles multiple interpolations', () => {
|
|
14
|
+
let result = code`${'a'} + ${'b'} = ${'c'}`;
|
|
15
|
+
|
|
16
|
+
expect(result).toBe('a + b = c');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('collapses null to empty string', () => {
|
|
20
|
+
let result = code`x${null}y`;
|
|
21
|
+
|
|
22
|
+
expect(result).toBe('xy');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('collapses undefined to empty string', () => {
|
|
26
|
+
let result = code`x${undefined}y`;
|
|
27
|
+
|
|
28
|
+
expect(result).toBe('xy');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('collapses false to empty string', () => {
|
|
32
|
+
let result = code`x${false}y`;
|
|
33
|
+
|
|
34
|
+
expect(result).toBe('xy');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('preserves zero', () => {
|
|
38
|
+
let result = code`x${0}y`;
|
|
39
|
+
|
|
40
|
+
expect(result).toBe('x0y');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('preserves empty string', () => {
|
|
44
|
+
let result = code`x${''}y`;
|
|
45
|
+
|
|
46
|
+
expect(result).toBe('xy');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles no interpolations', () => {
|
|
50
|
+
let result = code`just plain text`;
|
|
51
|
+
|
|
52
|
+
expect(result).toBe('just plain text');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
describe('code.escape', () => {
|
|
58
|
+
it('escapes single quotes', () => {
|
|
59
|
+
expect(code.escape("it's")).toBe("it\\'s");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('escapes multiple quotes', () => {
|
|
63
|
+
expect(code.escape("a'b'c")).toBe("a\\'b\\'c");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns unchanged string without quotes', () => {
|
|
67
|
+
expect(code.escape('hello')).toBe('hello');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('handles empty string', () => {
|
|
71
|
+
expect(code.escape('')).toBe('');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { bench, describe, vi } from 'vitest';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
import type { Plugin, 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 = 'bench.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
|
+
let code = 'let x = 1;',
|
|
41
|
+
file = parse(code),
|
|
42
|
+
program = makeProgram(file);
|
|
43
|
+
|
|
44
|
+
describe('applyImports batching', () => {
|
|
45
|
+
bench('10 intents, 1 package', () => {
|
|
46
|
+
let plugin = makePlugin(() => ({
|
|
47
|
+
imports: [
|
|
48
|
+
{ add: ['a'], package: '@pkg/a' },
|
|
49
|
+
{ add: ['b'], package: '@pkg/a' },
|
|
50
|
+
{ add: ['c'], package: '@pkg/a' },
|
|
51
|
+
{ add: ['d'], package: '@pkg/a' },
|
|
52
|
+
{ add: ['e'], package: '@pkg/a' },
|
|
53
|
+
{ add: ['f'], package: '@pkg/a' },
|
|
54
|
+
{ add: ['g'], package: '@pkg/a' },
|
|
55
|
+
{ add: ['h'], package: '@pkg/a' },
|
|
56
|
+
{ add: ['i'], package: '@pkg/a' },
|
|
57
|
+
{ add: ['j'], package: '@pkg/a' }
|
|
58
|
+
]
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
coordinator.transform([plugin], code, file, program, '/root', new Map());
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
bench('10 intents, 3 packages', () => {
|
|
65
|
+
let plugin = makePlugin(() => ({
|
|
66
|
+
imports: [
|
|
67
|
+
{ add: ['a'], package: '@pkg/a' },
|
|
68
|
+
{ add: ['b'], package: '@pkg/a' },
|
|
69
|
+
{ add: ['c'], package: '@pkg/a' },
|
|
70
|
+
{ add: ['d'], package: '@pkg/b' },
|
|
71
|
+
{ add: ['e'], package: '@pkg/b' },
|
|
72
|
+
{ add: ['f'], package: '@pkg/b' },
|
|
73
|
+
{ add: ['g'], package: '@pkg/b' },
|
|
74
|
+
{ add: ['h'], package: '@pkg/c' },
|
|
75
|
+
{ add: ['i'], package: '@pkg/c' },
|
|
76
|
+
{ add: ['j'], package: '@pkg/c' }
|
|
77
|
+
]
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
coordinator.transform([plugin], code, file, program, '/root', new Map());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
bench('10 intents, 10 packages', () => {
|
|
84
|
+
let plugin = makePlugin(() => ({
|
|
85
|
+
imports: [
|
|
86
|
+
{ add: ['a'], package: '@pkg/a' },
|
|
87
|
+
{ add: ['b'], package: '@pkg/b' },
|
|
88
|
+
{ add: ['c'], package: '@pkg/c' },
|
|
89
|
+
{ add: ['d'], package: '@pkg/d' },
|
|
90
|
+
{ add: ['e'], package: '@pkg/e' },
|
|
91
|
+
{ add: ['f'], package: '@pkg/f' },
|
|
92
|
+
{ add: ['g'], package: '@pkg/g' },
|
|
93
|
+
{ add: ['h'], package: '@pkg/h' },
|
|
94
|
+
{ add: ['i'], package: '@pkg/i' },
|
|
95
|
+
{ add: ['j'], package: '@pkg/j' }
|
|
96
|
+
]
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
coordinator.transform([plugin], code, file, program, '/root', new Map());
|
|
100
|
+
});
|
|
101
|
+
});
|