feat(node): Use wasm as a fallback (#5233)

This commit is contained in:
OJ Kwon 2022-07-17 02:33:51 -07:00 committed by GitHub
parent 0c7be690da
commit 1cebf626e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 240 additions and 19 deletions

View File

@ -11,10 +11,27 @@ import {
} from "./types"; } from "./types";
export * from "./types"; export * from "./types";
import { BundleInput, compileBundleOptions } from "./spack"; import { BundleInput, compileBundleOptions } from "./spack";
import * as assert from "assert";
// Allow overrides to the location of the .node binding file // Allow overrides to the location of the .node binding file
const bindingsOverride = process.env["SWC_BINARY_PATH"]; 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. * Version of the swc binding.
@ -34,11 +51,21 @@ export function plugins(ps: Plugin[]): Plugin {
export class Compiler { export class Compiler {
async minify(src: string, opts?: JsMinifyOptions): Promise<Output> { 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 ?? {})); return bindings.minify(toBuffer(src), toBuffer(opts ?? {}));
} }
minifySync(src: string, opts?: JsMinifyOptions): Output { minifySync(src: string, opts?: JsMinifyOptions): Output {
if (bindings) {
return bindings.minifySync(toBuffer(src), toBuffer(opts ?? {})); return bindings.minifySync(toBuffer(src), toBuffer(opts ?? {}));
} else if (fallbackBindings) {
return fallbackBindings.minifySync(src, opts);
}
throw new Error('Bindings not found.');
} }
parse( parse(
@ -50,6 +77,12 @@ export class Compiler {
options = options || { syntax: "ecmascript" }; options = options || { syntax: "ecmascript" };
options.syntax = 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); const res = await bindings.parse(src, toBuffer(options), filename);
return JSON.parse(res); return JSON.parse(res);
} }
@ -60,7 +93,13 @@ export class Compiler {
options = options || { syntax: "ecmascript" }; options = options || { syntax: "ecmascript" };
options.syntax = options.syntax || "ecmascript"; options.syntax = options.syntax || "ecmascript";
if (bindings) {
return JSON.parse(bindings.parseSync(src, toBuffer(options), filename)); 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( parseFile(
@ -72,6 +111,12 @@ export class Compiler {
options = options || { syntax: "ecmascript" }; options = options || { syntax: "ecmascript" };
options.syntax = 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)); const res = await bindings.parseFile(path, toBuffer(options));
return JSON.parse(res); return JSON.parse(res);
@ -86,6 +131,12 @@ export class Compiler {
options = options || { syntax: "ecmascript" }; options = options || { syntax: "ecmascript" };
options.syntax = 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))); return JSON.parse(bindings.parseFileSync(path, toBuffer(options)));
} }
@ -96,6 +147,12 @@ export class Compiler {
async print(m: Program, options?: Options): Promise<Output> { async print(m: Program, options?: Options): Promise<Output> {
options = options || {}; 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)) return bindings.print(JSON.stringify(m), toBuffer(options))
} }
@ -106,10 +163,22 @@ export class Compiler {
printSync(m: Program, options?: Options): Output { printSync(m: Program, options?: Options): Output {
options = options || {}; options = options || {};
if (bindings) {
return bindings.printSync(JSON.stringify(m), toBuffer(options)); 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> { 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"; const isModule = typeof src !== "string";
options = options || {}; options = options || {};
@ -117,7 +186,6 @@ export class Compiler {
options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript'; options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript';
} }
const { plugin, ...newOptions } = options; const { plugin, ...newOptions } = options;
if (plugin) { if (plugin) {
@ -139,7 +207,7 @@ export class Compiler {
options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript'; options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript';
} }
if (bindings) {
const { plugin, ...newOptions } = options; const { plugin, ...newOptions } = options;
if (plugin) { if (plugin) {
@ -153,9 +221,20 @@ export class Compiler {
isModule, isModule,
toBuffer(newOptions), toBuffer(newOptions),
) )
} else if (fallbackBindings) {
return fallbackBindings.transformSync(src, options);
}
throw new Error("Bindings not found");
} }
async transformFile(path: string, options?: Options): Promise<Output> { 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 || {}; options = options || {};
if (options?.jsc?.parser) { if (options?.jsc?.parser) {
@ -174,13 +253,18 @@ export class Compiler {
} }
transformFileSync(path: string, options?: Options): Output { 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 || {}; options = options || {};
if (options?.jsc?.parser) { if (options?.jsc?.parser) {
options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript'; options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript';
} }
const { plugin, ...newOptions } = options; const { plugin, ...newOptions } = options;
newOptions.filename = path; newOptions.filename = path;
@ -194,6 +278,12 @@ export class Compiler {
async bundle(options?: BundleInput | string): Promise<{ [name: string]: Output }> { 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); const opts = await compileBundleOptions(options);
if (Array.isArray(opts)) { if (Array.isArray(opts)) {
@ -329,10 +419,14 @@ export function __experimental_registerGlobalTraceConfig(traceConfig: {
type: 'traceEvent', type: 'traceEvent',
fileName?: string fileName?: string
}) { }) {
// Do not raise error if binding doesn't exists - fallback binding will not support
// this ever.
if (bindings) {
if (traceConfig.type === 'traceEvent') { if (traceConfig.type === 'traceEvent') {
bindings.initCustomTraceSubscriber(traceConfig.fileName); bindings.initCustomTraceSubscriber(traceConfig.fileName);
} }
} }
}
/** /**
* @ignore * @ignore

124
node-swc/src/postinstall.ts Normal file
View 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;
});

View File

@ -54,6 +54,7 @@
"scripts": { "scripts": {
"changelog": "git cliff --output CHANGELOG.md", "changelog": "git cliff --output CHANGELOG.md",
"prepare": "husky install && git config feature.manyFiles true && node ./crates/swc_ecma_preset_env/scripts/copy-data.js", "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", "artifacts": "napi artifacts --dist scripts/npm",
"prepublishOnly": "tsc -d && napi prepublish -p scripts/npm --tagstyle npm", "prepublishOnly": "tsc -d && napi prepublish -p scripts/npm --tagstyle npm",
"pack": "wasm-pack", "pack": "wasm-pack",
@ -167,6 +168,7 @@
"binding.js", "binding.js",
"package.json", "package.json",
"spack.d.ts", "spack.d.ts",
"types.js" "types.js",
"postinstall.js"
] ]
} }

1
postinstall.js Normal file
View File

@ -0,0 +1 @@
// This'll be generated by build process for the installation of the `@swc/core` package.