@esportsplus/typescript 0.28.5 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import ts from 'typescript';
3
+
4
+ import imports from '~/compiler/imports';
5
+
6
+
7
+ function parse(code: string, fileName = 'test.ts'): ts.SourceFile {
8
+ return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
9
+ }
10
+
11
+
12
+ describe('imports.all', () => {
13
+ it('finds named imports from a package', () => {
14
+ let file = parse("import { foo, bar } from 'my-pkg';"),
15
+ result = imports.all(file, 'my-pkg');
16
+
17
+ expect(result).toHaveLength(1);
18
+ expect(result[0].specifiers.get('foo')).toBe('foo');
19
+ expect(result[0].specifiers.get('bar')).toBe('bar');
20
+ });
21
+
22
+ it('handles aliased imports', () => {
23
+ let file = parse("import { foo as f } from 'my-pkg';"),
24
+ result = imports.all(file, 'my-pkg');
25
+
26
+ expect(result).toHaveLength(1);
27
+ expect(result[0].specifiers.get('foo')).toBe('f');
28
+ });
29
+
30
+ it('returns empty for non-matching package', () => {
31
+ let file = parse("import { foo } from 'other-pkg';"),
32
+ result = imports.all(file, 'my-pkg');
33
+
34
+ expect(result).toHaveLength(0);
35
+ });
36
+
37
+ it('returns empty when no imports', () => {
38
+ let file = parse('let x = 1;'),
39
+ result = imports.all(file, 'my-pkg');
40
+
41
+ expect(result).toHaveLength(0);
42
+ });
43
+
44
+ it('finds multiple import statements for same package', () => {
45
+ let file = parse("import { a } from 'pkg';\nimport { b } from 'pkg';"),
46
+ result = imports.all(file, 'pkg');
47
+
48
+ expect(result).toHaveLength(2);
49
+ });
50
+
51
+ it('tracks start and end positions', () => {
52
+ let file = parse("import { foo } from 'my-pkg';"),
53
+ result = imports.all(file, 'my-pkg');
54
+
55
+ expect(result[0].start).toBe(0);
56
+ expect(result[0].end).toBeGreaterThan(0);
57
+ });
58
+
59
+ it('handles default import (no named bindings)', () => {
60
+ let file = parse("import pkg from 'my-pkg';"),
61
+ result = imports.all(file, 'my-pkg');
62
+
63
+ expect(result).toHaveLength(1);
64
+ expect(result[0].specifiers.size).toBe(0);
65
+ });
66
+
67
+ it('handles namespace import', () => {
68
+ let file = parse("import * as pkg from 'my-pkg';"),
69
+ result = imports.all(file, 'my-pkg');
70
+
71
+ expect(result).toHaveLength(1);
72
+ expect(result[0].specifiers.size).toBe(0);
73
+ });
74
+ });
75
+
76
+
77
+ describe('imports.includes', () => {
78
+ let mockChecker = { getSymbolAtLocation: () => null } as unknown as ts.TypeChecker;
79
+
80
+ function findIdentifier(file: ts.SourceFile, name: string): ts.Identifier | undefined {
81
+ let found: ts.Identifier | undefined;
82
+
83
+ ts.forEachChild(file, function visit(n) {
84
+ if (ts.isIdentifier(n) && n.text === name && !found) {
85
+ let parent = n.parent;
86
+
87
+ if (!ts.isImportSpecifier(parent) && !ts.isImportClause(parent) && !ts.isNamespaceImport(parent)) {
88
+ found = n;
89
+ }
90
+ }
91
+
92
+ ts.forEachChild(n, visit);
93
+ });
94
+
95
+ return found;
96
+ }
97
+
98
+ it('direct named import matches', () => {
99
+ let file = parse("import { reactive } from 'my-pkg';\nreactive(x);"),
100
+ node = findIdentifier(file, 'reactive');
101
+
102
+ expect(node).toBeDefined();
103
+ expect(imports.includes(mockChecker, node!, 'my-pkg', 'reactive')).toBe(true);
104
+ });
105
+
106
+ it('aliased import matches', () => {
107
+ let file = parse("import { foo as f } from 'my-pkg';\nf();"),
108
+ node = findIdentifier(file, 'f');
109
+
110
+ expect(node).toBeDefined();
111
+ expect(imports.includes(mockChecker, node!, 'my-pkg')).toBe(true);
112
+ });
113
+
114
+ it('non-matching package returns false', () => {
115
+ let file = parse("import { foo } from 'other-pkg';\nfoo();"),
116
+ node = findIdentifier(file, 'foo');
117
+
118
+ expect(node).toBeDefined();
119
+ expect(imports.includes(mockChecker, node!, 'my-pkg')).toBe(false);
120
+ });
121
+
122
+ it('non-matching symbol name returns false', () => {
123
+ let file = parse("import { foo } from 'my-pkg';\nfoo();"),
124
+ node = findIdentifier(file, 'foo');
125
+
126
+ expect(node).toBeDefined();
127
+ expect(imports.includes(mockChecker, node!, 'my-pkg', 'bar')).toBe(false);
128
+ });
129
+
130
+ it('cache returns consistent results', () => {
131
+ let file = parse("import { reactive } from 'my-pkg';\nreactive(1);"),
132
+ node = findIdentifier(file, 'reactive');
133
+
134
+ expect(node).toBeDefined();
135
+
136
+ let first = imports.includes(mockChecker, node!, 'my-pkg', 'reactive'),
137
+ second = imports.includes(mockChecker, node!, 'my-pkg', 'reactive');
138
+
139
+ expect(first).toBe(true);
140
+ expect(second).toBe(true);
141
+ expect(first).toBe(second);
142
+ });
143
+
144
+ it('no imports at all returns false', () => {
145
+ let file = parse('let x = 1;\nx;'),
146
+ node = findIdentifier(file, 'x');
147
+
148
+ expect(node).toBeDefined();
149
+ expect(imports.includes(mockChecker, node!, 'my-pkg')).toBe(false);
150
+ });
151
+
152
+ it('returns false when getAliasedSymbol throws', () => {
153
+ let file = parse("import { foo } from 'my-pkg';\nbar();"),
154
+ node = findIdentifier(file, 'bar');
155
+
156
+ expect(node).toBeDefined();
157
+
158
+ let checker = {
159
+ getSymbolAtLocation: () => ({
160
+ getDeclarations: () => []
161
+ }),
162
+ getAliasedSymbol: () => { throw new Error('not an alias'); }
163
+ } as unknown as ts.TypeChecker;
164
+
165
+ expect(imports.includes(checker, node!, 'my-pkg')).toBe(false);
166
+ });
167
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import languageService from '~/compiler/language-service';
4
+
5
+
6
+ describe('language-service', () => {
7
+ describe('update', () => {
8
+ it('returns a Program when given valid root + fileName + content', () => {
9
+ let root = process.cwd().replace(/\\/g, '/'),
10
+ fileName = root + '/test-virtual-update.ts',
11
+ content = 'let x: number = 42;',
12
+ program = languageService.update(root, fileName, content);
13
+
14
+ expect(program).toBeDefined();
15
+ expect(program.getTypeChecker).toBeDefined();
16
+ });
17
+
18
+ it('updated content is reflected in the program SourceFile', () => {
19
+ let root = process.cwd().replace(/\\/g, '/'),
20
+ fileName = root + '/test-virtual-reflect.ts',
21
+ content = 'let hello = "world";',
22
+ program = languageService.update(root, fileName, content),
23
+ sourceFile = program.getSourceFile(fileName);
24
+
25
+ expect(sourceFile).toBeDefined();
26
+ expect(sourceFile!.text).toBe(content);
27
+ });
28
+
29
+ it('increments version for updated files', () => {
30
+ let root = process.cwd().replace(/\\/g, '/'),
31
+ fileName = root + '/test-virtual-version.ts';
32
+
33
+ languageService.update(root, fileName, 'let a = 1;');
34
+
35
+ let program = languageService.update(root, fileName, 'let a = 2;'),
36
+ sourceFile = program.getSourceFile(fileName);
37
+
38
+ expect(sourceFile).toBeDefined();
39
+ expect(sourceFile!.text).toBe('let a = 2;');
40
+ });
41
+
42
+ it('adds new files to rootFiles', () => {
43
+ let root = process.cwd().replace(/\\/g, '/'),
44
+ fileName = root + '/test-virtual-new-root.ts',
45
+ content = 'export const value = 1;',
46
+ program = languageService.update(root, fileName, content),
47
+ sourceFile = program.getSourceFile(fileName);
48
+
49
+ expect(sourceFile).toBeDefined();
50
+ expect(sourceFile!.text).toBe(content);
51
+ });
52
+ });
53
+
54
+ describe('invalidate', () => {
55
+ it('removes content so next getProgram reads from disk', () => {
56
+ let root = process.cwd().replace(/\\/g, '/'),
57
+ fileName = root + '/test-virtual-invalidate.ts',
58
+ content = 'let val = 99;';
59
+
60
+ languageService.update(root, fileName, content);
61
+ languageService.invalidate(root, fileName);
62
+
63
+ let program = languageService.update(root, fileName, 'let val = 100;'),
64
+ sourceFile = program.getSourceFile(fileName);
65
+
66
+ expect(sourceFile).toBeDefined();
67
+ expect(sourceFile!.text).toBe('let val = 100;');
68
+ });
69
+
70
+ it('increments version for invalidated files', () => {
71
+ let root = process.cwd().replace(/\\/g, '/'),
72
+ fileName = root + '/test-virtual-inv-version.ts';
73
+
74
+ languageService.update(root, fileName, 'let a = 1;');
75
+ languageService.invalidate(root, fileName);
76
+
77
+ let program = languageService.update(root, fileName, 'let a = 3;'),
78
+ sourceFile = program.getSourceFile(fileName);
79
+
80
+ expect(sourceFile).toBeDefined();
81
+ expect(sourceFile!.text).toBe('let a = 3;');
82
+ });
83
+
84
+ it('no-op when root does not exist in cache', () => {
85
+ expect(() => {
86
+ languageService.invalidate('/nonexistent/root', 'file.ts');
87
+ }).not.toThrow();
88
+ });
89
+ });
90
+ });
@@ -0,0 +1,172 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import ts from 'typescript';
3
+
4
+ import type { Plugin } from '~/compiler/types';
5
+
6
+ import tsc from '~/compiler/plugins/tsc';
7
+ import vite from '~/compiler/plugins/vite';
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
+ vi.mock('~/compiler/coordinator', () => ({
24
+ default: {
25
+ transform: vi.fn((_plugins: Plugin[], code: string, _file: ts.SourceFile, _prog: ts.Program, _root: string, _ctx: Map<string, unknown>) => ({
26
+ changed: false,
27
+ code,
28
+ sourceFile: {} as ts.SourceFile
29
+ }))
30
+ }
31
+ }));
32
+
33
+ import coordinator from '~/compiler/coordinator';
34
+ import languageService from '~/compiler/language-service';
35
+
36
+
37
+ describe('plugin.tsc', () => {
38
+ it('returns a function that returns the plugins array', () => {
39
+ let p1: Plugin = { transform: () => ({}) },
40
+ p2: Plugin = { transform: () => ({}) },
41
+ factory = tsc([p1, p2]),
42
+ result = factory();
43
+
44
+ expect(result).toEqual([p1, p2]);
45
+ });
46
+
47
+ it('returns empty array for empty input', () => {
48
+ let result = tsc([])();
49
+
50
+ expect(result).toEqual([]);
51
+ });
52
+ });
53
+
54
+
55
+ describe('plugin.vite', () => {
56
+ beforeEach(() => {
57
+ vi.clearAllMocks();
58
+ });
59
+
60
+ it('creates VitePlugin with correct shape', () => {
61
+ let factory = vite({ name: 'test-pkg', plugins: [] }),
62
+ plugin = factory();
63
+
64
+ expect(plugin).toHaveProperty('configResolved');
65
+ expect(plugin).toHaveProperty('enforce');
66
+ expect(plugin).toHaveProperty('name');
67
+ expect(plugin).toHaveProperty('transform');
68
+ expect(plugin).toHaveProperty('watchChange');
69
+ });
70
+
71
+ it('name includes package name', () => {
72
+ let plugin = vite({ name: 'test-pkg', plugins: [] })();
73
+
74
+ expect(plugin.name).toBe('test-pkg/compiler/vite');
75
+ });
76
+
77
+ it('filters non-ts files', () => {
78
+ let plugin = vite({ name: 'test-pkg', plugins: [] })();
79
+
80
+ expect(plugin.transform('code', 'file.css')).toBeNull();
81
+ });
82
+
83
+ it('filters node_modules', () => {
84
+ let plugin = vite({ name: 'test-pkg', plugins: [] })();
85
+
86
+ expect(plugin.transform('code', 'node_modules/pkg/index.ts')).toBeNull();
87
+ });
88
+
89
+ it('processes .ts files — returns null when unchanged', () => {
90
+ let plugin = vite({ name: 'test-pkg', plugins: [] })();
91
+
92
+ let result = plugin.transform('let x = 1;', 'src/app.ts');
93
+
94
+ expect(result).toBeNull();
95
+ expect(coordinator.transform).toHaveBeenCalled();
96
+ });
97
+
98
+ it('returns transformed code when changed', () => {
99
+ vi.mocked(coordinator.transform).mockReturnValueOnce({
100
+ changed: true,
101
+ code: 'TRANSFORMED',
102
+ sourceFile: {} as ts.SourceFile
103
+ });
104
+
105
+ let plugin = vite({ name: 'test-pkg', plugins: [] })();
106
+
107
+ let result = plugin.transform('let x = 1;', 'src/app.ts');
108
+
109
+ expect(result).toEqual({ code: 'TRANSFORMED', map: null });
110
+ });
111
+
112
+ it('watchChange calls onWatchChange and invalidate', () => {
113
+ let onWatchChange = vi.fn(),
114
+ plugin = vite({ name: 'test-pkg', onWatchChange, plugins: [] })();
115
+
116
+ plugin.watchChange('src/app.ts');
117
+
118
+ expect(onWatchChange).toHaveBeenCalled();
119
+ expect(languageService.invalidate).toHaveBeenCalledWith('', 'src/app.ts');
120
+ });
121
+
122
+ it('watchChange ignores non-ts files', () => {
123
+ let onWatchChange = vi.fn(),
124
+ plugin = vite({ name: 'test-pkg', onWatchChange, plugins: [] })();
125
+
126
+ plugin.watchChange('style.css');
127
+
128
+ expect(onWatchChange).not.toHaveBeenCalled();
129
+ });
130
+
131
+ it('configResolved sets root', () => {
132
+ let plugin = vite({ name: 'test-pkg', plugins: [] })();
133
+
134
+ plugin.configResolved({ root: '/my/root' });
135
+ plugin.transform('let x = 1;', 'src/app.ts');
136
+
137
+ expect(languageService.update).toHaveBeenCalledWith('/my/root', expect.any(String), expect.any(String));
138
+ });
139
+
140
+ it('catches coordinator.transform() error and returns null', () => {
141
+ vi.mocked(coordinator.transform).mockImplementationOnce(() => {
142
+ throw new Error('transform failed');
143
+ });
144
+
145
+ let consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}),
146
+ plugin = vite({ name: 'test-pkg', plugins: [] })();
147
+
148
+ let result = plugin.transform('let x = 1;', 'src/app.ts');
149
+
150
+ expect(result).toBeNull();
151
+ expect(consoleSpy).toHaveBeenCalledWith(
152
+ expect.stringContaining('test-pkg'),
153
+ expect.any(Error)
154
+ );
155
+
156
+ consoleSpy.mockRestore();
157
+ });
158
+
159
+ it('falls back to createSourceFile when getSourceFile returns undefined', () => {
160
+ vi.mocked(languageService.update).mockReturnValueOnce({
161
+ getSourceFile: () => undefined,
162
+ getTypeChecker: () => ({} as ts.TypeChecker)
163
+ } as unknown as ts.Program);
164
+
165
+ let plugin = vite({ name: 'test-pkg', plugins: [] })();
166
+
167
+ let result = plugin.transform('let x = 1;', 'src/app.ts');
168
+
169
+ expect(result).toBeNull();
170
+ expect(coordinator.transform).toHaveBeenCalled();
171
+ });
172
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import uid from '~/compiler/uid';
4
+
5
+
6
+ describe('uid', () => {
7
+ it('generates unique ids', () => {
8
+ let a = uid('test'),
9
+ b = uid('test');
10
+
11
+ expect(a).not.toBe(b);
12
+ });
13
+
14
+ it('prefixes with given name', () => {
15
+ let result = uid('myPrefix');
16
+
17
+ expect(result.startsWith('myPrefix_')).toBe(true);
18
+ });
19
+
20
+ it('contains only alphanumeric characters after prefix', () => {
21
+ let result = uid('x'),
22
+ suffix = result.slice(2); // after 'x_'
23
+
24
+ expect(suffix).toMatch(/^[A-Za-z0-9]+$/);
25
+ });
26
+
27
+ // F-TEST-008: uid sequential guarantees
28
+
29
+ it('sequential calls produce different suffixes (5 calls)', () => {
30
+ let results = new Set<string>();
31
+
32
+ for (let i = 0; i < 5; i++) {
33
+ results.add(uid('x'));
34
+ }
35
+
36
+ expect(results.size).toBe(5);
37
+ });
38
+
39
+ it('different prefixes share same namespace', () => {
40
+ let a1 = uid('a'),
41
+ a2 = uid('a'),
42
+ b1 = uid('b'),
43
+ suffixA1 = a1.slice(2), // after 'a_'
44
+ suffixA2 = a2.slice(2),
45
+ suffixB1 = b1.slice(2); // after 'b_'
46
+
47
+ // Find common prefix between two 'a' calls — that's the namespace
48
+ let common = '';
49
+
50
+ for (let i = 0, n = Math.min(suffixA1.length, suffixA2.length); i < n; i++) {
51
+ if (suffixA1[i] !== suffixA2[i]) {
52
+ break;
53
+ }
54
+
55
+ common += suffixA1[i];
56
+ }
57
+
58
+ expect(common.length).toBeGreaterThan(0);
59
+ expect(suffixB1.startsWith(common)).toBe(true);
60
+ });
61
+
62
+ it('suffix contains valid base-36 characters', () => {
63
+ let result = uid('z'),
64
+ parts = result.slice(2), // after 'z_'
65
+ base36Suffix = parts.match(/[0-9a-z]+$/);
66
+
67
+ expect(base36Suffix).not.toBeNull();
68
+ expect(base36Suffix![0]).toMatch(/^[0-9a-z]+$/);
69
+ });
70
+ });
@@ -3,7 +3,6 @@
3
3
  "compilerOptions": {
4
4
  "allowJs": true,
5
5
  "allowSyntheticDefaultImports": true,
6
- "baseUrl": "${configDir}",
7
6
  "declaration": false,
8
7
  "esModuleInterop": true,
9
8
  "isolatedModules": true,
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import path from 'path';
3
+
4
+
5
+ export default defineConfig({
6
+ resolve: {
7
+ alias: {
8
+ '~': path.resolve(__dirname, 'src')
9
+ }
10
+ },
11
+ test: {
12
+ include: ['tests/**/*.test.ts']
13
+ }
14
+ });
@@ -1,6 +0,0 @@
1
- import ts from 'typescript';
2
- declare const _default: {
3
- get: (root: string) => ts.Program;
4
- delete: (root: string) => void;
5
- };
6
- export default _default;
@@ -1,34 +0,0 @@
1
- import path from 'path';
2
- import ts from 'typescript';
3
- import { PACKAGE_NAME } from '../constants.js';
4
- let cache = new Map();
5
- function create(root) {
6
- let tsconfig = ts.findConfigFile(root, ts.sys.fileExists, 'tsconfig.json');
7
- if (!tsconfig) {
8
- throw new Error('tsconfig.json not found');
9
- }
10
- let file = ts.readConfigFile(tsconfig, ts.sys.readFile);
11
- if (file.error) {
12
- throw new Error(`${PACKAGE_NAME}: error reading tsconfig.json ${file.error.messageText}`);
13
- }
14
- let parsed = ts.parseJsonConfigFileContent(file.config, ts.sys, path.dirname(tsconfig));
15
- if (parsed.errors.length > 0) {
16
- throw new Error(`${PACKAGE_NAME}: error parsing tsconfig.json ${parsed.errors[0].messageText}`);
17
- }
18
- return ts.createProgram({
19
- options: parsed.options,
20
- rootNames: parsed.fileNames
21
- });
22
- }
23
- const get = (root) => {
24
- let program = cache.get(root);
25
- if (!program) {
26
- program = create(root);
27
- cache.set(root, program);
28
- }
29
- return program;
30
- };
31
- const del = (root) => {
32
- cache.delete(root);
33
- };
34
- export default { get, delete: del };
@@ -1,55 +0,0 @@
1
- import path from 'path';
2
- import ts from 'typescript';
3
- import { PACKAGE_NAME } from '~/constants';
4
-
5
-
6
- let cache = new Map<string, ts.Program>();
7
-
8
-
9
- function create(root: string): ts.Program {
10
- let tsconfig = ts.findConfigFile(root, ts.sys.fileExists, 'tsconfig.json');
11
-
12
- if (!tsconfig) {
13
- throw new Error('tsconfig.json not found');
14
- }
15
-
16
- let file = ts.readConfigFile(tsconfig, ts.sys.readFile);
17
-
18
- if (file.error) {
19
- throw new Error(`${PACKAGE_NAME}: error reading tsconfig.json ${file.error.messageText}`);
20
- }
21
-
22
- let parsed = ts.parseJsonConfigFileContent(
23
- file.config,
24
- ts.sys,
25
- path.dirname(tsconfig)
26
- );
27
-
28
- if (parsed.errors.length > 0) {
29
- throw new Error(`${PACKAGE_NAME}: error parsing tsconfig.json ${parsed.errors[0].messageText}`);
30
- }
31
-
32
- return ts.createProgram({
33
- options: parsed.options,
34
- rootNames: parsed.fileNames
35
- });
36
- }
37
-
38
-
39
- const get = (root: string): ts.Program => {
40
- let program = cache.get(root);
41
-
42
- if (!program) {
43
- program = create(root);
44
- cache.set(root, program);
45
- }
46
-
47
- return program;
48
- }
49
-
50
- const del = (root: string): void => {
51
- cache.delete(root);
52
- }
53
-
54
-
55
- export default { get, delete: del };