@tsrx/oxc 0.0.0-trusted-publishing-bootstrap → 0.8.0
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 +21 -0
- package/README.md +141 -0
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/bin/oxc-tsrx +2 -0
- package/bin/oxc-tsrx-fmt +2 -0
- package/bin/oxc-tsrx-lint +2 -0
- package/bin/oxc-tsrx-lsp +2 -0
- package/bin/oxfmt +2 -0
- package/bin/oxlint +2 -0
- package/dist/bin/oxc-tsrx-fmt.js +13 -0
- package/dist/bin/oxc-tsrx-lint.js +13 -0
- package/dist/bin/oxc-tsrx-lsp.js +13 -0
- package/dist/bin/oxc-tsrx.js +115 -0
- package/dist/bin/oxfmt.js +24 -0
- package/dist/bin/oxlint.js +33 -0
- package/dist/canonical-command.d.ts +50 -0
- package/dist/canonical-command.js +196 -0
- package/dist/compat.d.ts +149 -0
- package/dist/compat.js +1615 -0
- package/dist/editor-resolution.js +508 -0
- package/dist/format-cli.js +276 -0
- package/dist/format-invocation.js +97 -0
- package/dist/format.d.ts +1 -0
- package/dist/format.js +56 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -0
- package/dist/lint-cli.js +487 -0
- package/dist/lint-invocation.js +192 -0
- package/dist/lint-js-plugins.js +819 -0
- package/dist/lint-plugins-dev.d.ts +1 -0
- package/dist/lint-plugins-dev.js +2 -0
- package/dist/lint-prestart.js +16 -0
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +2 -0
- package/dist/native-targets.js +76 -0
- package/dist/oxlint-lsp-multiplexer.js +622 -0
- package/dist/package-binary.js +29 -0
- package/dist/parser.d.ts +216 -0
- package/dist/parser.js +557 -0
- package/dist/process.js +88 -0
- package/dist/provider-resolve.d.ts +160 -0
- package/dist/provider-resolve.js +471 -0
- package/dist/providers-report.js +49 -0
- package/dist/runtime.js +323 -0
- package/dist/spawn-command.d.ts +20 -0
- package/dist/spawn-command.js +87 -0
- package/dist/tsrx-core-compat/facade.js +1184 -0
- package/dist/tsrx-core-compat/index.d.ts +6 -0
- package/dist/tsrx-core-compat/index.js +9 -0
- package/dist/tsrx-core-compat/style.js +525 -0
- package/dist/tsrx-core-compat/types/estree.d.ts +20 -0
- package/dist/tsrx-core-compat/types/index.d.ts +50 -0
- package/dist/tsrx-transfer.js +352 -0
- package/package.json +144 -5
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { nativePackageName, nativeTargetForHost } from "./native-targets.js";
|
|
2
|
+
import { resolvePackageBinary } from "./package-binary.js";
|
|
3
|
+
import { runCaptured, runPassthrough } from "./process.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, isAbsolute, join, parse, resolve, sep } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { existsSync, statSync } from "node:fs";
|
|
10
|
+
//#region src/runtime.ts
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const runtimeManifest = require("../package.json");
|
|
13
|
+
const NATIVE_PROTOCOL_VERSION = 2;
|
|
14
|
+
const OXC_REVISION = "8e0ed2ebb96137fb1611cdbd5742d5cb46037d40";
|
|
15
|
+
const ENVIRONMENTS = {
|
|
16
|
+
lint: "OXC_TSRX_LINT_BIN",
|
|
17
|
+
format: "OXC_TSRX_FORMAT_BIN",
|
|
18
|
+
server: "OXC_TSRX_LSP_BIN"
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Protocol 2 ships one multi-call native executable instead of three. A
|
|
22
|
+
* platform package that carried `oxc-tsrx`, `oxc-tsrx-fmt`, and `oxc-tsrx-lsp`
|
|
23
|
+
* linked the same oxc parser, linter, and formatter three times; one binary
|
|
24
|
+
* that dispatches on `argv[0]` and on a leading subcommand is a little over
|
|
25
|
+
* half the download.
|
|
26
|
+
*/
|
|
27
|
+
const EXECUTABLE = process.platform === "win32" ? "oxc-tsrx.exe" : "oxc-tsrx";
|
|
28
|
+
/**
|
|
29
|
+
* Arguments that select a tool inside the multi-call binary. Every JS caller
|
|
30
|
+
* uses the explicit subcommand rather than the `argv[0]` form, because
|
|
31
|
+
* `argv[0]` is not dependable across hosts: a Windows `.cmd` shim and anything
|
|
32
|
+
* that resolves a symlink before exec both report the real file name. Linting
|
|
33
|
+
* needs no subcommand, so `oxc-tsrx FILE...` is unchanged from protocol 1.
|
|
34
|
+
*/
|
|
35
|
+
const SUBCOMMANDS = {
|
|
36
|
+
lint: [],
|
|
37
|
+
format: ["fmt"],
|
|
38
|
+
server: ["lsp"]
|
|
39
|
+
};
|
|
40
|
+
const VITE_CONFIG_FILES = [
|
|
41
|
+
"vite.config.ts",
|
|
42
|
+
"vite.config.mts",
|
|
43
|
+
"vite.config.cts",
|
|
44
|
+
"vite.config.js",
|
|
45
|
+
"vite.config.mjs",
|
|
46
|
+
"vite.config.cjs"
|
|
47
|
+
];
|
|
48
|
+
function linuxLibc() {
|
|
49
|
+
if (process.platform !== "linux") return null;
|
|
50
|
+
return (process.report?.getReport?.())?.header?.glibcVersionRuntime ? "glibc" : "musl";
|
|
51
|
+
}
|
|
52
|
+
function platformPackage() {
|
|
53
|
+
return nativePackageName(nativeTargetForHost(process.platform, process.arch, linuxLibc()));
|
|
54
|
+
}
|
|
55
|
+
function assertExecutable(path, source) {
|
|
56
|
+
let metadata;
|
|
57
|
+
try {
|
|
58
|
+
metadata = statSync(path);
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error(`OXC for TSRX native artifact is missing at ${path} (${source})`);
|
|
61
|
+
}
|
|
62
|
+
if (!metadata.isFile()) throw new Error(`OXC for TSRX native artifact is not a file at ${path} (${source})`);
|
|
63
|
+
if (process.platform !== "win32" && (metadata.mode & 73) === 0) throw new Error(`OXC for TSRX native artifact is not executable at ${path} (${source})`);
|
|
64
|
+
return path;
|
|
65
|
+
}
|
|
66
|
+
function validateNativeManifest(manifest, packageName, executable) {
|
|
67
|
+
const metadata = manifest.oxcTsrx;
|
|
68
|
+
if (manifest.version !== runtimeManifest.version) throw new Error(`OXC for TSRX native package ${packageName} has version ${manifest.version}; runtime ${runtimeManifest.version} requires an exact match`);
|
|
69
|
+
if (metadata?.nativeProtocolVersion !== NATIVE_PROTOCOL_VERSION) throw new Error(`OXC for TSRX native package ${packageName} has unsupported protocol ${metadata?.nativeProtocolVersion ?? "unknown"}; expected ${NATIVE_PROTOCOL_VERSION}`);
|
|
70
|
+
const expectedTarget = nativeTargetForHost(process.platform, process.arch, linuxLibc()).target;
|
|
71
|
+
if (metadata.target !== expectedTarget) throw new Error(`OXC for TSRX native package ${packageName} targets ${metadata.target}; this process requires ${expectedTarget}`);
|
|
72
|
+
if (metadata.oxcRevision !== OXC_REVISION) throw new Error(`OXC for TSRX native package ${packageName} pins OXC ${metadata.oxcRevision}; runtime ${runtimeManifest.version} requires ${OXC_REVISION}`);
|
|
73
|
+
if (!Array.isArray(metadata.binaries) || !metadata.binaries.includes(executable)) throw new Error(`OXC for TSRX native package ${packageName} does not declare ${executable}`);
|
|
74
|
+
}
|
|
75
|
+
function resolveNativeBinary(kind) {
|
|
76
|
+
const environment = ENVIRONMENTS[kind];
|
|
77
|
+
if (!environment || !SUBCOMMANDS[kind]) throw new Error(`unknown native binary kind: ${kind}`);
|
|
78
|
+
const explicit = process.env[environment];
|
|
79
|
+
if (explicit) return assertExecutable(resolve(explicit), environment);
|
|
80
|
+
const packageName = platformPackage();
|
|
81
|
+
let packageRoot;
|
|
82
|
+
try {
|
|
83
|
+
const manifestPath = require.resolve(`${packageName}/package.json`);
|
|
84
|
+
validateNativeManifest(require(manifestPath), packageName, EXECUTABLE);
|
|
85
|
+
packageRoot = dirname(manifestPath);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
88
|
+
throw new Error(`OXC for TSRX native package ${packageName} is unavailable; install it or set ${environment}. ${detail}`);
|
|
89
|
+
}
|
|
90
|
+
return assertExecutable(join(packageRoot, "bin", EXECUTABLE), packageName);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The subcommand arguments a native invocation of `kind` must lead with. Every
|
|
94
|
+
* caller that spawns a native binary prepends these to its own argument vector.
|
|
95
|
+
*/
|
|
96
|
+
function nativeSubcommand(kind) {
|
|
97
|
+
const subcommand = SUBCOMMANDS[kind];
|
|
98
|
+
if (!subcommand) throw new Error(`unknown native binary kind: ${kind}`);
|
|
99
|
+
return [...subcommand];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The complete native invocation for `kind`: the multi-call executable plus the
|
|
103
|
+
* caller's arguments behind the subcommand that selects the tool.
|
|
104
|
+
*/
|
|
105
|
+
function resolveNativeCommand(kind, args = []) {
|
|
106
|
+
return {
|
|
107
|
+
executable: resolveNativeBinary(kind),
|
|
108
|
+
args: [...nativeSubcommand(kind), ...args]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function findViteConfig(cwd) {
|
|
112
|
+
let directory = resolve(cwd);
|
|
113
|
+
const root = parse(directory).root;
|
|
114
|
+
for (;;) {
|
|
115
|
+
for (const file of VITE_CONFIG_FILES) {
|
|
116
|
+
const candidate = join(directory, file);
|
|
117
|
+
if (existsSync(candidate)) return candidate;
|
|
118
|
+
}
|
|
119
|
+
if (directory === root) return null;
|
|
120
|
+
directory = dirname(directory);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function moduleEntry(manifest) {
|
|
124
|
+
const rootExport = manifest.exports?.["."];
|
|
125
|
+
if (typeof rootExport === "string") return rootExport;
|
|
126
|
+
if (rootExport && typeof rootExport === "object") return rootExport.import ?? rootExport.default ?? rootExport.require;
|
|
127
|
+
return manifest.module ?? manifest.main;
|
|
128
|
+
}
|
|
129
|
+
function strictConfigJson(config, field) {
|
|
130
|
+
const ancestors = [];
|
|
131
|
+
return JSON.stringify(config, function serialize(key, value) {
|
|
132
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") throw new TypeError(`Vite+ ${field} config contains non-JSON value ${key || "<root>"}; the native TSRX lane requires serializable Oxlint/Oxfmt options`);
|
|
133
|
+
if (value && typeof value === "object") {
|
|
134
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== this) ancestors.pop();
|
|
135
|
+
if (ancestors.includes(value)) throw new TypeError(`Vite+ ${field} config contains a circular object graph`);
|
|
136
|
+
ancestors.push(value);
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function requiresAuthoredConfigBase(field, config) {
|
|
142
|
+
return (field === "lint" ? [
|
|
143
|
+
"extends",
|
|
144
|
+
"overrides",
|
|
145
|
+
"ignorePatterns",
|
|
146
|
+
"jsPlugins"
|
|
147
|
+
] : ["overrides", "ignorePatterns"]).some((name) => {
|
|
148
|
+
const value = config[name];
|
|
149
|
+
return Array.isArray(value) ? value.length > 0 : value !== void 0 && value !== null;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve Vite+'s public universal config once in the thin Node host and write only
|
|
154
|
+
* the selected Oxlint/Oxfmt field to a disposable JSON file for the native process.
|
|
155
|
+
*/
|
|
156
|
+
async function prepareVitePlusConfig(field, cwd = process.cwd(), explicitConfig = null) {
|
|
157
|
+
if (!Boolean(process.env.VP_VERSION || process.env.VP_COMMAND || process.env.NODE_PACKAGE_MANAGER === "vite-plus")) return null;
|
|
158
|
+
const configFile = explicitConfig ? isAbsolute(explicitConfig) ? explicitConfig : resolve(cwd, explicitConfig) : findViteConfig(cwd);
|
|
159
|
+
if (configFile === null) return null;
|
|
160
|
+
const manifestPath = createRequire(join(resolve(cwd), "package.json")).resolve("vite-plus/package.json");
|
|
161
|
+
const packageRoot = dirname(manifestPath);
|
|
162
|
+
const entry = moduleEntry(JSON.parse(await readFile(manifestPath, "utf8")));
|
|
163
|
+
if (!entry) throw new Error("installed Vite+ package has no public module entry");
|
|
164
|
+
const vitePlus = await import(pathToFileURL(resolve(packageRoot, entry)).href);
|
|
165
|
+
if (typeof vitePlus.resolveConfig !== "function") throw new Error("installed Vite+ package does not export public resolveConfig");
|
|
166
|
+
const selected = (await vitePlus.resolveConfig({ configFile }, "build"))[field] ?? {};
|
|
167
|
+
if (!selected || typeof selected !== "object" || Array.isArray(selected)) throw new TypeError(`Vite+ ${field} config must resolve to an object`);
|
|
168
|
+
const directory = await mkdtemp(join(tmpdir(), `oxc-tsrx-vite-plus-${field}-`));
|
|
169
|
+
const path = join(directory, field === "lint" ? ".oxlintrc.json" : ".oxfmtrc.json");
|
|
170
|
+
try {
|
|
171
|
+
await writeFile(path, `${strictConfigJson(selected, field)}\n`);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
await rm(directory, {
|
|
174
|
+
recursive: true,
|
|
175
|
+
force: true
|
|
176
|
+
});
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
path,
|
|
181
|
+
source: configFile,
|
|
182
|
+
base: dirname(configFile),
|
|
183
|
+
requiresAuthoredBase: requiresAuthoredConfigBase(field, selected),
|
|
184
|
+
typeAware: field === "lint" && selected.options?.typeAware === true,
|
|
185
|
+
typeCheck: field === "lint" && selected.options?.typeCheck === true,
|
|
186
|
+
async cleanup() {
|
|
187
|
+
await rm(directory, {
|
|
188
|
+
recursive: true,
|
|
189
|
+
force: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function isViteConfigPath(path) {
|
|
195
|
+
if (!path) return false;
|
|
196
|
+
return VITE_CONFIG_FILES.some((name) => path.endsWith(name));
|
|
197
|
+
}
|
|
198
|
+
function replaceConfigArgument(args, configPath) {
|
|
199
|
+
const output = [];
|
|
200
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
201
|
+
const argument = args[index];
|
|
202
|
+
if (argument === "-c" || argument === "--config") {
|
|
203
|
+
index += 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (argument.startsWith("-c=") || argument.startsWith("-c") && argument.length > 2 || argument.startsWith("--config=")) continue;
|
|
207
|
+
output.push(argument);
|
|
208
|
+
}
|
|
209
|
+
const terminator = output.indexOf("--");
|
|
210
|
+
const values = ["--config", configPath];
|
|
211
|
+
if (terminator === -1) output.push(...values);
|
|
212
|
+
else output.splice(terminator, 0, ...values);
|
|
213
|
+
return output;
|
|
214
|
+
}
|
|
215
|
+
function canonicalToolEnvironment(useResolvedViteConfig) {
|
|
216
|
+
if (!useResolvedViteConfig) return process.env;
|
|
217
|
+
const environment = { ...process.env };
|
|
218
|
+
delete environment.VP_VERSION;
|
|
219
|
+
return environment;
|
|
220
|
+
}
|
|
221
|
+
function positionalIndices(args, valueOptions) {
|
|
222
|
+
const indices = [];
|
|
223
|
+
let positionalOnly = false;
|
|
224
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
225
|
+
const argument = args[index];
|
|
226
|
+
if (positionalOnly) {
|
|
227
|
+
indices.push(index);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (argument === "--") {
|
|
231
|
+
positionalOnly = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (!argument.startsWith("-") || argument === "-") {
|
|
235
|
+
indices.push(index);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!argument.includes("=") && valueOptions.has(argument)) index += 1;
|
|
239
|
+
}
|
|
240
|
+
return indices;
|
|
241
|
+
}
|
|
242
|
+
function removeExplicitTsrx(args, valueOptions) {
|
|
243
|
+
const positions = positionalIndices(args, valueOptions);
|
|
244
|
+
const removed = new Set(positions.filter((index) => args[index].split("?")[0].endsWith(".tsrx")));
|
|
245
|
+
return {
|
|
246
|
+
args: args.filter((_, index) => !removed.has(index)),
|
|
247
|
+
hadPositionals: positions.length > 0,
|
|
248
|
+
remainingPositionals: positions.length - removed.size
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function slash(path) {
|
|
252
|
+
return sep === "/" ? path : path.split(sep).join("/");
|
|
253
|
+
}
|
|
254
|
+
function hasMagic(path) {
|
|
255
|
+
return /[*?[\]{}()!]/u.test(path);
|
|
256
|
+
}
|
|
257
|
+
async function classifyPattern(raw, cwd, positives, patterns) {
|
|
258
|
+
const negative = raw.startsWith("!");
|
|
259
|
+
const value = negative ? raw.slice(1) : raw;
|
|
260
|
+
const absolute = isAbsolute(value) ? value : resolve(cwd, value);
|
|
261
|
+
if (!hasMagic(value)) try {
|
|
262
|
+
const metadata = await stat(absolute);
|
|
263
|
+
if (metadata.isFile()) {
|
|
264
|
+
if (!negative && absolute.endsWith(".tsrx")) positives.add(absolute);
|
|
265
|
+
else if (negative) patterns.push(`!${slash(absolute)}`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (metadata.isDirectory()) {
|
|
269
|
+
patterns.push(`${negative ? "!" : ""}${slash(join(absolute, "**/*.tsrx"))}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
} catch {}
|
|
273
|
+
patterns.push(`${negative ? "!" : ""}${slash(value)}`);
|
|
274
|
+
}
|
|
275
|
+
async function classifyPatterns(inputs, cwd, positives, patterns) {
|
|
276
|
+
const classified = await Promise.all(inputs.map(async (input) => {
|
|
277
|
+
const entryPositives = /* @__PURE__ */ new Set();
|
|
278
|
+
const entryPatterns = [];
|
|
279
|
+
await classifyPattern(input, cwd, entryPositives, entryPatterns);
|
|
280
|
+
return {
|
|
281
|
+
entryPositives,
|
|
282
|
+
entryPatterns
|
|
283
|
+
};
|
|
284
|
+
}));
|
|
285
|
+
for (const { entryPositives, entryPatterns } of classified) {
|
|
286
|
+
for (const positive of entryPositives) positives.add(positive);
|
|
287
|
+
for (const pattern of entryPatterns) patterns.push(pattern);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function discoverTsrxFiles(positionals, cwd = process.cwd()) {
|
|
291
|
+
const positives = /* @__PURE__ */ new Set();
|
|
292
|
+
const patterns = [];
|
|
293
|
+
await classifyPatterns(positionals.length === 0 ? ["."] : positionals, cwd, positives, patterns);
|
|
294
|
+
if (patterns.length > 0) {
|
|
295
|
+
const { glob } = await import("tinyglobby");
|
|
296
|
+
const matches = await glob(patterns, {
|
|
297
|
+
cwd,
|
|
298
|
+
absolute: true,
|
|
299
|
+
onlyFiles: true,
|
|
300
|
+
dot: true,
|
|
301
|
+
followSymbolicLinks: false,
|
|
302
|
+
ignore: ["**/node_modules/**", "**/.git/**"]
|
|
303
|
+
});
|
|
304
|
+
for (const match of matches) if (match.endsWith(".tsrx")) positives.add(resolve(match));
|
|
305
|
+
}
|
|
306
|
+
return [...positives].sort();
|
|
307
|
+
}
|
|
308
|
+
function argumentValue(args, names) {
|
|
309
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
310
|
+
const argument = args[index];
|
|
311
|
+
if (names.has(argument)) return args[index + 1] ?? null;
|
|
312
|
+
for (const name of names) {
|
|
313
|
+
if (argument.startsWith(`${name}=`)) return argument.slice(name.length + 1);
|
|
314
|
+
if (name.length === 2 && argument.startsWith(name) && argument.length > name.length) return argument.slice(name.length);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
function ensureSupportedOutput(format, files) {
|
|
320
|
+
if (files.length > 0 && format !== "default" && format !== "json") throw new Error(`OXC for TSRX currently combines default and json lint output; ${format} is unavailable for mixed .tsrx runs`);
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
export { argumentValue, canonicalToolEnvironment, discoverTsrxFiles, ensureSupportedOutput, isViteConfigPath, platformPackage, prepareVitePlusConfig, removeExplicitTsrx, replaceConfigArgument, resolveNativeBinary, resolveNativeCommand, resolvePackageBinary, runCaptured, runPassthrough };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface OxcTsrxCommandInvocation {
|
|
2
|
+
readonly file: string;
|
|
3
|
+
readonly args: string[];
|
|
4
|
+
readonly windowsVerbatimArguments: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export declare function escapeCommandArgument(argument: string): string;
|
|
8
|
+
|
|
9
|
+
export declare function resolveCommandInvocation(
|
|
10
|
+
file: string,
|
|
11
|
+
args?: readonly string[],
|
|
12
|
+
platform?: NodeJS.Platform,
|
|
13
|
+
): OxcTsrxCommandInvocation;
|
|
14
|
+
|
|
15
|
+
export declare function spawnCommand(
|
|
16
|
+
file: string,
|
|
17
|
+
args?: readonly string[],
|
|
18
|
+
options?: import("node:child_process").SpawnOptions,
|
|
19
|
+
spawnProcess?: typeof import("node:child_process").spawn,
|
|
20
|
+
): import("node:child_process").ChildProcess;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
//#region src/spawn-command.ts
|
|
4
|
+
/**
|
|
5
|
+
* Executing a declared `bin` target on every host this package publishes for.
|
|
6
|
+
*
|
|
7
|
+
* On POSIX the target is simply the file: it is executable, and `spawn` runs
|
|
8
|
+
* it. Windows has two shapes this package must expect and one it must not use.
|
|
9
|
+
*
|
|
10
|
+
* The shape it must expect is a batch launcher. `npm` writes `<name>.cmd` (and
|
|
11
|
+
* `<name>.ps1`, and a POSIX `sh` script under the bare name) into
|
|
12
|
+
* `node_modules/.bin` instead of a symlink, and a package is free to declare a
|
|
13
|
+
* `.cmd` or `.bat` file as its own `bin` entry. Windows cannot execute either
|
|
14
|
+
* one directly: only `cmd.exe` can, and it re-parses the whole command line, so
|
|
15
|
+
* every argument has to be escaped for `cmd.exe` rather than for `CreateProcess`.
|
|
16
|
+
*
|
|
17
|
+
* The shape it must not use is `shell: true`. Node concatenates `args` into the
|
|
18
|
+
* command line unescaped in that mode — which is exactly the injection this
|
|
19
|
+
* escaping exists to prevent — and since Node 22 it emits DEP0190 for it.
|
|
20
|
+
*
|
|
21
|
+
* Recent libuv escapes batch arguments itself, so on a new enough Node a plain
|
|
22
|
+
* `spawn("x.cmd", args)` is already safe. This module does not depend on that:
|
|
23
|
+
* `oxc-tsrx` supports Node 20.19 and up across eight published platforms, the
|
|
24
|
+
* behaviour differs by libuv version rather than by Node major, and the whole
|
|
25
|
+
* point of a launcher is that it behaves the same everywhere. Handing `cmd.exe`
|
|
26
|
+
* a verbatim, pre-escaped command line is correct under either libuv, because
|
|
27
|
+
* `cmd.exe` is an ordinary executable and never takes the batch path at all.
|
|
28
|
+
*
|
|
29
|
+
* This is the ~20 lines of `cross-spawn` that this package actually needs. It
|
|
30
|
+
* does not need that package's `PATH`/`PATHEXT` search (every path here is
|
|
31
|
+
* already absolute and resolved from a manifest) or its shebang re-targeting
|
|
32
|
+
* (a Node wrapper is detected and imported in process before spawning is even
|
|
33
|
+
* considered), and adding it would pull `path-key`, `shebang-command`,
|
|
34
|
+
* `shebang-regex`, `which`, and `isexe` into a published dependency graph.
|
|
35
|
+
*/
|
|
36
|
+
/** Extensions Windows will only run through a command interpreter. */
|
|
37
|
+
const BATCH_EXTENSIONS = /* @__PURE__ */ new Set([".cmd", ".bat"]);
|
|
38
|
+
/**
|
|
39
|
+
* Characters `cmd.exe` acts on rather than passes through. A caret escapes each
|
|
40
|
+
* one; the set is `cross-spawn`'s, which is the de facto reference for this.
|
|
41
|
+
*/
|
|
42
|
+
const COMMAND_METACHARACTERS = /([()\][%!^"`<>&|;, *?])/gu;
|
|
43
|
+
function isBatchFile(file, platform = process.platform) {
|
|
44
|
+
return platform === "win32" && BATCH_EXTENSIONS.has(extname(String(file)).toLowerCase());
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Quote one argument so `cmd.exe` reproduces it exactly. Backslashes that
|
|
48
|
+
* precede a quote (or the closing quote this adds) are doubled first, because
|
|
49
|
+
* the Windows command-line parser treats `\"` as a literal quote.
|
|
50
|
+
*/
|
|
51
|
+
function escapeCommandArgument(argument) {
|
|
52
|
+
return `"${String(argument).replaceAll(/(\\*)"/gu, "$1$1\\\"").replace(/(\\*)$/u, "$1$1")}"`.replaceAll(COMMAND_METACHARACTERS, "^$1");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The file, argv, and options to hand `spawn`/`spawnSync` for `file`.
|
|
56
|
+
*
|
|
57
|
+
* `platform` is a parameter so the Windows branch is assertable from any host;
|
|
58
|
+
* a lane that could only run this on Windows would never be run at all.
|
|
59
|
+
*/
|
|
60
|
+
function resolveCommandInvocation(file, args = [], platform = process.platform) {
|
|
61
|
+
if (!isBatchFile(file, platform)) return {
|
|
62
|
+
file,
|
|
63
|
+
args: [...args],
|
|
64
|
+
windowsVerbatimArguments: false
|
|
65
|
+
};
|
|
66
|
+
const command = [String(file).replaceAll(COMMAND_METACHARACTERS, "^$1"), ...args.map((argument) => escapeCommandArgument(argument))].join(" ");
|
|
67
|
+
return {
|
|
68
|
+
file: process.env.ComSpec || process.env.comspec || "cmd.exe",
|
|
69
|
+
args: [
|
|
70
|
+
"/d",
|
|
71
|
+
"/s",
|
|
72
|
+
"/c",
|
|
73
|
+
`"${command}"`
|
|
74
|
+
],
|
|
75
|
+
windowsVerbatimArguments: true
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** `child_process.spawn`, with the Windows batch-launcher case handled. */
|
|
79
|
+
function spawnCommand(file, args = [], options = {}, spawnProcess = spawn) {
|
|
80
|
+
const invocation = resolveCommandInvocation(file, args);
|
|
81
|
+
return spawnProcess(invocation.file, invocation.args, {
|
|
82
|
+
...options,
|
|
83
|
+
...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { escapeCommandArgument, resolveCommandInvocation, spawnCommand };
|