@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.
@@ -1,5 +1,9 @@
1
1
  import { ts } from '../index.js';
2
2
  let cache = new WeakMap();
3
+ function fileNameMatchesPackage(fileName, pkg) {
4
+ let normalized = fileName.replace(/\\/g, '/'), marker = `/node_modules/${pkg}/`;
5
+ return normalized.includes(marker);
6
+ }
3
7
  const all = (file, pkg) => {
4
8
  let imports = [];
5
9
  for (let i = 0, n = file.statements.length; i < n; i++) {
@@ -60,7 +64,7 @@ const includes = (checker, node, pkg, symbolName) => {
60
64
  }
61
65
  }
62
66
  }
63
- if (decl.getSourceFile().fileName.includes(pkg)) {
67
+ if (fileNameMatchesPackage(decl.getSourceFile().fileName, pkg)) {
64
68
  return true;
65
69
  }
66
70
  }
@@ -76,7 +80,7 @@ const includes = (checker, node, pkg, symbolName) => {
76
80
  if (declarations && declarations.length > 0) {
77
81
  for (let i = 0, n = declarations.length; i < n; i++) {
78
82
  let decl = declarations[i];
79
- if (decl.getSourceFile().fileName.includes(pkg)) {
83
+ if (fileNameMatchesPackage(decl.getSourceFile().fileName, pkg)) {
80
84
  return true;
81
85
  }
82
86
  }
@@ -87,7 +91,7 @@ const includes = (checker, node, pkg, symbolName) => {
87
91
  let aliasedDecls = aliased.getDeclarations();
88
92
  if (aliasedDecls) {
89
93
  for (let i = 0, n = aliasedDecls.length; i < n; i++) {
90
- if (aliasedDecls[i].getSourceFile().fileName.includes(pkg)) {
94
+ if (fileNameMatchesPackage(aliasedDecls[i].getSourceFile().fileName, pkg)) {
91
95
  return true;
92
96
  }
93
97
  }
@@ -1,12 +1,9 @@
1
- import ts from 'typescript';
2
- declare const get: (root: string) => ts.Program;
1
+ import { ts } from '../index.js';
3
2
  declare const invalidate: (root: string, fileName: string) => void;
4
3
  declare const update: (root: string, fileName: string, content: string) => ts.Program;
5
4
  declare const _default: {
6
- delete: (root: string) => void;
7
- get: (root: string) => ts.Program;
8
5
  invalidate: (root: string, fileName: string) => void;
9
6
  update: (root: string, fileName: string, content: string) => ts.Program;
10
7
  };
11
8
  export default _default;
12
- export { get, invalidate, update };
9
+ export { invalidate, update };
@@ -1,6 +1,6 @@
1
- import path from 'path';
2
- import ts from 'typescript';
3
1
  import { PACKAGE_NAME } from '../constants.js';
2
+ import { ts } from '../index.js';
3
+ import path from 'path';
4
4
  let cache = new Map();
5
5
  function create(root) {
6
6
  let tsconfig = ts.findConfigFile(root, ts.sys.fileExists, 'tsconfig.json');
@@ -48,16 +48,6 @@ function getEntry(root) {
48
48
  }
49
49
  return entry;
50
50
  }
51
- const del = (root) => {
52
- cache.delete(root);
53
- };
54
- const get = (root) => {
55
- let entry = getEntry(root), program = entry.service.getProgram();
56
- if (!program) {
57
- throw new Error(`${PACKAGE_NAME}: failed to get program from language service`);
58
- }
59
- return program;
60
- };
61
51
  const invalidate = (root, fileName) => {
62
52
  let entry = cache.get(root);
63
53
  if (entry) {
@@ -79,5 +69,5 @@ const update = (root, fileName, content) => {
79
69
  }
80
70
  return program;
81
71
  };
82
- export default { delete: del, get, invalidate, update };
83
- export { get, invalidate, update };
72
+ export default { invalidate, update };
73
+ export { invalidate, update };
@@ -19,9 +19,14 @@ export default ({ name, onWatchChange, plugins }) => {
19
19
  try {
20
20
  let normalizedId = id.replace(DIRECTORY_SEPARATOR_REGEX, '/'), prog = languageService.update(root || '', normalizedId, code), sourceFile = prog.getSourceFile(normalizedId);
21
21
  if (!sourceFile) {
22
- sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
22
+ sourceFile = ts.createSourceFile(normalizedId, code, ts.ScriptTarget.Latest, true);
23
23
  }
24
- let result = coordinator.transform(plugins, code, sourceFile, prog, root || '', contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || ''));
24
+ let key = root || '', ctx = contexts.get(key);
25
+ if (!ctx) {
26
+ ctx = new Map();
27
+ contexts.set(key, ctx);
28
+ }
29
+ let result = coordinator.transform(plugins, code, sourceFile, prog, key, ctx);
25
30
  if (!result.changed) {
26
31
  return null;
27
32
  }
package/package.json CHANGED
@@ -7,14 +7,15 @@
7
7
  "tsc-alias": "./bin/tsc-alias"
8
8
  },
9
9
  "dependencies": {
10
- "@esportsplus/cli-passthrough": "^0.0.12",
11
- "@esportsplus/utilities": "^0.27.2",
12
- "@types/node": "^25.0.3",
13
- "tsc-alias": "^1.8.16",
14
- "typescript": "^5.9.3"
10
+ "@esportsplus/cli-passthrough": "^0.0.15",
11
+ "@esportsplus/utilities": "^0.28.0",
12
+ "@types/node": "^26.0.1",
13
+ "tsc-alias": "^1.8.17",
14
+ "typescript": "^6.0.3"
15
15
  },
16
16
  "devDependencies": {
17
- "vite": "^6.0.7"
17
+ "vite": "^8.1.0",
18
+ "vitest": "^4.1.9"
18
19
  },
19
20
  "exports": {
20
21
  "./package.json": "./package.json",
@@ -32,6 +33,9 @@
32
33
  },
33
34
  "main": "build/index.js",
34
35
  "name": "@esportsplus/typescript",
36
+ "overrides": {
37
+ "postcss": "8.5.15"
38
+ },
35
39
  "private": false,
36
40
  "repository": {
37
41
  "type": "git",
@@ -39,9 +43,11 @@
39
43
  },
40
44
  "type": "module",
41
45
  "types": "build/index.d.ts",
42
- "version": "0.29.0",
46
+ "version": "0.29.5",
43
47
  "scripts": {
48
+ "bench:run": "vitest bench --run",
44
49
  "build": "tsc && tsc-alias",
45
- "-": "-"
50
+ "-": "-",
51
+ "test": "vitest run"
46
52
  }
47
53
  }
package/src/cli/tsc.ts CHANGED
@@ -1,15 +1,15 @@
1
- import { spawn } from 'child_process';
1
+ import type { Plugin, SharedContext } from '~/compiler/types';
2
2
  import { createRequire } from 'module';
3
+ import { PACKAGE_NAME } from '~/constants';
3
4
  import { pathToFileURL } from 'url';
5
+ import { spawn } from 'child_process';
6
+
7
+ import coordinator from '~/compiler/coordinator';
4
8
  import path from 'path';
5
9
  import ts from 'typescript';
6
- import coordinator from '~/compiler/coordinator';
7
- import type { Plugin, SharedContext } from '~/compiler/types';
8
- import { PACKAGE_NAME } from '~/constants';
9
10
 
10
11
 
11
12
  type PluginConfig = {
12
- after?: boolean;
13
13
  transform: string;
14
14
  };
15
15
 
@@ -227,4 +227,9 @@ function runTscAlias(args: string[]): Promise<number> {
227
227
  }
228
228
 
229
229
 
230
- main();
230
+ if (process.env.VITEST === undefined) {
231
+ main();
232
+ }
233
+
234
+
235
+ export { build, isPlugin, loadPlugins, normalizePath, runTscAlias };
@@ -1,10 +1,5 @@
1
- import ts from 'typescript';
2
-
3
-
4
- type Range = {
5
- end: number;
6
- start: number;
7
- };
1
+ import type { Range } from './types';
2
+ import { ts } from '~/index';
8
3
 
9
4
 
10
5
  const expression = {
@@ -1,6 +1,8 @@
1
1
  import type { ImportIntent, Plugin, Replacement, ReplacementIntent, SharedContext } from './types';
2
+ import type { ModifyOptions } from './imports';
2
3
  import { ts } from '~/index';
3
- import imports, { ModifyOptions } from './imports';
4
+
5
+ import imports from './imports';
4
6
  import languageService from './language-service';
5
7
 
6
8
 
@@ -12,22 +14,39 @@ type CoordinatorResult = {
12
14
 
13
15
 
14
16
  function applyImports(code: string, file: ts.SourceFile, intents: ImportIntent[]): string {
17
+ let merged = new Map<string, { add?: string[]; namespace?: string; remove?: string[] }>();
18
+
15
19
  for (let i = 0, n = intents.length; i < n; i++) {
16
- let intent = intents[i];
20
+ let intent = intents[i],
21
+ existing = merged.get(intent.package);
17
22
 
18
- code = modify(code, file, intent.package, {
19
- add: intent.add,
20
- namespace: intent.namespace,
21
- remove: intent.remove
22
- });
23
+ if (existing) {
24
+ if (intent.add) {
25
+ (existing.add ??= []).push(...intent.add);
26
+ }
27
+ if (intent.namespace) {
28
+ existing.namespace = intent.namespace;
29
+ }
30
+ if (intent.remove) {
31
+ (existing.remove ??= []).push(...intent.remove);
32
+ }
33
+ }
34
+ else {
35
+ merged.set(intent.package, {
36
+ add: intent.add ? [...intent.add] : undefined,
37
+ namespace: intent.namespace,
38
+ remove: intent.remove ? [...intent.remove] : undefined
39
+ });
40
+ }
41
+ }
42
+
43
+ let keys = [...merged.keys()];
44
+
45
+ for (let i = 0, n = keys.length; i < n; i++) {
46
+ code = modify(code, file, keys[i], merged.get(keys[i])!);
23
47
 
24
48
  if (i < n - 1) {
25
- file = ts.createSourceFile(
26
- file.fileName,
27
- code,
28
- file.languageVersion,
29
- true
30
- );
49
+ file = ts.createSourceFile(file.fileName, code, file.languageVersion, true);
31
50
  }
32
51
  }
33
52
 
@@ -68,10 +87,10 @@ function applyPrepend(code: string, file: ts.SourceFile, prepend: string[]): str
68
87
  }
69
88
 
70
89
  if (position === 0) {
71
- return prepend.join('\n') + code;
90
+ return prepend.join('\n') + '\n' + code;
72
91
  }
73
92
 
74
- return code.slice(0, position) + prepend.join('\n') + code.slice(position);
93
+ return code.slice(0, position) + '\n' + prepend.join('\n') + '\n' + code.slice(position);
75
94
  }
76
95
 
77
96
  function hasPattern(code: string, patterns: string[]): boolean {
@@ -158,15 +177,25 @@ function replaceReverse(code: string, replacements: Replacement[]): string {
158
177
 
159
178
  replacements.sort((a, b) => b.start - a.start);
160
179
 
161
- let result = code;
180
+ let parts: string[] = [],
181
+ pos = code.length;
162
182
 
163
183
  for (let i = 0, n = replacements.length; i < n; i++) {
164
184
  let r = replacements[i];
165
185
 
166
- result = result.substring(0, r.start) + r.newText + result.substring(r.end);
186
+ if (r.end < pos) {
187
+ parts.push(code.substring(r.end, pos));
188
+ }
189
+
190
+ parts.push(r.newText);
191
+ pos = r.start;
167
192
  }
168
193
 
169
- return result;
194
+ if (pos > 0) {
195
+ parts.push(code.substring(0, pos));
196
+ }
197
+
198
+ return parts.reverse().join('');
170
199
  }
171
200
 
172
201
 
@@ -211,11 +240,19 @@ const transform = (
211
240
  }
212
241
 
213
242
  if (prepend?.length) {
243
+ if (pluginChanged) {
244
+ currentFile = ts.createSourceFile(fileName, currentCode, file.languageVersion, true);
245
+ }
246
+
214
247
  currentCode = applyPrepend(currentCode, currentFile, prepend);
215
248
  pluginChanged = true;
216
249
  }
217
250
 
218
251
  if (imports?.length) {
252
+ if (pluginChanged) {
253
+ currentFile = ts.createSourceFile(fileName, currentCode, file.languageVersion, true);
254
+ }
255
+
219
256
  currentCode = applyImports(currentCode, currentFile, imports);
220
257
  pluginChanged = true;
221
258
  }
@@ -17,6 +17,14 @@ type ModifyOptions = {
17
17
  let cache = new WeakMap<ts.SourceFile, Map<string, Set<string>>>();
18
18
 
19
19
 
20
+ function fileNameMatchesPackage(fileName: string, pkg: string): boolean {
21
+ let normalized = fileName.replace(/\\/g, '/'),
22
+ marker = `/node_modules/${pkg}/`;
23
+
24
+ return normalized.includes(marker);
25
+ }
26
+
27
+
20
28
  // Find all named imports from a specific package
21
29
  const all = (file: ts.SourceFile, pkg: string): ImportInfo[] => {
22
30
  let imports: ImportInfo[] = [];
@@ -108,7 +116,7 @@ const includes = (checker: ts.TypeChecker, node: ts.Node, pkg: string, symbolNam
108
116
  }
109
117
  }
110
118
 
111
- if (decl.getSourceFile().fileName.includes(pkg)) {
119
+ if (fileNameMatchesPackage(decl.getSourceFile().fileName, pkg)) {
112
120
  return true;
113
121
  }
114
122
  }
@@ -133,7 +141,7 @@ const includes = (checker: ts.TypeChecker, node: ts.Node, pkg: string, symbolNam
133
141
  for (let i = 0, n = declarations.length; i < n; i++) {
134
142
  let decl = declarations[i];
135
143
 
136
- if (decl.getSourceFile().fileName.includes(pkg)) {
144
+ if (fileNameMatchesPackage(decl.getSourceFile().fileName, pkg)) {
137
145
  return true;
138
146
  }
139
147
  }
@@ -148,7 +156,7 @@ const includes = (checker: ts.TypeChecker, node: ts.Node, pkg: string, symbolNam
148
156
 
149
157
  if (aliasedDecls) {
150
158
  for (let i = 0, n = aliasedDecls.length; i < n; i++) {
151
- if (aliasedDecls[i].getSourceFile().fileName.includes(pkg)) {
159
+ if (fileNameMatchesPackage(aliasedDecls[i].getSourceFile().fileName, pkg)) {
152
160
  return true;
153
161
  }
154
162
  }
@@ -1,6 +1,7 @@
1
- import path from 'path';
2
- import ts from 'typescript';
3
1
  import { PACKAGE_NAME } from '~/constants';
2
+ import { ts } from '~/index';
3
+
4
+ import path from 'path';
4
5
 
5
6
 
6
7
  type LanguageServiceEntry = {
@@ -84,21 +85,6 @@ function getEntry(root: string): LanguageServiceEntry {
84
85
  }
85
86
 
86
87
 
87
- const del = (root: string): void => {
88
- cache.delete(root);
89
- };
90
-
91
- const get = (root: string): ts.Program => {
92
- let entry = getEntry(root),
93
- program = entry.service.getProgram();
94
-
95
- if (!program) {
96
- throw new Error(`${PACKAGE_NAME}: failed to get program from language service`);
97
- }
98
-
99
- return program;
100
- };
101
-
102
88
  const invalidate = (root: string, fileName: string): void => {
103
89
  let entry = cache.get(root);
104
90
 
@@ -131,5 +117,5 @@ const update = (root: string, fileName: string, content: string): ts.Program =>
131
117
  };
132
118
 
133
119
 
134
- export default { delete: del, get, invalidate, update };
135
- export { get, invalidate, update };
120
+ export default { invalidate, update };
121
+ export { invalidate, update };
@@ -1,6 +1,7 @@
1
- import type { ResolvedConfig } from 'vite';
2
1
  import type { Plugin, SharedContext } from '../types';
2
+ import type { ResolvedConfig } from 'vite';
3
3
  import { ts } from '~/index';
4
+
4
5
  import coordinator from '../coordinator';
5
6
  import languageService from '../language-service';
6
7
 
@@ -47,7 +48,15 @@ export default ({ name, onWatchChange, plugins }: VitePluginOptions) => {
47
48
  sourceFile = prog.getSourceFile(normalizedId);
48
49
 
49
50
  if (!sourceFile) {
50
- sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
51
+ sourceFile = ts.createSourceFile(normalizedId, code, ts.ScriptTarget.Latest, true);
52
+ }
53
+
54
+ let key = root || '',
55
+ ctx = contexts.get(key);
56
+
57
+ if (!ctx) {
58
+ ctx = new Map();
59
+ contexts.set(key, ctx);
51
60
  }
52
61
 
53
62
  let result = coordinator.transform(
@@ -55,8 +64,8 @@ export default ({ name, onWatchChange, plugins }: VitePluginOptions) => {
55
64
  code,
56
65
  sourceFile,
57
66
  prog,
58
- root || '',
59
- contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || '')!
67
+ key,
68
+ ctx
60
69
  );
61
70
 
62
71
  if (!result.changed) {
@@ -0,0 +1,155 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ import { isPlugin, loadPlugins, normalizePath, runTscAlias } from '~/cli/tsc';
7
+
8
+
9
+ describe('isPlugin', () => {
10
+ it('returns true for valid plugin', () => {
11
+ expect(isPlugin({ transform: () => {} })).toBe(true);
12
+ });
13
+
14
+ it('returns false for null', () => {
15
+ expect(isPlugin(null)).toBe(false);
16
+ });
17
+
18
+ it('returns false for empty object', () => {
19
+ expect(isPlugin({})).toBe(false);
20
+ });
21
+
22
+ it('returns false when transform is not a function', () => {
23
+ expect(isPlugin({ transform: 'not-fn' })).toBe(false);
24
+ });
25
+
26
+ it('returns false for primitives', () => {
27
+ expect(isPlugin(42)).toBe(false);
28
+ expect(isPlugin('str')).toBe(false);
29
+ expect(isPlugin(undefined)).toBe(false);
30
+ });
31
+ });
32
+
33
+
34
+ describe('normalizePath', () => {
35
+ it('converts backslashes to forward slashes', () => {
36
+ let result = normalizePath('C:\\foo\\bar.ts');
37
+
38
+ expect(result).not.toContain('\\');
39
+ expect(result).toContain('/foo/bar');
40
+ });
41
+
42
+ it('resolves to absolute path', () => {
43
+ let result = normalizePath('relative/file.ts');
44
+
45
+ expect(path.isAbsolute(result)).toBe(true);
46
+ });
47
+ });
48
+
49
+
50
+ describe('loadPlugins', () => {
51
+ let tmpDir: string;
52
+
53
+ beforeEach(() => {
54
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tsc-test-'));
55
+ });
56
+
57
+ afterEach(() => {
58
+ fs.rmSync(tmpDir, { recursive: true, force: true });
59
+ });
60
+
61
+ it('loads a valid plugin with transform export', async () => {
62
+ let pluginFile = 'plugin.mjs';
63
+
64
+ fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default { transform: () => ({}) };');
65
+
66
+ let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
67
+
68
+ expect(plugins).toHaveLength(1);
69
+ expect(typeof plugins[0].transform).toBe('function');
70
+ });
71
+
72
+ it('loads a factory function that returns a plugin', async () => {
73
+ let pluginFile = 'factory.mjs';
74
+
75
+ fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default function() { return { transform: () => ({}) }; };');
76
+
77
+ let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
78
+
79
+ expect(plugins).toHaveLength(1);
80
+ expect(typeof plugins[0].transform).toBe('function');
81
+ });
82
+
83
+ it('loads array of plugins', async () => {
84
+ let pluginFile = 'array.mjs';
85
+
86
+ fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default [{ transform: () => ({}) }, { transform: () => ({}) }];');
87
+
88
+ let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
89
+
90
+ expect(plugins).toHaveLength(2);
91
+ });
92
+
93
+ it('skips invalid plugin format with error', async () => {
94
+ let pluginFile = 'invalid.mjs';
95
+
96
+ fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default { notTransform: true };');
97
+
98
+ let spy = vi.spyOn(console, 'error').mockImplementation(() => {});
99
+ let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
100
+
101
+ expect(plugins).toHaveLength(0);
102
+ expect(spy).toHaveBeenCalled();
103
+ spy.mockRestore();
104
+ });
105
+
106
+ it('skips invalid array element with error', async () => {
107
+ let pluginFile = 'mixed.mjs';
108
+
109
+ fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default [{ transform: () => ({}) }, { bad: true }];');
110
+
111
+ let spy = vi.spyOn(console, 'error').mockImplementation(() => {});
112
+ let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
113
+
114
+ expect(plugins).toHaveLength(1);
115
+ expect(spy).toHaveBeenCalled();
116
+ spy.mockRestore();
117
+ });
118
+
119
+ it('resolves relative paths from root', async () => {
120
+ let pluginFile = 'relative.mjs';
121
+
122
+ fs.writeFileSync(path.join(tmpDir, pluginFile), 'export default { transform: () => ({}) };');
123
+
124
+ let plugins = await loadPlugins([{ transform: './' + pluginFile }], tmpDir);
125
+
126
+ expect(plugins).toHaveLength(1);
127
+ });
128
+ });
129
+
130
+
131
+ describe('runTscAlias', () => {
132
+ it('returns 0 for --noEmit flag', async () => {
133
+ let code = await runTscAlias(['--noEmit']);
134
+
135
+ expect(code).toBe(0);
136
+ });
137
+
138
+ it('returns 0 for --help flag', async () => {
139
+ let code = await runTscAlias(['--help']);
140
+
141
+ expect(code).toBe(0);
142
+ });
143
+
144
+ it('returns 0 for --version flag', async () => {
145
+ let code = await runTscAlias(['--version']);
146
+
147
+ expect(code).toBe(0);
148
+ });
149
+
150
+ it('returns 0 for -v flag', async () => {
151
+ let code = await runTscAlias(['-v']);
152
+
153
+ expect(code).toBe(0);
154
+ });
155
+ });