@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.
@@ -1,9 +1,8 @@
1
1
  import { ts } from '../../index.js';
2
2
  import coordinator from '../coordinator.js';
3
- import program from '../program.js';
4
- const FILE_REGEX = /\.[tj]sx?$/;
3
+ import languageService from '../language-service.js';
5
4
  const DIRECTORY_SEPARATOR_REGEX = /\\/g;
6
- const LINE_ENDINGS_REGEX = /\r\n/g;
5
+ const FILE_REGEX = /\.[tj]sx?$/;
7
6
  let contexts = new Map();
8
7
  export default ({ name, onWatchChange, plugins }) => {
9
8
  return ({ root } = {}) => {
@@ -18,15 +17,16 @@ export default ({ name, onWatchChange, plugins }) => {
18
17
  return null;
19
18
  }
20
19
  try {
21
- let normalizedId = id.replace(DIRECTORY_SEPARATOR_REGEX, '/'), prog = program.get(root || ''), sourceFile = prog.getSourceFile(normalizedId) || prog.getSourceFile(id);
22
- if (sourceFile && sourceFile.getText().replace(LINE_ENDINGS_REGEX, '\n') !== code.replace(LINE_ENDINGS_REGEX, '\n')) {
23
- sourceFile = undefined;
24
- }
20
+ let normalizedId = id.replace(DIRECTORY_SEPARATOR_REGEX, '/'), prog = languageService.update(root || '', normalizedId, code), sourceFile = prog.getSourceFile(normalizedId);
25
21
  if (!sourceFile) {
26
- prog = coordinator.createPatchedProgram(prog, normalizedId, code);
27
- sourceFile = prog.getSourceFile(normalizedId) || ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
22
+ sourceFile = ts.createSourceFile(normalizedId, code, ts.ScriptTarget.Latest, true);
23
+ }
24
+ let key = root || '', ctx = contexts.get(key);
25
+ if (!ctx) {
26
+ ctx = new Map();
27
+ contexts.set(key, ctx);
28
28
  }
29
- let result = coordinator.transform(plugins, code, sourceFile, prog, contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || ''));
29
+ let result = coordinator.transform(plugins, code, sourceFile, prog, key, ctx);
30
30
  if (!result.changed) {
31
31
  return null;
32
32
  }
@@ -41,7 +41,7 @@ export default ({ name, onWatchChange, plugins }) => {
41
41
  if (FILE_REGEX.test(id)) {
42
42
  onWatchChange?.();
43
43
  contexts.delete(root || '');
44
- program.delete(root || '');
44
+ languageService.invalidate(root || '', id);
45
45
  }
46
46
  }
47
47
  };
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",
10
+ "@esportsplus/cli-passthrough": "^0.0.15",
11
+ "@esportsplus/utilities": "^0.27.3",
12
+ "@types/node": "^25.6.0",
13
13
  "tsc-alias": "^1.8.16",
14
- "typescript": "^5.9.3"
14
+ "typescript": "^6.0.2"
15
15
  },
16
16
  "devDependencies": {
17
- "vite": "^6.0.7"
17
+ "vite": "^8.0.8",
18
+ "vitest": "^4.1.4"
18
19
  },
19
20
  "exports": {
20
21
  "./package.json": "./package.json",
@@ -39,9 +40,11 @@
39
40
  },
40
41
  "type": "module",
41
42
  "types": "build/index.d.ts",
42
- "version": "0.28.5",
43
+ "version": "0.29.1",
43
44
  "scripts": {
45
+ "bench:run": "vitest bench --run",
44
46
  "build": "tsc && tsc-alias",
45
- "-": "-"
47
+ "-": "-",
48
+ "test": "vitest run"
46
49
  }
47
50
  }
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
 
@@ -54,6 +54,7 @@ async function build(config: object, tsconfig: string, pluginConfigs: PluginConf
54
54
  sourceFile.getFullText(),
55
55
  sourceFile,
56
56
  program,
57
+ root,
57
58
  shared
58
59
  );
59
60
 
@@ -226,4 +227,9 @@ function runTscAlias(args: string[]): Promise<number> {
226
227
  }
227
228
 
228
229
 
229
- 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,9 @@
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';
6
+ import languageService from './language-service';
4
7
 
5
8
 
