af066d9679
## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) Co-authored-by: JJ Kasper <jj@jjsweb.site>
455 lines
13 KiB
JavaScript
455 lines
13 KiB
JavaScript
import path from 'path'
|
|
import { pathToFileURL } from 'url'
|
|
import { platform, arch } from 'os'
|
|
import { platformArchTriples } from 'next/dist/compiled/@napi-rs/triples'
|
|
import * as Log from '../output/log'
|
|
import { getParserOptions } from './options'
|
|
import { eventSwcLoadFailure } from '../../telemetry/events/swc-load-failure'
|
|
import { patchIncorrectLockfile } from '../../lib/patch-incorrect-lockfile'
|
|
import { downloadWasmSwc } from '../../lib/download-wasm-swc'
|
|
import { version as nextVersion } from 'next/package.json'
|
|
|
|
const ArchName = arch()
|
|
const PlatformName = platform()
|
|
const triples = platformArchTriples[PlatformName][ArchName] || []
|
|
|
|
// These are the platforms we'll try to load wasm bindings first,
|
|
// only try to load native bindings if loading wasm binding somehow fails.
|
|
// Fallback to native binding is for migration period only,
|
|
// once we can verify loading-wasm-first won't cause visible regressions,
|
|
// we'll not include native bindings for these platform at all.
|
|
const knownDefaultWasmFallbackTriples = [
|
|
'aarch64-linux-android',
|
|
'x86_64-unknown-freebsd',
|
|
'aarch64-pc-windows-msvc',
|
|
'arm-linux-androideabi',
|
|
'armv7-unknown-linux-gnueabihf',
|
|
'i686-pc-windows-msvc',
|
|
]
|
|
|
|
let nativeBindings
|
|
let wasmBindings
|
|
let downloadWasmPromise
|
|
let pendingBindings
|
|
let swcTraceFlushGuard
|
|
let swcCrashReporterFlushGuard
|
|
export const lockfilePatchPromise = {}
|
|
|
|
export async function loadBindings() {
|
|
if (pendingBindings) {
|
|
return pendingBindings
|
|
}
|
|
pendingBindings = new Promise(async (resolve, reject) => {
|
|
if (!lockfilePatchPromise.cur) {
|
|
// always run lockfile check once so that it gets patched
|
|
// even if it doesn't fail to load locally
|
|
lockfilePatchPromise.cur = patchIncorrectLockfile(process.cwd()).catch(
|
|
console.error
|
|
)
|
|
}
|
|
|
|
let attempts = []
|
|
const shouldLoadWasmFallbackFirst = triples.some(
|
|
(triple) =>
|
|
!!triple?.raw && knownDefaultWasmFallbackTriples.includes(triple.raw)
|
|
)
|
|
|
|
if (shouldLoadWasmFallbackFirst) {
|
|
const fallbackBindings = await tryLoadWasmWithFallback(attempts)
|
|
if (fallbackBindings) {
|
|
return resolve(fallbackBindings)
|
|
}
|
|
}
|
|
|
|
try {
|
|
return resolve(loadNative())
|
|
} catch (a) {
|
|
attempts = attempts.concat(a)
|
|
}
|
|
|
|
// For these platforms we already tried to load wasm and failed, skip reattempt
|
|
if (!shouldLoadWasmFallbackFirst) {
|
|
const fallbackBindings = await tryLoadWasmWithFallback(attempts)
|
|
if (fallbackBindings) {
|
|
return resolve(fallbackBindings)
|
|
}
|
|
}
|
|
|
|
logLoadFailure(attempts, true)
|
|
})
|
|
return pendingBindings
|
|
}
|
|
|
|
async function tryLoadWasmWithFallback(attempts) {
|
|
try {
|
|
let bindings = await loadWasm()
|
|
eventSwcLoadFailure({ wasm: 'enabled' })
|
|
return bindings
|
|
} catch (a) {
|
|
attempts = attempts.concat(a)
|
|
}
|
|
|
|
try {
|
|
// if not installed already download wasm package on-demand
|
|
// we download to a custom directory instead of to node_modules
|
|
// as node_module import attempts are cached and can't be re-attempted
|
|
// x-ref: https://github.com/nodejs/modules/issues/307
|
|
const wasmDirectory = path.join(
|
|
path.dirname(require.resolve('next/package.json')),
|
|
'wasm'
|
|
)
|
|
if (!downloadWasmPromise) {
|
|
downloadWasmPromise = downloadWasmSwc(nextVersion, wasmDirectory)
|
|
}
|
|
await downloadWasmPromise
|
|
let bindings = await loadWasm(pathToFileURL(wasmDirectory).href)
|
|
eventSwcLoadFailure({ wasm: 'fallback' })
|
|
|
|
// still log native load attempts so user is
|
|
// aware it failed and should be fixed
|
|
for (const attempt of attempts) {
|
|
Log.warn(attempt)
|
|
}
|
|
return bindings
|
|
} catch (a) {
|
|
attempts = attempts.concat(a)
|
|
}
|
|
}
|
|
|
|
function loadBindingsSync() {
|
|
let attempts = []
|
|
try {
|
|
return loadNative()
|
|
} catch (a) {
|
|
attempts = attempts.concat(a)
|
|
}
|
|
|
|
// we can leverage the wasm bindings if they are already
|
|
// loaded
|
|
if (wasmBindings) {
|
|
return wasmBindings
|
|
}
|
|
|
|
logLoadFailure(attempts)
|
|
}
|
|
|
|
let loggingLoadFailure = false
|
|
|
|
function logLoadFailure(attempts, triedWasm = false) {
|
|
// make sure we only emit the event and log the failure once
|
|
if (loggingLoadFailure) return
|
|
loggingLoadFailure = true
|
|
|
|
for (let attempt of attempts) {
|
|
Log.warn(attempt)
|
|
}
|
|
|
|
eventSwcLoadFailure({ wasm: triedWasm ? 'failed' : undefined })
|
|
.then(() => lockfilePatchPromise.cur || Promise.resolve())
|
|
.finally(() => {
|
|
Log.error(
|
|
`Failed to load SWC binary for ${PlatformName}/${ArchName}, see more info here: https://nextjs.org/docs/messages/failed-loading-swc`
|
|
)
|
|
process.exit(1)
|
|
})
|
|
}
|
|
|
|
async function loadWasm(importPath = '') {
|
|
if (wasmBindings) {
|
|
return wasmBindings
|
|
}
|
|
|
|
let attempts = []
|
|
for (let pkg of ['@next/swc-wasm-nodejs', '@next/swc-wasm-web']) {
|
|
try {
|
|
let pkgPath = pkg
|
|
|
|
if (importPath) {
|
|
// the import path must be exact when not in node_modules
|
|
pkgPath = path.join(importPath, pkg, 'wasm.js')
|
|
}
|
|
let bindings = await import(pkgPath)
|
|
if (pkg === '@next/swc-wasm-web') {
|
|
bindings = await bindings.default()
|
|
}
|
|
Log.info('Using wasm build of next-swc')
|
|
|
|
// Note wasm binary does not support async intefaces yet, all async
|
|
// interface coereces to sync interfaces.
|
|
wasmBindings = {
|
|
isWasm: true,
|
|
transform(src, options) {
|
|
// TODO: we can remove fallback to sync interface once new stable version of next-swc gets published (current v12.2)
|
|
return bindings?.transform
|
|
? bindings.transform(src.toString(), options)
|
|
: Promise.resolve(bindings.transformSync(src.toString(), options))
|
|
},
|
|
transformSync(src, options) {
|
|
return bindings.transformSync(src.toString(), options)
|
|
},
|
|
minify(src, options) {
|
|
return bindings?.minify
|
|
? bindings.minify(src.toString(), options)
|
|
: Promise.resolve(bindings.minifySync(src.toString(), options))
|
|
},
|
|
minifySync(src, options) {
|
|
return bindings.minifySync(src.toString(), options)
|
|
},
|
|
parse(src, options) {
|
|
return bindings?.parse
|
|
? bindings.parse(src.toString(), options)
|
|
: Promise.resolve(bindings.parseSync(src.toString(), options))
|
|
},
|
|
parseSync(src, options) {
|
|
const astStr = bindings.parseSync(src.toString(), options)
|
|
return astStr
|
|
},
|
|
getTargetTriple() {
|
|
return undefined
|
|
},
|
|
diagnostics: {
|
|
startDiagnostics: () => {
|
|
Log.error('Wasm binding does not support --diagnostics yet')
|
|
},
|
|
},
|
|
}
|
|
return wasmBindings
|
|
} catch (e) {
|
|
// Only log attempts for loading wasm when loading as fallback
|
|
if (importPath) {
|
|
if (e?.code === 'ERR_MODULE_NOT_FOUND') {
|
|
attempts.push(`Attempted to load ${pkg}, but it was not installed`)
|
|
} else {
|
|
attempts.push(
|
|
`Attempted to load ${pkg}, but an error occurred: ${e.message ?? e}`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
throw attempts
|
|
}
|
|
|
|
function loadNative() {
|
|
if (nativeBindings) {
|
|
return nativeBindings
|
|
}
|
|
|
|
let bindings
|
|
let attempts = []
|
|
|
|
for (const triple of triples) {
|
|
try {
|
|
bindings = require(`@next/swc/native/next-swc.${triple.platformArchABI}.node`)
|
|
Log.info('Using locally built binary of @next/swc')
|
|
break
|
|
} catch (e) {}
|
|
}
|
|
|
|
if (!bindings) {
|
|
for (const triple of triples) {
|
|
let pkg = `@next/swc-${triple.platformArchABI}`
|
|
try {
|
|
bindings = require(pkg)
|
|
break
|
|
} catch (e) {
|
|
if (e?.code === 'MODULE_NOT_FOUND') {
|
|
attempts.push(`Attempted to load ${pkg}, but it was not installed`)
|
|
} else {
|
|
attempts.push(
|
|
`Attempted to load ${pkg}, but an error occurred: ${e.message ?? e}`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bindings) {
|
|
// Initialize crash reporter, as earliest as possible from any point of import.
|
|
// The first-time import to next-swc is not predicatble in the import tree of next.js, which makes
|
|
// we can't rely on explicit manual initialization as similar to trace reporter.
|
|
if (!swcCrashReporterFlushGuard) {
|
|
// Crash reports in next-swc should be treated in the same way we treat telemetry to opt out.
|
|
/* TODO: temporarily disable initialization while confirming logistics.
|
|
let telemetry = new Telemetry({ distDir: process.cwd() })
|
|
if (telemetry.isEnabled) {
|
|
swcCrashReporterFlushGuard = bindings.initCrashReporter?.()
|
|
}*/
|
|
}
|
|
|
|
nativeBindings = {
|
|
isWasm: false,
|
|
transform(src, options) {
|
|
const isModule =
|
|
typeof src !== undefined &&
|
|
typeof src !== 'string' &&
|
|
!Buffer.isBuffer(src)
|
|
options = options || {}
|
|
|
|
if (options?.jsc?.parser) {
|
|
options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript'
|
|
}
|
|
|
|
return bindings.transform(
|
|
isModule ? JSON.stringify(src) : src,
|
|
isModule,
|
|
toBuffer(options)
|
|
)
|
|
},
|
|
|
|
transformSync(src, options) {
|
|
if (typeof src === undefined) {
|
|
throw new Error(
|
|
"transformSync doesn't implement reading the file from filesystem"
|
|
)
|
|
} else if (Buffer.isBuffer(src)) {
|
|
throw new Error(
|
|
"transformSync doesn't implement taking the source code as Buffer"
|
|
)
|
|
}
|
|
const isModule = typeof src !== 'string'
|
|
options = options || {}
|
|
|
|
if (options?.jsc?.parser) {
|
|
options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript'
|
|
}
|
|
|
|
return bindings.transformSync(
|
|
isModule ? JSON.stringify(src) : src,
|
|
isModule,
|
|
toBuffer(options)
|
|
)
|
|
},
|
|
|
|
minify(src, options) {
|
|
return bindings.minify(toBuffer(src), toBuffer(options ?? {}))
|
|
},
|
|
|
|
minifySync(src, options) {
|
|
return bindings.minifySync(toBuffer(src), toBuffer(options ?? {}))
|
|
},
|
|
|
|
parse(src, options) {
|
|
return bindings.parse(src, toBuffer(options ?? {}))
|
|
},
|
|
|
|
getTargetTriple: bindings.getTargetTriple,
|
|
initCustomTraceSubscriber: bindings.initCustomTraceSubscriber,
|
|
teardownTraceSubscriber: bindings.teardownTraceSubscriber,
|
|
teardownCrashReporter: bindings.teardownCrashReporter,
|
|
diagnostics: {
|
|
startDiagnostics: (options) =>
|
|
bindings.startDiagnostics(toBuffer(options)),
|
|
},
|
|
}
|
|
return nativeBindings
|
|
}
|
|
|
|
throw attempts
|
|
}
|
|
|
|
function toBuffer(t) {
|
|
return Buffer.from(JSON.stringify(t))
|
|
}
|
|
|
|
export async function isWasm() {
|
|
let bindings = await loadBindings()
|
|
return bindings.isWasm
|
|
}
|
|
|
|
export async function transform(src, options) {
|
|
let bindings = await loadBindings()
|
|
return bindings.transform(src, options)
|
|
}
|
|
|
|
export function transformSync(src, options) {
|
|
let bindings = loadBindingsSync()
|
|
return bindings.transformSync(src, options)
|
|
}
|
|
|
|
export async function minify(src, options) {
|
|
let bindings = await loadBindings()
|
|
return bindings.minify(src, options)
|
|
}
|
|
|
|
export function minifySync(src, options) {
|
|
let bindings = loadBindingsSync()
|
|
return bindings.minifySync(src, options)
|
|
}
|
|
|
|
export async function parse(src, options) {
|
|
let bindings = await loadBindings()
|
|
let parserOptions = getParserOptions(options)
|
|
return bindings.parse(src, parserOptions).then((astStr) => JSON.parse(astStr))
|
|
}
|
|
|
|
export function getBinaryMetadata() {
|
|
let bindings
|
|
try {
|
|
bindings = loadNative()
|
|
} catch (e) {
|
|
// Suppress exceptions, this fn allows to fail to load native bindings
|
|
}
|
|
|
|
return {
|
|
target: bindings?.getTargetTriple?.(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize trace subscriber to emit traces.
|
|
*
|
|
*/
|
|
export const initCustomTraceSubscriber = (() => {
|
|
return (filename) => {
|
|
if (!swcTraceFlushGuard) {
|
|
// Wasm binary doesn't support trace emission
|
|
let bindings = loadNative()
|
|
swcTraceFlushGuard = bindings.initCustomTraceSubscriber(filename)
|
|
}
|
|
}
|
|
})()
|
|
|
|
/**
|
|
* Teardown swc's trace subscriber if there's an initialized flush guard exists.
|
|
*
|
|
* This is workaround to amend behavior with process.exit
|
|
* (https://github.com/vercel/next.js/blob/4db8c49cc31e4fc182391fae6903fb5ef4e8c66e/packages/next/bin/next.ts#L134=)
|
|
* seems preventing napi's cleanup hook execution (https://github.com/swc-project/swc/blob/main/crates/node/src/util.rs#L48-L51=),
|
|
*
|
|
* instead parent process manually drops guard when process gets signal to exit.
|
|
*/
|
|
export const teardownTraceSubscriber = (() => {
|
|
let flushed = false
|
|
return () => {
|
|
if (!flushed) {
|
|
flushed = true
|
|
try {
|
|
let bindings = loadNative()
|
|
if (swcTraceFlushGuard) {
|
|
bindings.teardownTraceSubscriber(swcTraceFlushGuard)
|
|
}
|
|
} catch (e) {
|
|
// Suppress exceptions, this fn allows to fail to load native bindings
|
|
}
|
|
}
|
|
}
|
|
})()
|
|
|
|
export const teardownCrashReporter = (() => {
|
|
let flushed = false
|
|
return () => {
|
|
if (!flushed) {
|
|
flushed = true
|
|
try {
|
|
let bindings = loadNative()
|
|
if (swcCrashReporterFlushGuard) {
|
|
bindings.teardownCrashReporter(swcCrashReporterFlushGuard)
|
|
}
|
|
} catch (e) {
|
|
// Suppress exceptions, this fn allows to fail to load native bindings
|
|
}
|
|
}
|
|
}
|
|
})()
|