mirror of
https://github.com/swc-project/swc.git
synced 2024-12-23 13:51:19 +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";
|
} 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 {
|
||||||
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(
|
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";
|
||||||
|
|
||||||
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(
|
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 || {};
|
||||||
|
|
||||||
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> {
|
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,23 +207,34 @@ 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) {
|
||||||
|
const m =
|
||||||
|
typeof src === "string" ? this.parseSync(src, options?.jsc?.parser, options.filename) : src;
|
||||||
|
return this.transformSync(plugin(m), newOptions);
|
||||||
|
}
|
||||||
|
|
||||||
if (plugin) {
|
return bindings.transformSync(
|
||||||
const m =
|
isModule ? JSON.stringify(src) : src,
|
||||||
typeof src === "string" ? this.parseSync(src, options?.jsc?.parser, options.filename) : src;
|
isModule,
|
||||||
return this.transformSync(plugin(m), newOptions);
|
toBuffer(newOptions),
|
||||||
|
)
|
||||||
|
} else if (fallbackBindings) {
|
||||||
|
return fallbackBindings.transformSync(src, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bindings.transformSync(
|
throw new Error("Bindings not found");
|
||||||
isModule ? JSON.stringify(src) : src,
|
|
||||||
isModule,
|
|
||||||
toBuffer(newOptions),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,8 +419,12 @@ export function __experimental_registerGlobalTraceConfig(traceConfig: {
|
|||||||
type: 'traceEvent',
|
type: 'traceEvent',
|
||||||
fileName?: string
|
fileName?: string
|
||||||
}) {
|
}) {
|
||||||
if (traceConfig.type === 'traceEvent') {
|
// Do not raise error if binding doesn't exists - fallback binding will not support
|
||||||
bindings.initCustomTraceSubscriber(traceConfig.fileName);
|
// 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": {
|
"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
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