ttsc 0.3.1 → 0.4.0-dev.20260425
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/LICENSE +1 -1
- package/README.md +93 -362
- package/cmd/ttsc/build.go +128 -0
- package/cmd/ttsc/main.go +222 -0
- package/cmd/ttsc/main_test.go +288 -0
- package/cmd/ttsc/transform.go +166 -0
- package/driver/host.go +31 -0
- package/driver/program.go +298 -0
- package/driver/rewrite.go +315 -0
- package/go.mod +48 -0
- package/go.sum +22 -0
- package/lib/api.d.ts +91 -0
- package/lib/api.js +325 -0
- package/lib/api.js.map +1 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +481 -0
- package/lib/cli.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +35 -0
- package/lib/index.js.map +1 -0
- package/lib/native.d.ts +15 -0
- package/lib/native.js +34 -0
- package/lib/native.js.map +1 -0
- package/lib/platform.d.ts +66 -0
- package/lib/platform.js +170 -0
- package/lib/platform.js.map +1 -0
- package/lib/plugin.d.ts +47 -0
- package/lib/plugin.js +150 -0
- package/lib/plugin.js.map +1 -0
- package/lib/project.d.ts +32 -0
- package/lib/project.js +303 -0
- package/lib/project.js.map +1 -0
- package/lib/runner/cli.d.ts +1 -0
- package/lib/runner/cli.js +284 -0
- package/lib/runner/cli.js.map +1 -0
- package/lib/runner/register.d.ts +13 -0
- package/lib/runner/register.js +424 -0
- package/lib/runner/register.js.map +1 -0
- package/package.json +72 -63
- package/shim/ast/extra-shim.json +4 -0
- package/shim/ast/go.mod +5 -0
- package/shim/ast/shim.go +117 -0
- package/shim/bundled/extra-shim.json +1 -0
- package/shim/bundled/go.mod +5 -0
- package/shim/bundled/shim.go +19 -0
- package/shim/checker/extra-shim.json +31 -0
- package/shim/checker/go.mod +5 -0
- package/shim/checker/shim.go +146 -0
- package/shim/compiler/extra-shim.json +4 -0
- package/shim/compiler/go.mod +5 -0
- package/shim/compiler/shim.go +50 -0
- package/shim/core/extra-shim.json +4 -0
- package/shim/core/go.mod +5 -0
- package/shim/core/shim.go +11 -0
- package/shim/diagnosticwriter/go.mod +5 -0
- package/shim/diagnosticwriter/shim.go +29 -0
- package/shim/parser/extra-shim.json +4 -0
- package/shim/parser/go.mod +5 -0
- package/shim/parser/shim.go +55 -0
- package/shim/scanner/extra-shim.json +4 -0
- package/shim/scanner/go.mod +5 -0
- package/shim/scanner/shim.go +96 -0
- package/shim/tsoptions/extra-shim.json +4 -0
- package/shim/tsoptions/go.mod +5 -0
- package/shim/tsoptions/shim.go +123 -0
- package/shim/tspath/extra-shim.json +4 -0
- package/shim/tspath/go.mod +5 -0
- package/shim/tspath/shim.go +165 -0
- package/shim/vfs/cachedvfs/extra-shim.json +4 -0
- package/shim/vfs/cachedvfs/go.mod +5 -0
- package/shim/vfs/cachedvfs/shim.go +12 -0
- package/shim/vfs/extra-shim.json +4 -0
- package/shim/vfs/go.mod +5 -0
- package/shim/vfs/osvfs/extra-shim.json +4 -0
- package/shim/vfs/osvfs/go.mod +5 -0
- package/shim/vfs/osvfs/shim.go +13 -0
- package/shim/vfs/shim.go +19 -0
- package/src/api.ts +416 -0
- package/src/cli.ts +492 -0
- package/src/index.ts +19 -0
- package/src/launcher/ttsc-dev.js +42 -0
- package/src/launcher/ttsc.js +19 -0
- package/src/launcher/ttsx.js +19 -0
- package/src/native.ts +60 -0
- package/src/platform.ts +163 -0
- package/src/plugin.ts +241 -0
- package/src/project.ts +330 -0
- package/src/runner/cli.ts +286 -0
- package/src/runner/register.ts +487 -0
- package/test/native.test.ts +60 -0
- package/test/package.test.ts +131 -0
- package/test/platform.test.ts +58 -0
- package/test/project.test.ts +195 -0
- package/tools/gen_shims/go.mod +14 -0
- package/tools/gen_shims/go.sum +12 -0
- package/tools/gen_shims/main.go +446 -0
- package/CHANGELOG.md +0 -2
- package/bin/tsc +0 -2
- package/bin/tsserver +0 -2
- package/lib/PluginCreator.d.ts +0 -67
- package/lib/PluginCreator.js +0 -190
- package/lib/cancelationToken.d.ts +0 -0
- package/lib/cancelationToken.js +0 -2
- package/lib/loadTypescript.d.ts +0 -7
- package/lib/loadTypescript.js +0 -56
- package/lib/patchCreateProgram.d.ts +0 -21
- package/lib/patchCreateProgram.js +0 -190
- package/lib/tsc.d.ts +0 -1
- package/lib/tsc.js +0 -24
- package/lib/tsserver.d.ts +0 -0
- package/lib/tsserver.js +0 -2
- package/lib/tsserverlibrary.d.ts +0 -3
- package/lib/tsserverlibrary.js +0 -4
- package/lib/typescript.d.ts +0 -3
- package/lib/typescript.js +0 -4
- package/lib/typescriptServices.d.ts +0 -3
- package/lib/typescriptServices.js +0 -4
- package/lib/typingsInstaller.d.ts +0 -0
- package/lib/typingsInstaller.js +0 -2
- package/lib/watchGuard.d.ts +0 -0
- package/lib/watchGuard.js +0 -2
package/src/project.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
export interface ProjectLocatorOptions {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
tsconfig?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ProjectPluginConfig {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
transform?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ParsedProjectConfig {
|
|
19
|
+
compilerOptions: {
|
|
20
|
+
outDir?: string;
|
|
21
|
+
plugins: ProjectPluginConfig[];
|
|
22
|
+
};
|
|
23
|
+
path: string;
|
|
24
|
+
root: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveWorkingDirectory(cwd?: string): string {
|
|
28
|
+
return path.resolve(cwd ?? process.cwd());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveAbsolutePath(cwd: string, target: string): string {
|
|
32
|
+
return path.isAbsolute(target) ? target : path.resolve(cwd, target);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveProjectConfig(opts: ProjectLocatorOptions = {}): string {
|
|
36
|
+
const cwd = resolveWorkingDirectory(opts.cwd);
|
|
37
|
+
if (opts.tsconfig) {
|
|
38
|
+
const resolved = resolveAbsolutePath(cwd, opts.tsconfig);
|
|
39
|
+
if (!fs.existsSync(resolved)) {
|
|
40
|
+
throw new Error(`ttsc: tsconfig not found: ${resolved}`);
|
|
41
|
+
}
|
|
42
|
+
return resolveRealPath(resolved);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const start = opts.file
|
|
46
|
+
? resolveRealPath(resolveAbsolutePath(cwd, opts.file))
|
|
47
|
+
: cwd;
|
|
48
|
+
const from = isDirectory(start) ? start : path.dirname(start);
|
|
49
|
+
const found = findUp(from, ["tsconfig.json", "jsconfig.json"]);
|
|
50
|
+
if (!found) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`ttsc: could not find tsconfig.json or jsconfig.json starting from ${from}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return resolveRealPath(found);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveProjectRoot(opts: ProjectLocatorOptions = {}): string {
|
|
59
|
+
return path.dirname(resolveProjectConfig(opts));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function defaultCacheDirectory(projectRoot: string, tool: string): string {
|
|
63
|
+
return path.join(projectRoot, "node_modules", ".cache", "ttsc", tool);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function readProjectConfig(opts: ProjectLocatorOptions = {}): ParsedProjectConfig {
|
|
67
|
+
const tsconfig = resolveProjectConfig(opts);
|
|
68
|
+
const root = path.dirname(tsconfig);
|
|
69
|
+
const compilerOptions = readResolvedCompilerOptions(tsconfig);
|
|
70
|
+
return {
|
|
71
|
+
compilerOptions: {
|
|
72
|
+
outDir: compilerOptions.outDir,
|
|
73
|
+
plugins: compilerOptions.plugins,
|
|
74
|
+
},
|
|
75
|
+
path: tsconfig,
|
|
76
|
+
root,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CacheKeyInput {
|
|
81
|
+
file: string;
|
|
82
|
+
tsconfig: string;
|
|
83
|
+
version: string;
|
|
84
|
+
mode?: string;
|
|
85
|
+
extra?: readonly string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createTransformCacheKey(input: CacheKeyInput): string {
|
|
89
|
+
const hash = crypto.createHash("sha256");
|
|
90
|
+
hash.update("file\0");
|
|
91
|
+
hash.update(fs.readFileSync(input.file));
|
|
92
|
+
hash.update("\0tsconfig\0");
|
|
93
|
+
hash.update(fs.readFileSync(input.tsconfig));
|
|
94
|
+
hash.update(`\0version\0${input.version}`);
|
|
95
|
+
hash.update(`\0mode\0${input.mode ?? ""}`);
|
|
96
|
+
for (const item of input.extra ?? []) {
|
|
97
|
+
hash.update(`\0extra\0${item}`);
|
|
98
|
+
}
|
|
99
|
+
return hash.digest("hex");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findUp(from: string, names: readonly string[]): string | null {
|
|
103
|
+
let current = path.resolve(from);
|
|
104
|
+
while (true) {
|
|
105
|
+
for (const name of names) {
|
|
106
|
+
const candidate = path.join(current, name);
|
|
107
|
+
if (fs.existsSync(candidate)) {
|
|
108
|
+
return candidate;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const parent = path.dirname(current);
|
|
112
|
+
if (parent === current) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
current = parent;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isDirectory(location: string): boolean {
|
|
120
|
+
try {
|
|
121
|
+
return fs.statSync(location).isDirectory();
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isProjectPluginConfig(value: unknown): value is ProjectPluginConfig {
|
|
128
|
+
return typeof value === "object" && value !== null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface RawTsconfig {
|
|
132
|
+
extends?: unknown;
|
|
133
|
+
compilerOptions?: {
|
|
134
|
+
outDir?: unknown;
|
|
135
|
+
plugins?: unknown;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface ResolvedCompilerOptions {
|
|
140
|
+
outDir?: string;
|
|
141
|
+
plugins: ProjectPluginConfig[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveRealPath(location: string): string {
|
|
145
|
+
try {
|
|
146
|
+
return fs.realpathSync(location);
|
|
147
|
+
} catch {
|
|
148
|
+
return location;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readResolvedCompilerOptions(
|
|
153
|
+
tsconfig: string,
|
|
154
|
+
seen: Set<string> = new Set(),
|
|
155
|
+
): ResolvedCompilerOptions {
|
|
156
|
+
const canonical = resolveRealPath(tsconfig);
|
|
157
|
+
if (seen.has(canonical)) {
|
|
158
|
+
throw new Error(`ttsc: circular tsconfig extends detected: ${canonical}`);
|
|
159
|
+
}
|
|
160
|
+
seen.add(canonical);
|
|
161
|
+
|
|
162
|
+
const parsed = parseJsonc(fs.readFileSync(canonical, "utf8")) as RawTsconfig;
|
|
163
|
+
const own = parsed.compilerOptions;
|
|
164
|
+
const base =
|
|
165
|
+
typeof parsed.extends === "string"
|
|
166
|
+
? readResolvedCompilerOptions(resolveExtendsConfig(canonical, parsed.extends), seen)
|
|
167
|
+
: { plugins: [] };
|
|
168
|
+
return {
|
|
169
|
+
outDir:
|
|
170
|
+
typeof own?.outDir === "string"
|
|
171
|
+
? resolveAbsolutePath(path.dirname(canonical), own.outDir)
|
|
172
|
+
: base.outDir,
|
|
173
|
+
plugins: Array.isArray(own?.plugins)
|
|
174
|
+
? own.plugins.filter(isProjectPluginConfig)
|
|
175
|
+
: base.plugins,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resolveExtendsConfig(tsconfig: string, specifier: string): string {
|
|
180
|
+
const baseDir = path.dirname(tsconfig);
|
|
181
|
+
if (path.isAbsolute(specifier)) {
|
|
182
|
+
return resolveExistingExtendsPath(specifier);
|
|
183
|
+
}
|
|
184
|
+
if (isRelativePluginSpecifier(specifier)) {
|
|
185
|
+
return resolveExistingExtendsPath(path.resolve(baseDir, specifier));
|
|
186
|
+
}
|
|
187
|
+
const resolver = createRequire(tsconfig);
|
|
188
|
+
try {
|
|
189
|
+
return resolveRealPath(resolver.resolve(specifier));
|
|
190
|
+
} catch {
|
|
191
|
+
return resolveRealPath(resolver.resolve(`${specifier}.json`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resolveExistingExtendsPath(location: string): string {
|
|
196
|
+
const candidates = new Set<string>([
|
|
197
|
+
location,
|
|
198
|
+
`${location}.json`,
|
|
199
|
+
path.join(location, "tsconfig.json"),
|
|
200
|
+
]);
|
|
201
|
+
for (const candidate of candidates) {
|
|
202
|
+
if (fs.existsSync(candidate)) {
|
|
203
|
+
return resolveRealPath(candidate);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
throw new Error(`ttsc: extended tsconfig not found: ${location}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isRelativePluginSpecifier(specifier: string): boolean {
|
|
210
|
+
return (
|
|
211
|
+
specifier === "." ||
|
|
212
|
+
specifier === ".." ||
|
|
213
|
+
specifier.startsWith("./") ||
|
|
214
|
+
specifier.startsWith("../") ||
|
|
215
|
+
specifier.startsWith(".\\") ||
|
|
216
|
+
specifier.startsWith("..\\")
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parseJsonc(input: string): unknown {
|
|
221
|
+
return JSON.parse(stripTrailingCommas(stripComments(input)));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function stripComments(input: string): string {
|
|
225
|
+
let output = "";
|
|
226
|
+
let inBlockComment = false;
|
|
227
|
+
let inLineComment = false;
|
|
228
|
+
let inString = false;
|
|
229
|
+
let quote = "";
|
|
230
|
+
let escape = false;
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
233
|
+
const current = input[i]!;
|
|
234
|
+
const next = input[i + 1];
|
|
235
|
+
|
|
236
|
+
if (inBlockComment) {
|
|
237
|
+
if (current === "*" && next === "/") {
|
|
238
|
+
inBlockComment = false;
|
|
239
|
+
i += 1;
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (inLineComment) {
|
|
244
|
+
if (current === "\n") {
|
|
245
|
+
inLineComment = false;
|
|
246
|
+
output += current;
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (inString) {
|
|
251
|
+
output += current;
|
|
252
|
+
if (escape) {
|
|
253
|
+
escape = false;
|
|
254
|
+
} else if (current === "\\") {
|
|
255
|
+
escape = true;
|
|
256
|
+
} else if (current === quote) {
|
|
257
|
+
inString = false;
|
|
258
|
+
quote = "";
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if ((current === '"' || current === "'") && !inString) {
|
|
264
|
+
inString = true;
|
|
265
|
+
quote = current;
|
|
266
|
+
output += current;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (current == "/" && next === "/") {
|
|
270
|
+
inLineComment = true;
|
|
271
|
+
i += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (current == "/" && next === "*") {
|
|
275
|
+
inBlockComment = true;
|
|
276
|
+
i += 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
output += current;
|
|
280
|
+
}
|
|
281
|
+
return output;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function stripTrailingCommas(input: string): string {
|
|
285
|
+
let output = "";
|
|
286
|
+
let inString = false;
|
|
287
|
+
let quote = "";
|
|
288
|
+
let escape = false;
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
291
|
+
const current = input[i]!;
|
|
292
|
+
if (inString) {
|
|
293
|
+
output += current;
|
|
294
|
+
if (escape) {
|
|
295
|
+
escape = false;
|
|
296
|
+
} else if (current === "\\") {
|
|
297
|
+
escape = true;
|
|
298
|
+
} else if (current === quote) {
|
|
299
|
+
inString = false;
|
|
300
|
+
quote = "";
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (current === '"' || current === "'") {
|
|
306
|
+
inString = true;
|
|
307
|
+
quote = current;
|
|
308
|
+
output += current;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (current === ",") {
|
|
312
|
+
const next = nextNonWhitespace(input, i + 1);
|
|
313
|
+
if (next === "}" || next === "]") {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
output += current;
|
|
318
|
+
}
|
|
319
|
+
return output;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function nextNonWhitespace(input: string, from: number): string | undefined {
|
|
323
|
+
for (let i = from; i < input.length; i += 1) {
|
|
324
|
+
const current = input[i]!;
|
|
325
|
+
if (/\s/.test(current) === false) {
|
|
326
|
+
return current;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import Module = require("node:module");
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { version } from "../api";
|
|
7
|
+
import {
|
|
8
|
+
prepareExecution,
|
|
9
|
+
register,
|
|
10
|
+
type PreparedExecution,
|
|
11
|
+
type RegisterOptions,
|
|
12
|
+
} from "./register";
|
|
13
|
+
|
|
14
|
+
interface ParsedCLI extends RegisterOptions {
|
|
15
|
+
entry: string;
|
|
16
|
+
passthrough: string[];
|
|
17
|
+
preload: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
21
|
+
try {
|
|
22
|
+
return run(argv);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
process.stderr.write(`${formatError(error)}\n`);
|
|
25
|
+
return 2;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function run(argv: readonly string[]): number {
|
|
30
|
+
const parsed = parseCLI(argv);
|
|
31
|
+
if (parsed === "help") {
|
|
32
|
+
printHelp();
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
if (parsed === "version") {
|
|
36
|
+
process.stdout.write(`${version().replace(/^ttsc\b/, "ttsx")}\n`);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const cwd = path.resolve(parsed.cwd ?? process.cwd());
|
|
41
|
+
const entry = path.resolve(cwd, parsed.entry);
|
|
42
|
+
if (!fs.existsSync(entry)) {
|
|
43
|
+
process.stderr.write(`ttsx: entry not found: ${entry}\n`);
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const prepared = prepareExecution(entry, {
|
|
48
|
+
binary: parsed.binary,
|
|
49
|
+
cacheDir: parsed.cacheDir,
|
|
50
|
+
cwd,
|
|
51
|
+
env: parsed.env,
|
|
52
|
+
extensions: parsed.extensions,
|
|
53
|
+
project: parsed.project,
|
|
54
|
+
});
|
|
55
|
+
if (prepared.moduleKind === "esm") {
|
|
56
|
+
return runEsmEntry(parsed, prepared, cwd);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const unregister = register({
|
|
60
|
+
binary: parsed.binary,
|
|
61
|
+
cacheDir: parsed.cacheDir,
|
|
62
|
+
cwd,
|
|
63
|
+
env: parsed.env,
|
|
64
|
+
extensions: parsed.extensions,
|
|
65
|
+
project: parsed.project,
|
|
66
|
+
});
|
|
67
|
+
process.once("exit", unregister);
|
|
68
|
+
for (const preload of parsed.preload) {
|
|
69
|
+
require(resolvePreload(cwd, preload));
|
|
70
|
+
}
|
|
71
|
+
process.argv = [process.execPath, entry, ...parsed.passthrough];
|
|
72
|
+
Module.runMain();
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatError(error: unknown): string {
|
|
77
|
+
if (error instanceof Error) {
|
|
78
|
+
return error.message;
|
|
79
|
+
}
|
|
80
|
+
return String(error);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseCLI(argv: readonly string[]): ParsedCLI | "help" | "version" {
|
|
84
|
+
const preload: string[] = [];
|
|
85
|
+
const passthroughIndex = argv.indexOf("--");
|
|
86
|
+
const head = passthroughIndex === -1 ? [...argv] : [...argv.slice(0, passthroughIndex)];
|
|
87
|
+
const passthrough = passthroughIndex === -1 ? [] : [...argv.slice(passthroughIndex + 1)];
|
|
88
|
+
|
|
89
|
+
let binary: string | undefined;
|
|
90
|
+
let cacheDir: string | undefined;
|
|
91
|
+
let cwd: string | undefined;
|
|
92
|
+
let entry: string | undefined;
|
|
93
|
+
let project: string | undefined;
|
|
94
|
+
|
|
95
|
+
while (head.length !== 0) {
|
|
96
|
+
const current = head.shift()!;
|
|
97
|
+
if (entry) {
|
|
98
|
+
passthrough.push(current, ...head);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
switch (current) {
|
|
102
|
+
case "-h":
|
|
103
|
+
case "--help":
|
|
104
|
+
return "help";
|
|
105
|
+
case "-v":
|
|
106
|
+
case "--version":
|
|
107
|
+
return "version";
|
|
108
|
+
case "-P":
|
|
109
|
+
case "--project":
|
|
110
|
+
project = takeValue(current, head);
|
|
111
|
+
break;
|
|
112
|
+
case "--cwd":
|
|
113
|
+
cwd = takeValue(current, head);
|
|
114
|
+
break;
|
|
115
|
+
case "--cache-dir":
|
|
116
|
+
cacheDir = takeValue(current, head);
|
|
117
|
+
break;
|
|
118
|
+
case "-r":
|
|
119
|
+
case "--require":
|
|
120
|
+
preload.push(takeValue(current, head));
|
|
121
|
+
break;
|
|
122
|
+
case "--binary":
|
|
123
|
+
binary = takeValue(current, head);
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
if (current.startsWith("--project=")) {
|
|
127
|
+
project = current.slice("--project=".length);
|
|
128
|
+
} else if (current.startsWith("--cwd=")) {
|
|
129
|
+
cwd = current.slice("--cwd=".length);
|
|
130
|
+
} else if (current.startsWith("--cache-dir=")) {
|
|
131
|
+
cacheDir = current.slice("--cache-dir=".length);
|
|
132
|
+
} else if (current.startsWith("--binary=")) {
|
|
133
|
+
binary = current.slice("--binary=".length);
|
|
134
|
+
} else if (current.startsWith("-")) {
|
|
135
|
+
throw new Error(`ttsx: unknown option ${current}`);
|
|
136
|
+
} else {
|
|
137
|
+
entry = current;
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!entry) {
|
|
144
|
+
throw new Error("ttsx: entry file is required");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
binary,
|
|
149
|
+
cacheDir,
|
|
150
|
+
cwd,
|
|
151
|
+
entry,
|
|
152
|
+
passthrough,
|
|
153
|
+
preload,
|
|
154
|
+
project,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function printHelp(): void {
|
|
159
|
+
process.stdout.write(
|
|
160
|
+
[
|
|
161
|
+
"ttsx — TypeScript runner provided by ttsc.",
|
|
162
|
+
"",
|
|
163
|
+
"Usage:",
|
|
164
|
+
" ttsx [options] <entry.ts> [-- <argv...>]",
|
|
165
|
+
"",
|
|
166
|
+
"Options:",
|
|
167
|
+
" -P, --project <file> Use an explicit tsconfig.json",
|
|
168
|
+
" --cwd <dir> Resolve entry/project relative to this directory",
|
|
169
|
+
" --cache-dir <dir> Override the compiled JS cache directory",
|
|
170
|
+
" --binary <path> Use an explicit ttsc native binary",
|
|
171
|
+
" -r, --require <file> Preload a module before the entrypoint",
|
|
172
|
+
" -h, --help Show this help",
|
|
173
|
+
" -v, --version Print the runner version",
|
|
174
|
+
"",
|
|
175
|
+
"Runtime:",
|
|
176
|
+
" CommonJS projects execute in-process via the require hook.",
|
|
177
|
+
" ESM projects execute the cached emitted entry in a child Node process.",
|
|
178
|
+
"",
|
|
179
|
+
"Examples:",
|
|
180
|
+
" ttsx src/index.ts",
|
|
181
|
+
" ttsx --project tsconfig.json src/index.ts -- --port 3000",
|
|
182
|
+
].join("\n"),
|
|
183
|
+
);
|
|
184
|
+
process.stdout.write("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolvePreload(cwd: string, preload: string): string {
|
|
188
|
+
if (preload.startsWith(".") || preload.startsWith("/") || preload.includes(path.sep)) {
|
|
189
|
+
return path.resolve(cwd, preload);
|
|
190
|
+
}
|
|
191
|
+
return preload;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function takeValue(flag: string, rest: string[]): string {
|
|
195
|
+
const value = rest.shift();
|
|
196
|
+
if (!value) {
|
|
197
|
+
throw new Error(`ttsx: ${flag} requires a value`);
|
|
198
|
+
}
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function runEsmEntry(parsed: ParsedCLI, execution: PreparedExecution, cwd: string): number {
|
|
203
|
+
fs.mkdirSync(execution.emitDir, { recursive: true });
|
|
204
|
+
rewriteEsmSpecifiers(execution.emitDir);
|
|
205
|
+
fs.writeFileSync(
|
|
206
|
+
path.join(execution.emitDir, "package.json"),
|
|
207
|
+
JSON.stringify({ type: "module" }),
|
|
208
|
+
"utf8",
|
|
209
|
+
);
|
|
210
|
+
const args = [
|
|
211
|
+
...parsed.preload.flatMap((preload) => ["-r", resolvePreload(cwd, preload)]),
|
|
212
|
+
execution.entryFile,
|
|
213
|
+
...parsed.passthrough,
|
|
214
|
+
];
|
|
215
|
+
const result = spawnSync(process.execPath, args, {
|
|
216
|
+
stdio: "inherit",
|
|
217
|
+
env: process.env,
|
|
218
|
+
windowsHide: true,
|
|
219
|
+
});
|
|
220
|
+
if (result.error) {
|
|
221
|
+
process.stderr.write(`${result.error.message}\n`);
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
return result.status ?? 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function rewriteEsmSpecifiers(root: string): void {
|
|
228
|
+
const stack = [root];
|
|
229
|
+
while (stack.length !== 0) {
|
|
230
|
+
const current = stack.pop()!;
|
|
231
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
232
|
+
const next = path.join(current, entry.name);
|
|
233
|
+
if (entry.isDirectory()) {
|
|
234
|
+
stack.push(next);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!entry.isFile() || !isJavaScriptOutput(next)) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const before = fs.readFileSync(next, "utf8");
|
|
241
|
+
const after = before
|
|
242
|
+
.replace(
|
|
243
|
+
/\bfrom\s+(['"])(\.[^'"]+)\1/g,
|
|
244
|
+
(_, quote: string, specifier: string) =>
|
|
245
|
+
`from ${quote}${withResolvableExtension(next, specifier)}${quote}`,
|
|
246
|
+
)
|
|
247
|
+
.replace(
|
|
248
|
+
/\bimport\(\s*(['"])(\.[^'"]+)\1\s*\)/g,
|
|
249
|
+
(_, quote: string, specifier: string) =>
|
|
250
|
+
`import(${quote}${withResolvableExtension(next, specifier)}${quote})`,
|
|
251
|
+
);
|
|
252
|
+
if (after !== before) {
|
|
253
|
+
fs.writeFileSync(next, after, "utf8");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function withResolvableExtension(fromFile: string, specifier: string): string {
|
|
260
|
+
if (!specifier.startsWith(".")) {
|
|
261
|
+
return specifier;
|
|
262
|
+
}
|
|
263
|
+
if (/\.(?:[cm]?js|json|node)$/i.test(specifier)) {
|
|
264
|
+
return specifier;
|
|
265
|
+
}
|
|
266
|
+
const [pathname, suffix = ""] = splitSpecifierSuffix(specifier);
|
|
267
|
+
const fromDir = path.dirname(fromFile);
|
|
268
|
+
for (const extension of [".js", ".mjs", ".cjs"]) {
|
|
269
|
+
if (fs.existsSync(path.resolve(fromDir, pathname + extension))) {
|
|
270
|
+
return pathname + extension + suffix;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return `${specifier}.js`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function splitSpecifierSuffix(specifier: string): [string, string?] {
|
|
277
|
+
const index = specifier.search(/[?#]/);
|
|
278
|
+
if (index === -1) {
|
|
279
|
+
return [specifier];
|
|
280
|
+
}
|
|
281
|
+
return [specifier.slice(0, index), specifier.slice(index)];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isJavaScriptOutput(filename: string): boolean {
|
|
285
|
+
return /\.(?:[cm]?js)$/i.test(filename);
|
|
286
|
+
}
|