mirror of
https://github.com/swc-project/swc.git
synced 2024-12-23 05:32:09 +03:00
feat(node): Use wasm as a fallback (#5233)
This commit is contained in:
parent
0c7be690da
commit
1cebf626e6
@ -11,10 +11,27 @@ import {
|
||||
} from "./types";
|
||||
export * from "./types";
|
||||
import { BundleInput, compileBundleOptions } from "./spack";
|
||||
import * as assert from "assert";
|
||||
|
||||
// Allow overrides to the location of the .node binding file
|
||||
const bindingsOverride = process.env["SWC_BINARY_PATH"];
|
||||
const bindings = !!bindingsOverride ? require(resolve(bindingsOverride)) : require('./binding');
|
||||
let fallbackBindings: any;
|
||||
const bindings = (() => {
|
||||
let binding
|
||||
try {
|
||||
binding = !!bindingsOverride ? require(resolve(bindingsOverride)) : require('./binding')
|
||||
|
||||
// If native binding loaded successfully, it should return proper target triple constant.
|
||||
const triple = binding.getTargetTriple();
|
||||
assert.ok(triple, 'Failed to read target triple from native binary.');
|
||||
return binding;
|
||||
} catch (_) {
|
||||
// postinstall supposed to install `@swc/wasm` already
|
||||
fallbackBindings = require('@swc/wasm');
|
||||
} finally {
|
||||
return binding;
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Version of the swc binding.
|
||||
@ -34,11 +51,21 @@ export function plugins(ps: Plugin[]): Plugin {
|
||||
export class Compiler {
|
||||
|
||||
async minify(src: string, opts?: JsMinifyOptions): Promise<Output> {
|
||||
if (!bindings && !!fallbackBindings) {
|
||||
throw new Error('Fallback bindings does not support this interface yet.');
|
||||
} else if (!bindings) {
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
return bindings.minify(toBuffer(src), toBuffer(opts ?? {}));
|
||||
}
|
||||
|
||||
minifySync(src: string, opts?: JsMinifyOptions): Output {
|
||||
return bindings.minifySync(toBuffer(src), toBuffer(opts ?? {}));
|
||||
if (bindings) {
|
||||
return bindings.minifySync(toBuffer(src), toBuffer(opts ?? {}));
|
||||
} else if (fallbackBindings) {
|
||||
return fallbackBindings.minifySync(src, opts);
|
||||
}
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
parse(
|
||||
@ -50,6 +77,12 @@ export class Compiler {
|
||||
options = options || { syntax: "ecmascript" };
|
||||
options.syntax = options.syntax || "ecmascript";
|
||||
|
||||
if (!bindings && !!fallbackBindings) {
|
||||
throw new Error('Fallback bindings does not support this interface yet.');
|
||||
} else if (!bindings) {
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
const res = await bindings.parse(src, toBuffer(options), filename);
|
||||
return JSON.parse(res);
|
||||
}
|
||||
@ -60,7 +93,13 @@ export class Compiler {
|
||||
options = options || { syntax: "ecmascript" };
|
||||
options.syntax = options.syntax || "ecmascript";
|
||||
|
||||
return JSON.parse(bindings.parseSync(src, toBuffer(options), filename));
|
||||
if (bindings) {
|
||||
return JSON.parse(bindings.parseSync(src, toBuffer(options), filename));
|
||||
} else if (fallbackBindings) {
|
||||
return JSON.parse(fallbackBindings.parseSync(src, options));
|
||||
}
|
||||
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
parseFile(
|
||||
@ -72,6 +111,12 @@ export class Compiler {
|
||||
options = options || { syntax: "ecmascript" };
|
||||
options.syntax = options.syntax || "ecmascript";
|
||||
|
||||
if (!bindings && !!fallbackBindings) {
|
||||
throw new Error('Fallback bindings does not support filesystem access.');
|
||||
} else if (!bindings) {
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
const res = await bindings.parseFile(path, toBuffer(options));
|
||||
|
||||
return JSON.parse(res);
|
||||
@ -86,6 +131,12 @@ export class Compiler {
|
||||
options = options || { syntax: "ecmascript" };
|
||||
options.syntax = options.syntax || "ecmascript";
|
||||
|
||||
if (!bindings && !!fallbackBindings) {
|
||||
throw new Error('Fallback bindings does not support filesystem access');
|
||||
} else if (!bindings) {
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
return JSON.parse(bindings.parseFileSync(path, toBuffer(options)));
|
||||
}
|
||||
|
||||
@ -96,6 +147,12 @@ export class Compiler {
|
||||
async print(m: Program, options?: Options): Promise<Output> {
|
||||
options = options || {};
|
||||
|
||||
if (!bindings && !!fallbackBindings) {
|
||||
throw new Error('Fallback bindings does not support this interface yet.');
|
||||
} else if (!bindings) {
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
return bindings.print(JSON.stringify(m), toBuffer(options))
|
||||
}
|
||||
|
||||
@ -106,10 +163,22 @@ export class Compiler {
|
||||
printSync(m: Program, options?: Options): Output {
|
||||
options = options || {};
|
||||
|
||||
return bindings.printSync(JSON.stringify(m), toBuffer(options));
|
||||
if (bindings) {
|
||||
return bindings.printSync(JSON.stringify(m), toBuffer(options));
|
||||
} else if (fallbackBindings) {
|
||||
return fallbackBindings.printSync(JSON.stringify(m), options);
|
||||
}
|
||||
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
async transform(src: string | Program, options?: Options): Promise<Output> {
|
||||
if (!bindings && !!fallbackBindings) {
|
||||
throw new Error('Fallback bindings does not support this interface yet.');
|
||||
} else if (!bindings) {
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
const isModule = typeof src !== "string";
|
||||
options = options || {};
|
||||
|
||||
@ -117,7 +186,6 @@ export class Compiler {
|
||||
options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript';
|
||||
}
|
||||
|
||||
|
||||
const { plugin, ...newOptions } = options;
|
||||
|
||||
if (plugin) {
|
||||
@ -139,23 +207,34 @@ export class Compiler {
|
||||
options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript';
|
||||
}
|
||||
|
||||
if (bindings) {
|
||||
const { plugin, ...newOptions } = options;
|
||||
|
||||
const { plugin, ...newOptions } = options;
|
||||
if (plugin) {
|
||||
const m =
|
||||
typeof src === "string" ? this.parseSync(src, options?.jsc?.parser, options.filename) : src;
|
||||
return this.transformSync(plugin(m), newOptions);
|
||||
}
|
||||
|
||||
if (plugin) {
|
||||
const m =
|
||||
typeof src === "string" ? this.parseSync(src, options?.jsc?.parser, options.filename) : src;
|
||||
return this.transformSync(plugin(m), newOptions);
|
||||
return bindings.transformSync(
|
||||
isModule ? JSON.stringify(src) : src,
|
||||
isModule,
|
||||
toBuffer(newOptions),
|
||||
)
|
||||
} else if (fallbackBindings) {
|
||||
return fallbackBindings.transformSync(src, options);
|
||||
}
|
||||
|
||||
return bindings.transformSync(
|
||||
isModule ? JSON.stringify(src) : src,
|
||||
isModule,
|
||||
toBuffer(newOptions),
|
||||
)
|
||||
throw new Error("Bindings not found");
|
||||
}
|
||||
|
||||
async transformFile(path: string, options?: Options): Promise<Output> {
|
||||
if (!bindings && !!fallbackBindings) {
|
||||
throw new Error('Fallback bindings does not support filesystem access.');
|
||||
} else if (!bindings) {
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (options?.jsc?.parser) {
|
||||
@ -174,13 +253,18 @@ export class Compiler {
|
||||
}
|
||||
|
||||
transformFileSync(path: string, options?: Options): Output {
|
||||
if (!bindings && !!fallbackBindings) {
|
||||
throw new Error('Fallback bindings does not support filesystem access.');
|
||||
} else if (!bindings) {
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (options?.jsc?.parser) {
|
||||
options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript';
|
||||
}
|
||||
|
||||
|
||||
const { plugin, ...newOptions } = options;
|
||||
newOptions.filename = path;
|
||||
|
||||
@ -194,6 +278,12 @@ export class Compiler {
|
||||
|
||||
|
||||
async bundle(options?: BundleInput | string): Promise<{ [name: string]: Output }> {
|
||||
if (!bindings && !!fallbackBindings) {
|
||||
throw new Error('Fallback bindings does not support this interface yet.');
|
||||
} else if (!bindings) {
|
||||
throw new Error('Bindings not found.');
|
||||
}
|
||||
|
||||
const opts = await compileBundleOptions(options);
|
||||
|
||||
if (Array.isArray(opts)) {
|
||||
@ -329,8 +419,12 @@ export function __experimental_registerGlobalTraceConfig(traceConfig: {
|
||||
type: 'traceEvent',
|
||||
fileName?: string
|
||||
}) {
|
||||
if (traceConfig.type === 'traceEvent') {
|
||||
bindings.initCustomTraceSubscriber(traceConfig.fileName);
|
||||
// Do not raise error if binding doesn't exists - fallback binding will not support
|
||||
// this ever.
|
||||
if (bindings) {
|
||||
if (traceConfig.type === 'traceEvent') {
|
||||
bindings.initCustomTraceSubscriber(traceConfig.fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
124
node-swc/src/postinstall.ts
Normal file
124
node-swc/src/postinstall.ts
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* A postinstall script runs after `@swc/core` is installed.
|
||||
*
|
||||
* It checks if corresponding optional dependencies for native binary is installed and can be loaded properly.
|
||||
* If it fails, it'll internally try to install `@swc/wasm` as fallback.
|
||||
*/
|
||||
import { existsSync } from "fs";
|
||||
import * as assert from "assert";
|
||||
import * as path from "path";
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
|
||||
function removeRecursive(dir: string): void {
|
||||
for (const entry of fs.readdirSync(dir)) {
|
||||
const entryPath = path.join(dir, entry);
|
||||
let stats;
|
||||
try {
|
||||
stats = fs.lstatSync(entryPath);
|
||||
} catch {
|
||||
continue; // Guard against https://github.com/nodejs/node/issues/4760
|
||||
}
|
||||
if (stats.isDirectory()) removeRecursive(entryPath);
|
||||
else fs.unlinkSync(entryPath);
|
||||
}
|
||||
fs.rmdirSync(dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trying to validate @swc/core's native binary installation, then installs if it is not supported.
|
||||
*/
|
||||
const validateBinary = async () => {
|
||||
try {
|
||||
const { name } = require(path.resolve(process.env.INIT_CWD!, 'package.json'));
|
||||
if (name === '@swc/core') {
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: We do not take care of the case if user try to install with `--no-optional`.
|
||||
// For now, it is considered as deliberate decision.
|
||||
let binding
|
||||
try {
|
||||
binding = require('./binding');
|
||||
|
||||
// Check if binding binary actually works.
|
||||
// For the latest version, checks target triple. If it's old version doesn't have target triple, use parseSync instead.
|
||||
const triple = binding.getTargetTriple ? binding.getTargetTriple() : binding.parseSync('console.log()', Buffer.from(JSON.stringify({ syntax: "ecmascript" })));
|
||||
assert.ok(triple, 'Failed to read target triple from native binary.');
|
||||
} catch (error: any) {
|
||||
// if error is unsupported architecture, ignore to display.
|
||||
if (!error.message?.includes('Unsupported architecture')) {
|
||||
console.warn(error);
|
||||
}
|
||||
|
||||
console.warn(`@swc/core was not able to resolve native bindings installation. It'll try to use @swc/wasm as fallback instead.`)
|
||||
}
|
||||
|
||||
if (!!binding) {
|
||||
return;
|
||||
}
|
||||
|
||||
// User choose to override the binary installation. Skip remanining validation.
|
||||
if (!!process.env["SWC_BINARY_PATH"]) {
|
||||
console.warn(
|
||||
`@swc/core could not resolve native bindings installation, but found manual override config SWC_BINARY_PATH specified. Skipping remaning validation.`)
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if top-level package.json installs @swc/wasm separately already
|
||||
let wasmBinding;
|
||||
try {
|
||||
wasmBinding = require.resolve(`@swc/wasm`);
|
||||
} catch (_) {
|
||||
|
||||
}
|
||||
|
||||
if (!!wasmBinding && existsSync(wasmBinding)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const env = { ...process.env, npm_config_global: undefined };
|
||||
const { version } = require(path.join(path.dirname(require.resolve('@swc/core')), 'package.json'));
|
||||
|
||||
// We want to place @swc/wasm next to the @swc/core as if normal installation was done,
|
||||
// but can't directly set cwd to INIT_CWD as npm seems to acquire lock to the working dir.
|
||||
// Instead, create a temporary inner and move it out.
|
||||
const coreDir = path.dirname(require.resolve('@swc/core'));
|
||||
const installDir = path.join(coreDir, 'npm-install');
|
||||
|
||||
try {
|
||||
fs.mkdirSync(installDir);
|
||||
fs.writeFileSync(path.join(installDir, 'package.json'), '{}');
|
||||
|
||||
// Instead of carrying over own dependencies to download & resolve package which increases installation sizes of `@swc/core`,
|
||||
// assume & relies on system's npm installation.
|
||||
child_process.execSync(`npm install --no-save --loglevel=error --prefer-offline --no-audit --progress=false @swc/wasm@${version}`,
|
||||
{ cwd: installDir, stdio: 'pipe', env });
|
||||
|
||||
const installedBinPath = path.join(installDir, 'node_modules', `@swc/wasm`);
|
||||
// INIT_CWD is injected via npm. If it doesn't exists, can't proceed.
|
||||
fs.renameSync(installedBinPath, path.resolve(process.env.INIT_CWD!, 'node_modules', `@swc/wasm`));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
console.error(
|
||||
`Failed to install fallback @swc/wasm@${version}. @swc/core will not properly.
|
||||
Please install @swc/wasm manually, or retry whole installation.
|
||||
If there are unexpected errors, please report at https://github.com/swc-project/swc/issues`);
|
||||
} finally {
|
||||
try {
|
||||
removeRecursive(installDir);
|
||||
} catch (_) {
|
||||
// Gracefully ignore any failures. This'll make few leftover files but it shouldn't block installation.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
validateBinary().catch((error) => {
|
||||
// for now just throw the error as-is.
|
||||
throw error;
|
||||
});
|
@ -54,6 +54,7 @@
|
||||
"scripts": {
|
||||
"changelog": "git cliff --output CHANGELOG.md",
|
||||
"prepare": "husky install && git config feature.manyFiles true && node ./crates/swc_ecma_preset_env/scripts/copy-data.js",
|
||||
"postinstall": "node postinstall.js",
|
||||
"artifacts": "napi artifacts --dist scripts/npm",
|
||||
"prepublishOnly": "tsc -d && napi prepublish -p scripts/npm --tagstyle npm",
|
||||
"pack": "wasm-pack",
|
||||
@ -167,6 +168,7 @@
|
||||
"binding.js",
|
||||
"package.json",
|
||||
"spack.d.ts",
|
||||
"types.js"
|
||||
"types.js",
|
||||
"postinstall.js"
|
||||
]
|
||||
}
|
1
postinstall.js
Normal file
1
postinstall.js
Normal file
@ -0,0 +1 @@
|
||||
// This'll be generated by build process for the installation of the `@swc/core` package.
|
Loading…
Reference in New Issue
Block a user