6
9
  type CoordinatorResult = {
@@ -11,22 +14,39 @@ type CoordinatorResult = {
11
14
 
12
15
 
13
16
  function applyImports(code: string, file: ts.SourceFile, intents: ImportIntent[]): string {
17
+ let merged = new Map<string, { add?: string[]; namespace?: string; remove?: string[] }>();
18
+
14
19
  for (let i = 0, n = intents.length; i < n; i++) {
15
- let intent = intents[i];
20
+ let intent = intents[i],
21
+ existing = merged.get(intent.package);
16
22
 
17
- code = modify(code, file, intent.package, {
18
- add: intent.add,
19
- namespace: intent.namespace,
20
- remove: intent.remove
21
- });
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])!);
22
47
 
23
48
  if (i < n - 1) {
24
- file = ts.createSourceFile(
25
- file.fileName,
26
- code,
27
- file.languageVersion,
28
- true
29
- );
49
+ file = ts.createSourceFile(file.fileName, code, file.languageVersion, true);
30
50
  }
31
51
  }
32
52
 
@@ -67,10 +87,10 @@ function applyPrepend(code: string, file: ts.SourceFile, prepend: string[]): str
67
87
  }
68
88
 
69
89
  if (position === 0) {
70
- return prepend.join('\n') + code;
90
+ return prepend.join('\n') + '\n' + code;
71
91
  }
72
92
 
73
- 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);
74
94
  }
75
95
 
76
96
  function hasPattern(code: string, patterns: string[]): boolean {
@@ -157,45 +177,34 @@ function replaceReverse(code: string, replacements: Replacement[]): string {
157
177
 
158
178
  replacements.sort((a, b) => b.start - a.start);
159
179
 
160
- let result = code;
180
+ let parts: string[] = [],
181
+ pos = code.length;
161
182
 
162
183
  for (let i = 0, n = replacements.length; i < n; i++) {
163
184
  let r = replacements[i];
164
185
 
165
- 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;
192
+ }
193
+
194
+ if (pos > 0) {
195
+ parts.push(code.substring(0, pos));
166
196
  }
167
197
 
168
- return result;
198
+ return parts.reverse().join('');
169
199
  }
170
200
 
171
201
 
172
- const createPatchedProgram = (baseProgram: ts.Program, fileName: string, newContent: string) => {
173
- let baseHost = ts.createCompilerHost(baseProgram.getCompilerOptions()),
174
- options = baseProgram.getCompilerOptions();
175
-
176
- return ts.createProgram(
177
- baseProgram.getRootFileNames(),
178
- options,
179
- {
180
- ...baseHost,
181
- getSourceFile: (name, languageVersion) => {
182
- if (name === fileName || name === fileName.replace(/\\/g, '/')) {
183
- return ts.createSourceFile(name, newContent, languageVersion, true);
184
- }
185
- return baseProgram.getSourceFile(name) ?? baseHost.getSourceFile(name, languageVersion);
186
- },
187
- fileExists: (name) => baseHost.fileExists(name),
188
- readFile: (name) => name === fileName ? newContent : baseHost.readFile(name)
189
- },
190
- baseProgram
191
- );
192
- };
193
-
194
202
  const transform = (
195
203
  plugins: Plugin[],
196
204
  code: string,
197
205
  file: ts.SourceFile,
198
206
  program: ts.Program,
207
+ root: string,
199
208
  shared: SharedContext
200
209
  ) => {
201
210
  if (plugins.length === 0) {
@@ -206,7 +215,8 @@ const transform = (
206
215
  currentCode = code,
207
216
  currentFile = file,
208
217
  currentProgram = program,
209
- fileName = file.fileName;
218
+ fileName = file.fileName,
219
+ last = plugins.length - 1;
210
220
 
211
221
  for (let i = 0, n = plugins.length; i < n; i++) {
212
222
  let plugin = plugins[i];
@@ -230,20 +240,28 @@ const transform = (
230
240
  }
231
241
 
232
242
  if (prepend?.length) {
243
+ if (pluginChanged) {
244
+ currentFile = ts.createSourceFile(fileName, currentCode, file.languageVersion, true);
245
+ }
246
+
233
247
  currentCode = applyPrepend(currentCode, currentFile, prepend);
234
248
  pluginChanged = true;
235
249
  }
236
250
 
237
251
  if (imports?.length) {
252
+ if (pluginChanged) {
253
+ currentFile = ts.createSourceFile(fileName, currentCode, file.languageVersion, true);
254
+ }
255
+
238
256
  currentCode = applyImports(currentCode, currentFile, imports);
239
257
  pluginChanged = true;
240
258
  }
241
259
 
242
260
  if (pluginChanged) {
243
261
  changed = true;
244
- // Create patched program for next plugin
245
- if (i < n - 1) {
246
- currentProgram = createPatchedProgram(program, fileName, currentCode);
262
+
263
+ if (i < last) {
264
+ currentProgram = languageService.update(root, fileName, currentCode);
247
265
  currentFile = currentProgram.getSourceFile(fileName) ||
248
266
  ts.createSourceFile(fileName, currentCode, file.languageVersion, true);
249
267
  }
@@ -257,5 +275,5 @@ const transform = (
257
275
  };
258
276
 
259
277
 
260
- export default { createPatchedProgram, transform };
278
+ export default { transform };
261
279
  export type { CoordinatorResult };
@@ -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
  }
@@ -0,0 +1,121 @@
1
+ import { PACKAGE_NAME } from '~/constants';
2
+ import { ts } from '~/index';
3
+
4
+ import path from 'path';
5
+
6
+
7
+ type LanguageServiceEntry = {
8
+ contents: Map<string, string>;
9
+ host: ts.LanguageServiceHost;
10
+ rootFiles: Set<string>;
11
+ service: ts.LanguageService;
12
+ versions: Map<string, number>;
13
+ };
14
+
15
+
16
+ let cache = new Map<string, LanguageServiceEntry>();
17
+
18
+
19
+ function create(root: string): LanguageServiceEntry {
20
+ let tsconfig = ts.findConfigFile(root, ts.sys.fileExists, 'tsconfig.json');
21
+
22
+ if (!tsconfig) {
23
+ throw new Error(`${PACKAGE_NAME}: tsconfig.json not found`);
24
+ }
25
+
26
+ let file = ts.readConfigFile(tsconfig, ts.sys.readFile);
27
+
28
+ if (file.error) {
29
+ throw new Error(`${PACKAGE_NAME}: error reading tsconfig.json ${file.error.messageText}`);
30
+ }
31
+
32
+ let parsed = ts.parseJsonConfigFileContent(
33
+ file.config,
34
+ ts.sys,
35
+ path.dirname(tsconfig)
36
+ );
37
+
38
+ if (parsed.errors.length > 0) {
39
+ throw new Error(`${PACKAGE_NAME}: error parsing tsconfig.json ${parsed.errors[0].messageText}`);
40
+ }
41
+
42
+ let contents = new Map<string, string>(),
43
+ rootFiles = new Set(parsed.fileNames.map(f => f.replace(/\\/g, '/'))),
44
+ versions = new Map<string, number>();
45
+
46
+ for (let fileName of rootFiles) {
47
+ versions.set(fileName, 0);
48
+ }
49
+
50
+ let host: ts.LanguageServiceHost = {
51
+ fileExists: ts.sys.fileExists,
52
+ getCompilationSettings: () => parsed.options,
53
+ getCurrentDirectory: () => root,
54
+ getDefaultLibFileName: ts.getDefaultLibFilePath,
55
+ getScriptFileNames: () => [...rootFiles],
56
+ getScriptSnapshot: (fileName: string) => {
57
+ let content = contents.get(fileName);
58
+
59
+ if (content !== undefined) {
60
+ return ts.ScriptSnapshot.fromString(content);
61
+ }
62
+
63
+ if (!ts.sys.fileExists(fileName)) {
64
+ return undefined;
65
+ }
66
+
67
+ return ts.ScriptSnapshot.fromString(ts.sys.readFile(fileName) || '');
68
+ },
69
+ getScriptVersion: (fileName: string) => String(versions.get(fileName) || 0),
70
+ readFile: ts.sys.readFile
71
+ };
72
+
73
+ return { contents, host, rootFiles, service: ts.createLanguageService(host), versions };
74
+ }
75
+
76
+ function getEntry(root: string): LanguageServiceEntry {
77
+ let entry = cache.get(root);
78
+
79
+ if (!entry) {
80
+ entry = create(root);
81
+ cache.set(root, entry);
82
+ }
83
+
84
+ return entry;
85
+ }
86
+
87
+
88
+ const invalidate = (root: string, fileName: string): void => {
89
+ let entry = cache.get(root);
90
+
91
+ if (entry) {
92
+ let normalized = fileName.replace(/\\/g, '/');
93
+
94
+ entry.contents.delete(normalized);
95
+ entry.versions.set(normalized, (entry.versions.get(normalized) || 0) + 1);
96
+ }
97
+ };
98
+
99
+ const update = (root: string, fileName: string, content: string): ts.Program => {
100
+ let entry = getEntry(root),
101
+ normalized = fileName.replace(/\\/g, '/');
102
+
103
+ if (!entry.rootFiles.has(normalized)) {
104
+ entry.rootFiles.add(normalized);
105
+ }
106
+
107
+ entry.contents.set(normalized, content);
108
+ entry.versions.set(normalized, (entry.versions.get(normalized) || 0) + 1);
109
+
110
+ let program = entry.service.getProgram();
111
+
112
+ if (!program) {
113
+ throw new Error(`${PACKAGE_NAME}: failed to get program from language service`);
114
+ }
115
+
116
+ return program;
117
+ };
118
+
119
+
120
+ export default { invalidate, update };
121
+ export { invalidate, update };
@@ -1,8 +1,9 @@
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
- import program from '../program';
6
+ import languageService from '../language-service';
6
7
 
7
8
 
8
9
  type VitePlugin = {
@@ -20,11 +21,9 @@ type VitePluginOptions = {
20
21
  };
21
22
 
22
23
 
23
- const FILE_REGEX = /\.[tj]sx?$/;
24
-
25
24
  const DIRECTORY_SEPARATOR_REGEX = /\\/g;
26
25
 
27
- const LINE_ENDINGS_REGEX = /\r\n/g;
26
+ const FILE_REGEX = /\.[tj]sx?$/;
28
27
 
29
28
 
30
29
  let contexts = new Map<string, SharedContext>();
@@ -45,17 +44,19 @@ export default ({ name, onWatchChange, plugins }: VitePluginOptions) => {
45
44
 
46
45
  try {
47
46
  let normalizedId = id.replace(DIRECTORY_SEPARATOR_REGEX, '/'),
48
- prog = program.get(root || ''),
49
- sourceFile = prog.getSourceFile(normalizedId) || prog.getSourceFile(id);
47
+ prog = languageService.update(root || '', normalizedId, code),
48
+ sourceFile = prog.getSourceFile(normalizedId);
50
49
 
51
- // Check if file content matches (existing file may have changed)
52
- if (sourceFile && sourceFile.getText().replace(LINE_ENDINGS_REGEX, '\n') !== code.replace(LINE_ENDINGS_REGEX, '\n')) {
53
- sourceFile = undefined;
50
+ if (!sourceFile) {
51
+ sourceFile = ts.createSourceFile(normalizedId, code, ts.ScriptTarget.Latest, true);
54
52
  }
55
53
 
56
- if (!sourceFile) {
57
- prog = coordinator.createPatchedProgram(prog, normalizedId, code);
58
- sourceFile = prog.getSourceFile(normalizedId) || ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
54
+ let key = root || '',
55
+ ctx = contexts.get(key);
56
+
57
+ if (!ctx) {
58
+ ctx = new Map();
59
+ contexts.set(key, ctx);
59
60
  }
60
61
 
61
62
  let result = coordinator.transform(
@@ -63,7 +64,8 @@ export default ({ name, onWatchChange, plugins }: VitePluginOptions) => {
63
64
  code,
64
65
  sourceFile,
65
66
  prog,
66
- contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || '')!
67
+ key,
68
+ ctx
67
69
  );
68
70
 
69
71
  if (!result.changed) {
@@ -81,7 +83,7 @@ export default ({ name, onWatchChange, plugins }: VitePluginOptions) => {
81
83
  if (FILE_REGEX.test(id)) {
82
84
  onWatchChange?.();
83
85
  contexts.delete(root || '');
84
- program.delete(root || '');
86
+ languageService.invalidate(root || '', id);
85
87
  }
86
88
  }
87
89
  };