diff --git a/packages/next/errors.json b/packages/next/errors.json index 8ef0f033634fa..8492f71e2eb54 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -903,5 +903,7 @@ "902": "Invalid handler fields configured for \"cacheHandlers\":\\n%s", "903": "The file \"%s\" must export a function, either as a default export or as a named \"%s\" export.\\nThis function is what Next.js runs for every request handled by this %s.\\n\\nWhy this happens:\\n%s- The file exists but doesn't export a function.\\n- The export is not a function (e.g., an object or constant).\\n- There's a syntax error preventing the export from being recognized.\\n\\nTo fix it:\\n- Ensure this file has either a default or \"%s\" function export.\\n\\nLearn more: https://nextjs.org/docs/messages/middleware-to-proxy", "904": "The file \"%s\" must export a function, either as a default export or as a named \"%s\" export.", - "905": "Page \"%s\" cannot use \\`export const unstable_prefetch = ...\\` without enabling \\`cacheComponents\\`." + "905": "Page \"%s\" cannot use \\`export const unstable_prefetch = ...\\` without enabling \\`cacheComponents\\`.", + "906": "Bindings not loaded yet, but they are being loaded, did you forget to await?", + "907": "bindings not loaded yet. Either call `loadBindings` to wait for them to be available or ensure that `installBindings` has already been called." } diff --git a/packages/next/src/build/babel/loader/get-config.ts b/packages/next/src/build/babel/loader/get-config.ts index 42da032432337..f62a2b749c9cc 100644 --- a/packages/next/src/build/babel/loader/get-config.ts +++ b/packages/next/src/build/babel/loader/get-config.ts @@ -17,6 +17,7 @@ import { } from './util' import * as Log from '../../output/log' import { isReactCompilerRequired } from '../../swc' +import { installBindings } from '../../swc/install-bindings' /** * An internal (non-exported) type used by babel. @@ -570,6 +571,10 @@ export default async function getConfig( inputSourceMap?: SourceMap | undefined } ): Promise { + // Install bindings early so they are definitely available to the loader. + // When run by webpack in next this is already done with correct configuration so this is a no-op. + // In turbopack loaders are run in a subprocess so it may or may not be done. + await installBindings() const cacheCharacteristics = await getCacheCharacteristics( loaderOptions, source, diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 79381932e0f18..67f50fdc9e01f 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -151,6 +151,7 @@ import type { NextError } from '../lib/is-error' import { isEdgeRuntime } from '../lib/is-edge-runtime' import { recursiveCopy } from '../lib/recursive-copy' import { lockfilePatchPromise, teardownTraceSubscriber } from './swc' +import { installBindings } from './swc/install-bindings' import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex' import { getFilesInDir } from '../lib/get-files-in-dir' import { eventSwcPlugins } from '../telemetry/events/swc-plugins' @@ -964,6 +965,8 @@ export default async function build( // Reading the config can modify environment variables that influence the bundler selection. bundler = finalizeBundlerFromConfig(bundler) nextBuildSpan.setAttribute('bundler', getBundlerForTelemetry(bundler)) + // Install the native bindings early so we can have synchronous access later. + await installBindings(config.experimental?.useWasmBinary) process.env.NEXT_DEPLOYMENT_ID = config.deploymentId || '' NextBuildContext.config = config diff --git a/packages/next/src/build/jest/jest.ts b/packages/next/src/build/jest/jest.ts index 2d64cc80b3e97..b8fe0b05f4e01 100644 --- a/packages/next/src/build/jest/jest.ts +++ b/packages/next/src/build/jest/jest.ts @@ -5,7 +5,8 @@ import { PHASE_TEST } from '../../shared/lib/constants' import loadJsConfig from '../load-jsconfig' import * as Log from '../output/log' import { findPagesDir } from '../../lib/find-pages-dir' -import { loadBindings, lockfilePatchPromise } from '../swc' +import { lockfilePatchPromise } from '../swc' +import { installBindings } from '../swc/install-bindings' import type { JestTransformerConfig } from '../swc/jest-transformer' import type { Config } from '@jest/types' @@ -96,7 +97,7 @@ export default function nextJest(options: { dir?: string } = {}) { : customJestConfig) ?? {} // eagerly load swc bindings instead of waiting for transform calls - await loadBindings(nextConfig?.experimental?.useWasmBinary) + await installBindings(nextConfig?.experimental?.useWasmBinary) if (lockfilePatchPromise.cur) { await lockfilePatchPromise.cur diff --git a/packages/next/src/build/load-entrypoint.ts b/packages/next/src/build/load-entrypoint.ts index db02a4d68e7b5..2ec8712b9fe57 100644 --- a/packages/next/src/build/load-entrypoint.ts +++ b/packages/next/src/build/load-entrypoint.ts @@ -1,6 +1,6 @@ import fs from 'fs/promises' import path from 'path' -import { loadBindings } from './swc' +import { getBindingsSync } from './swc' // NOTE: this should be updated if this loader file is moved. const PACKAGE_ROOT = path.normalize(path.join(__dirname, '../..')) @@ -39,14 +39,12 @@ export async function loadEntrypoint( injections?: Record, imports?: Record ): Promise { - let bindings = await loadBindings() - const templatePath = path.resolve( path.join(TEMPLATES_ESM_FOLDER, `${entrypoint}.js`) ) let content = await fs.readFile(templatePath) - return bindings.expandNextJsTemplate( + return getBindingsSync().expandNextJsTemplate( content, // Ensure that we use unix-style path separators for the import paths path.join(TEMPLATE_SRC_FOLDER, `${entrypoint}.js`).replace(/\\/g, '/'), diff --git a/packages/next/src/build/lockfile.ts b/packages/next/src/build/lockfile.ts index c06d211455947..08f12ed992a8f 100644 --- a/packages/next/src/build/lockfile.ts +++ b/packages/next/src/build/lockfile.ts @@ -1,5 +1,6 @@ import { bold, cyan } from '../lib/picocolors' import * as Log from './output/log' +import { getBindingsSync } from './swc' import type { Binding, Lockfile as NativeLockfile } from './swc/types' @@ -56,16 +57,11 @@ export class Lockfile { * - If we fail to acquire the lock, we return `undefined`. * - If we're on wasm, this always returns a dummy `Lockfile` object. */ - static async tryAcquire( + static tryAcquire( path: string, unlockOnExit: boolean = true - ): Promise { - const { loadBindings } = require('./swc') as typeof import('./swc') - // Ideally we could provide a sync version of `tryAcquire`, but - // `loadBindings` is async. We're okay with skipping async-loaded wasm - // bindings and the internal `loadNative` function is synchronous, but it - // lacks some checks that `loadBindings` has. - const bindings = await loadBindings() + ): Lockfile | undefined { + const bindings = getBindingsSync() if (bindings.isWasm) { Log.info( `Skipping creating a lockfile at ${cyan(path)} because we're using WASM bindings` @@ -74,7 +70,7 @@ export class Lockfile { } else { let nativeLockfile try { - nativeLockfile = await bindings.lockfileTryAcquire(path) + nativeLockfile = bindings.lockfileTryAcquireSync(path) } catch (e) { // this happens if there's an IO error (e.g. `ENOENT`), which is // different than if we just didn't acquire the lock @@ -123,7 +119,7 @@ export class Lockfile { const startMs = Date.now() let lockfile while (Date.now() - startMs < MAX_RETRY_MS) { - lockfile = await Lockfile.tryAcquire(path, unlockOnExit) + lockfile = Lockfile.tryAcquire(path, unlockOnExit) if (lockfile !== undefined) break await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)) } diff --git a/packages/next/src/build/next-config-ts/transpile-config.ts b/packages/next/src/build/next-config-ts/transpile-config.ts index 0fefd8c8551e1..bf6614d3a0de0 100644 --- a/packages/next/src/build/next-config-ts/transpile-config.ts +++ b/packages/next/src/build/next-config-ts/transpile-config.ts @@ -177,8 +177,9 @@ async function handleCJS({ const nextConfigString = await readFile(nextConfigPath, 'utf8') // lazy require swc since it loads React before even setting NODE_ENV // resulting loading Development React on Production - const { transform } = require('../swc') as typeof import('../swc') - const { code } = await transform(nextConfigString, swcOptions) + const { loadBindings } = require('../swc') as typeof import('../swc') + const bindings = await loadBindings() + const { code } = await bindings.transform(nextConfigString, swcOptions) // register require hook only if require exists if (code.includes('require(')) { @@ -187,7 +188,21 @@ async function handleCJS({ } // filename & extension don't matter here - return requireFromString(code, resolve(cwd, 'next.config.compiled.js')) + const config = requireFromString( + code, + resolve(cwd, 'next.config.compiled.js') + ) + // At this point we have already loaded the bindings without this configuration setting due to the `transform` call above. + // Possibly we fell back to wasm in which case, it all works out but if not we need to warn + // that the configuration was ignored. + if (config?.experimental?.useWasmBinary && !bindings.isWasm) { + warn( + 'Using a next.config.ts file is incompatible with `experimental.useWasmBinary` unless ' + + '`--experimental-next-config-strip-types` is also passed.\nSetting `useWasmBinary` to `false' + ) + config.experimental.useWasmBinary = false + } + return config } catch (error) { throw error } finally { diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index eff67e1f73981..f7ed97988e67c 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -160,21 +160,33 @@ let lastNativeBindingsLoadErrorCode: | 'unsupported_target' | string | undefined = undefined -// Used to cache calls to `loadBindings` -let pendingBindings: Promise -// some things call `loadNative` directly instead of `loadBindings`... Cache calls to that -// separately. -let nativeBindings: Binding -// can allow hacky sync access to bindings for loadBindingsSync -let wasmBindings: Binding +// Used to cache racing calls to `loadBindings` +let pendingBindings: Promise | undefined +// The cached loaded bindings +let loadedBindings: Binding | undefined = undefined let downloadWasmPromise: any let swcTraceFlushGuard: any let downloadNativeBindingsPromise: Promise | undefined = undefined export const lockfilePatchPromise: { cur?: Promise } = {} +/** Access the native bindings which should already have been loaded via `installBindings. Throws if they are not available. */ +export function getBindingsSync(): Binding { + if (!loadedBindings) { + if (pendingBindings) { + throw new Error( + 'Bindings not loaded yet, but they are being loaded, did you forget to await?' + ) + } + throw new Error( + 'bindings not loaded yet. Either call `loadBindings` to wait for them to be available or ensure that `installBindings` has already been called.' + ) + } + return loadedBindings +} + /** - * Attempts to load a native or wasm binding. + * Loads the native or wasm binding. * * By default, this first tries to use a native binding, falling back to a wasm binding if that * fails. @@ -184,6 +196,9 @@ export const lockfilePatchPromise: { cur?: Promise } = {} export async function loadBindings( useWasmBinary: boolean = false ): Promise { + if (loadedBindings) { + return loadedBindings + } if (pendingBindings) { return pendingBindings } @@ -281,7 +296,9 @@ export async function loadBindings( logLoadFailure(attempts, true) }) - return pendingBindings + loadedBindings = await pendingBindings + pendingBindings = undefined + return loadedBindings } async function tryLoadNativeWithFallback(attempts: Array) { @@ -363,12 +380,6 @@ function loadBindingsSync() { attempts = attempts.concat(a) } - // HACK: we can leverage the wasm bindings if they are already loaded - // this may introduce race conditions - if (wasmBindings) { - return wasmBindings - } - logLoadFailure(attempts) throw new Error('Failed to load bindings', { cause: attempts }) } @@ -1182,7 +1193,7 @@ async function loadWasm(importPath = '') { // Note wasm binary does not support async intefaces yet, all async // interface coereces to sync interfaces. - wasmBindings = { + let wasmBindings = { css: { lightning: { transform: function (_options: any) { @@ -1312,8 +1323,8 @@ async function loadWasm(importPath = '') { * wasm fallback. */ function loadNative(importPath?: string) { - if (nativeBindings) { - return nativeBindings + if (loadedBindings) { + return loadedBindings } if (process.env.NEXT_TEST_WASM) { @@ -1379,7 +1390,7 @@ function loadNative(importPath?: string) { } if (bindings) { - nativeBindings = { + loadedBindings = { isWasm: false, transform(src: string, options: any) { const isModule = @@ -1518,7 +1529,7 @@ function loadNative(importPath?: string) { return bindings.lockfileUnlockSync(lockfile) }, } - return nativeBindings + return loadedBindings } throw attempts @@ -1539,54 +1550,40 @@ function toBuffer(t: any) { return Buffer.from(JSON.stringify(t)) } -export async function isWasm(): Promise { - let bindings = await loadBindings() - return bindings.isWasm -} - export async function transform(src: string, options?: any): Promise { - let bindings = await loadBindings() + let bindings = getBindingsSync() return bindings.transform(src, options) } +/** Synchronously transforms the source and loads the native bindings. */ export function transformSync(src: string, options?: any): any { - let bindings = loadBindingsSync() + const bindings = loadBindingsSync() return bindings.transformSync(src, options) } -export async function minify( +export function minify( src: string, options: any ): Promise<{ code: string; map: any }> { - let bindings = await loadBindings() + const bindings = getBindingsSync() return bindings.minify(src, options) } -export async function isReactCompilerRequired( - filename: string -): Promise { - let bindings = await loadBindings() +export function isReactCompilerRequired(filename: string): Promise { + const bindings = getBindingsSync() return bindings.reactCompiler.isReactCompilerRequired(filename) } export async function parse(src: string, options: any): Promise { - let bindings = await loadBindings() - let parserOptions = getParserOptions(options) - return bindings - .parse(src, parserOptions) - .then((astStr: any) => JSON.parse(astStr)) + const bindings = getBindingsSync() + const parserOptions = getParserOptions(options) + const parsed = await bindings.parse(src, parserOptions) + return JSON.parse(parsed) } 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?.(), + target: loadedBindings?.getTargetTriple?.(), } } @@ -1597,8 +1594,8 @@ export function getBinaryMetadata() { export function initCustomTraceSubscriber(traceFileName?: string) { if (!swcTraceFlushGuard) { // Wasm binary doesn't support trace emission - let bindings = loadNative() - swcTraceFlushGuard = bindings.initCustomTraceSubscriber?.(traceFileName) + swcTraceFlushGuard = + getBindingsSync().initCustomTraceSubscriber?.(traceFileName) } } @@ -1625,9 +1622,8 @@ function once(fn: () => void): () => void { */ export const teardownTraceSubscriber = once(() => { try { - let bindings = loadNative() if (swcTraceFlushGuard) { - bindings.teardownTraceSubscriber?.(swcTraceFlushGuard) + getBindingsSync().teardownTraceSubscriber?.(swcTraceFlushGuard) } } catch (e) { // Suppress exceptions, this fn allows to fail to load native bindings @@ -1637,14 +1633,12 @@ export const teardownTraceSubscriber = once(() => { export async function getModuleNamedExports( resourcePath: string ): Promise { - const bindings = await loadBindings() - return bindings.rspack.getModuleNamedExports(resourcePath) + return getBindingsSync().rspack.getModuleNamedExports(resourcePath) } export async function warnForEdgeRuntime( source: string, isProduction: boolean ): Promise { - const bindings = await loadBindings() - return bindings.rspack.warnForEdgeRuntime(source, isProduction) + return getBindingsSync().rspack.warnForEdgeRuntime(source, isProduction) } diff --git a/packages/next/src/build/swc/install-bindings.ts b/packages/next/src/build/swc/install-bindings.ts new file mode 100644 index 0000000000000..db66fe8c0adf8 --- /dev/null +++ b/packages/next/src/build/swc/install-bindings.ts @@ -0,0 +1,23 @@ +/** + * This module provides a way to install SWC bindings without eagerly loading the entire swc/index module. + * + * The swc/index module can transitively load other modules (like webpack-config) that import React, + * and React's entry point checks process.env.NODE_ENV at require time to decide whether to load + * the development or production bundle. By deferring the require of swc/index until this function + * is called, we ensure NODE_ENV is set before React is loaded. + */ + +/** + * Loads and caches the native bindings. This is idempotent and should be called early so bindings + * can be accessed synchronously later. + * + * @param useWasmBinary - Whether to use WASM bindings instead of native bindings + */ +export async function installBindings( + useWasmBinary: boolean = false +): Promise { + // Lazy require to avoid loading swc/index (and transitively webpack-config/React) + // before NODE_ENV is set + const { loadBindings } = require('./index') as typeof import('./index') + await loadBindings(useWasmBinary) +} diff --git a/packages/next/src/build/turbopack-build/impl.ts b/packages/next/src/build/turbopack-build/impl.ts index d67393cc09d0c..029999ff52be2 100644 --- a/packages/next/src/build/turbopack-build/impl.ts +++ b/packages/next/src/build/turbopack-build/impl.ts @@ -2,7 +2,8 @@ import path from 'path' import { validateTurboNextConfig } from '../../lib/turbopack-warning' import { isFileSystemCacheEnabledForBuild } from '../../shared/lib/turbopack/utils' import { NextBuildContext } from '../build-context' -import { createDefineEnv, loadBindings } from '../swc' +import { createDefineEnv, getBindingsSync } from '../swc' +import { installBindings } from '../swc/install-bindings' import { handleRouteType, rawEntrypointsToEntrypoints, @@ -42,7 +43,7 @@ export async function turbopackBuild(): Promise<{ const currentNodeJsVersion = process.versions.node const startTime = process.hrtime() - const bindings = await loadBindings(config?.experimental?.useWasmBinary) + const bindings = getBindingsSync() // our caller should have already loaded these const dev = false const supportedBrowsers = getSupportedBrowsers(dir, dev) @@ -221,15 +222,14 @@ export async function workerMain(workerData: { Object.assign(NextBuildContext, workerData.buildContext) /// load the config because it's not serializable - NextBuildContext.config = await loadConfig( + const config = (NextBuildContext.config = await loadConfig( PHASE_PRODUCTION_BUILD, NextBuildContext.dir!, { debugPrerender: NextBuildContext.debugPrerender, reactProductionProfiling: NextBuildContext.reactProductionProfiling, } - ) - + )) // Matches handling in build/index.ts // https://github.com/vercel/next.js/blob/84f347fc86f4efc4ec9f13615c215e4b9fb6f8f0/packages/next/src/build/index.ts#L815-L818 // Ensures the `config.distDir` option is matched. @@ -242,6 +242,8 @@ export async function workerMain(workerData: { distDir: NextBuildContext.config.distDir, }) setGlobal('telemetry', telemetry) + // Install bindings early so we can access synchronously later + await installBindings(config.experimental?.useWasmBinary) const { shutdownPromise: resultShutdownPromise, diff --git a/packages/next/src/build/webpack-build/impl.ts b/packages/next/src/build/webpack-build/impl.ts index c64ac967ece60..17c32066f394e 100644 --- a/packages/next/src/build/webpack-build/impl.ts +++ b/packages/next/src/build/webpack-build/impl.ts @@ -41,6 +41,7 @@ import type { UnwrapPromise } from '../../lib/coalesced-function' import origDebug from 'next/dist/compiled/debug' import { Telemetry } from '../../telemetry/storage' import { durationToString, hrtimeToSeconds } from '../duration-to-string' +import { installBindings } from '../swc/install-bindings' const debug = origDebug('next:build:webpack-build') @@ -383,14 +384,15 @@ export async function workerMain(workerData: { resumePluginState(NextBuildContext.pluginState) /// load the config because it's not serializable - NextBuildContext.config = await loadConfig( + const config = (NextBuildContext.config = await loadConfig( PHASE_PRODUCTION_BUILD, NextBuildContext.dir!, { debugPrerender: NextBuildContext.debugPrerender, reactProductionProfiling: NextBuildContext.reactProductionProfiling, } - ) + )) + await installBindings(config.experimental?.useWasmBinary) NextBuildContext.nextBuildSpan = trace( `worker-main-${workerData.compilerName}` ) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 4f6ce78ec06a3..9c6e5b3270f68 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -61,7 +61,6 @@ import loadJsConfig, { type JsConfig, type ResolvedBaseUrl, } from './load-jsconfig' -import { loadBindings } from './swc' import { SubresourceIntegrityPlugin } from './webpack/plugins/subresource-integrity-plugin' import { NextFontManifestPlugin } from './webpack/plugins/next-font-manifest-plugin' import { getSupportedBrowsers } from './utils' @@ -422,11 +421,6 @@ export default async function getBaseWebpackConfig( loggedSwcDisabled = true } - // eagerly load swc bindings instead of waiting for transform calls - if (!babelConfigFile && isClient) { - await loadBindings(config.experimental.useWasmBinary) - } - // since `pages` doesn't always bundle by default we need to // auto-include optimizePackageImports in transpilePackages const finalTranspilePackages: string[] = ( diff --git a/packages/next/src/build/webpack/loaders/lightningcss-loader/src/loader.ts b/packages/next/src/build/webpack/loaders/lightningcss-loader/src/loader.ts index f7d4b720c8703..f8d4508a561dc 100644 --- a/packages/next/src/build/webpack/loaders/lightningcss-loader/src/loader.ts +++ b/packages/next/src/build/webpack/loaders/lightningcss-loader/src/loader.ts @@ -19,6 +19,8 @@ import { } from '../../css-loader/src/utils' import { stringifyRequest } from '../../../stringify-request' import { ECacheKey } from './interface' +import { getBindingsSync } from '../../../../../build/swc' +import { installBindings } from '../../../../../build/swc/install-bindings' const encoder = new TextEncoder() @@ -266,6 +268,10 @@ export async function LightningCssLoader( ): Promise { const done = this.async() const options = this.getOptions() + // Install bindings early so they are definitely available to the loader. + // When run by webpack in next this is already done with correct configuration so this is a no-op. + // In turbopack loaders are run in a subprocess so it may or may not be done. + await installBindings() const { implementation, targets: userTargets, ...opts } = options options.modules ??= {} @@ -305,12 +311,8 @@ export async function LightningCssLoader( ), }) } - const { loadBindings } = - require('../../../../../build/swc') as typeof import('../../../../../build/swc') - const transform = - implementation?.transformCss ?? - (await loadBindings()).css.lightning.transform + implementation?.transformCss ?? getBindingsSync().css.lightning.transform const replacedUrls = new Map() const icssReplacedUrls = new Map() diff --git a/packages/next/src/build/webpack/loaders/lightningcss-loader/src/minify.ts b/packages/next/src/build/webpack/loaders/lightningcss-loader/src/minify.ts index 8f41815b810b7..59843ba7183de 100644 --- a/packages/next/src/build/webpack/loaders/lightningcss-loader/src/minify.ts +++ b/packages/next/src/build/webpack/loaders/lightningcss-loader/src/minify.ts @@ -7,6 +7,7 @@ import { ECacheKey } from './interface' import type { Compilation, Compiler } from 'webpack' import { getTargets } from './utils' import { Buffer } from 'buffer' +import { getBindingsSync } from '../../../../../build/swc' const PLUGIN_NAME = 'lightning-css-minify' const CSS_FILE_REG = /\.css(?:\?.*)?$/i @@ -66,9 +67,7 @@ export class LightningCssMinifyPlugin { } = compilation.compiler if (!this.transform) { - const { loadBindings } = - require('../../../../../build/swc') as typeof import('../../../../../build/swc') - this.transform = (await loadBindings()).css.lightning.transform + this.transform = getBindingsSync().css.lightning.transform } const sourcemap = diff --git a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts index 91ae3343b58ff..7f71a94d7b89d 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts @@ -38,7 +38,9 @@ import { PARALLEL_ROUTE_DEFAULT_PATH } from '../../../../client/components/built import type { Compilation } from 'webpack' import { createAppRouteCode } from './create-app-route-code' import { MissingDefaultParallelRouteError } from '../../../../shared/lib/errors/missing-default-parallel-route-error' + import { normalizePathSep } from '../../../../shared/lib/page-path/normalize-path-sep' +import { installBindings } from '../../../swc/install-bindings' export type AppLoaderOptions = { name: string @@ -636,6 +638,11 @@ const filesInDirMapMap: WeakMap< Map>> > = new WeakMap() const nextAppLoader: AppLoader = async function nextAppLoader() { + // install native bindings early so they are always available. + // When run by webpack, next will have already done this, so this will be fast, + // but if run by turbopack in a subprocess it is required. In that case we cannot pass the + // `useWasmBinary` flag, but that is ok since turbopack doesn't currently support wasm. + await installBindings() const loaderOptions = this.getOptions() const { name, diff --git a/packages/next/src/build/webpack/loaders/next-barrel-loader.ts b/packages/next/src/build/webpack/loaders/next-barrel-loader.ts index bf7059f05aec4..a41db20828375 100644 --- a/packages/next/src/build/webpack/loaders/next-barrel-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-barrel-loader.ts @@ -88,6 +88,7 @@ import type webpack from 'webpack' import path from 'path' import { transform } from '../../swc' +import { installBindings } from '../../swc/install-bindings' // This is a in-memory cache for the mapping of barrel exports. This only applies // to the packages that we optimize. It will never change (e.g. upgrading packages) @@ -251,6 +252,10 @@ const NextBarrelLoader = async function ( ) { this.async() this.cacheable(true) + // Install bindings early so they are definitely available. + // When run by webpack in next this is already done with correct configuration so this is a no-op. + // In turbopack loaders are run in a subprocess so it may or may not be done. + await installBindings() const { names, swcCacheDir } = this.getOptions() diff --git a/packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts b/packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts index f0c27fd49e1e2..93c03f2c07f21 100644 --- a/packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-metadata-image-loader.ts @@ -16,6 +16,7 @@ import { WEBPACK_RESOURCE_QUERIES } from '../../../lib/constants' import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep' import type { PageExtensions } from '../../page-extensions-type' import { getLoaderModuleNamedExports } from './utils' +import { installBindings } from '../../swc/install-bindings' interface Options { segment: string @@ -30,6 +31,10 @@ async function nextMetadataImageLoader( this: webpack.LoaderContext, content: Buffer ) { + // Install bindings early so they are definitely available to the loader. + // When run by webpack in next this is already done with correct configuration so this is a no-op. + // In turbopack loaders are run in a subprocess so it may or may not be done. + await installBindings() const options: Options = this.getOptions() const { type, segment, pageExtensions, basePath } = options const { resourcePath, rootContext: context } = this diff --git a/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts b/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts index 27791f5dc00f1..0266d5e6138f7 100644 --- a/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts @@ -3,6 +3,7 @@ import fs from 'fs' import path from 'path' import { imageExtMimeTypeMap } from '../../../lib/mime-type' import { getLoaderModuleNamedExports } from './utils' +import { installBindings } from '../../swc/install-bindings' function errorOnBadHandler(resourcePath: string) { return ` @@ -373,6 +374,10 @@ async function getSitemapRouteCode( // TODO-METADATA: improve the cache control strategy const nextMetadataRouterLoader: webpack.LoaderDefinitionFunction = async function () { + // Install bindings early so they are definitely available to the loader. + // When run by webpack in next this is already done with correct configuration so this is a no-op. + // In turbopack loaders are run in a subprocess so it may or may not be done. + await installBindings() const { isDynamicRouteExtension, filePath } = this.getOptions() const { name: fileBaseName } = getFilenameAndExtension(filePath) this.addDependency(filePath) diff --git a/packages/next/src/build/webpack/loaders/next-swc-loader.ts b/packages/next/src/build/webpack/loaders/next-swc-loader.ts index 7f1168934d303..2ee2f51ed6748 100644 --- a/packages/next/src/build/webpack/loaders/next-swc-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-swc-loader.ts @@ -28,7 +28,8 @@ DEALINGS IN THE SOFTWARE. import type { NextConfig } from '../../../types' import { type WebpackLayerName, WEBPACK_LAYERS } from '../../../lib/constants' -import { isWasm, transform } from '../../swc' +import { getBindingsSync, transform } from '../../swc' +import { installBindings } from '../../swc/install-bindings' import { getLoaderSWCOptions } from '../../swc/options' import path, { isAbsolute } from 'path' import { babelIncludeRegexes } from '../../webpack-config' @@ -251,7 +252,7 @@ export function pitch(this: any) { !EXCLUDED_PATHS.test(this.resourcePath) && this.loaders.length - 1 === this.loaderIndex && isAbsolute(this.resourcePath) && - !(await isWasm()) + !getBindingsSync().isWasm ) { this.addDependency(this.resourcePath) return loaderTransform.call(this) @@ -268,13 +269,18 @@ export default function swcLoader( inputSourceMap: any ) { const callback = this.async() - loaderTransform.call(this, inputSource, inputSourceMap).then( - ([transformedSource, outputSourceMap]: any) => { - callback(null, transformedSource, outputSourceMap || inputSourceMap) - }, - (err: Error) => { - callback(err) - } + // Install bindings early so they are definitely available to the loader. + // When run by webpack in next this is already done with correct configuration so this is a no-op. + // In turbopack loaders are run in a subprocess so it may or may not be done. + installBindings().then(() => + loaderTransform.call(this, inputSource, inputSourceMap).then( + ([transformedSource, outputSourceMap]: any) => { + callback(null, transformedSource, outputSourceMap || inputSourceMap) + }, + (err: Error) => { + callback(err) + } + ) ) } diff --git a/packages/next/src/cli/next-typegen.ts b/packages/next/src/cli/next-typegen.ts index 94cf19cd14578..632c6a185ed62 100644 --- a/packages/next/src/cli/next-typegen.ts +++ b/packages/next/src/cli/next-typegen.ts @@ -32,6 +32,7 @@ import { } from '../server/lib/router-utils/route-types-utils' import { writeCacheLifeTypes } from '../server/lib/router-utils/cache-life-type-utils' import { createValidFileMatcher } from '../server/lib/find-page-file' +import { installBindings } from '../build/swc/install-bindings' export type NextTypegenOptions = { dir?: string @@ -49,6 +50,7 @@ const nextTypegen = async ( } const nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, baseDir) + await installBindings(nextConfig.experimental?.useWasmBinary) const distDir = join(baseDir, nextConfig.distDir) const { pagesDir, appDir } = findPagesDir(baseDir) diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index c42c171d2f62b..f624736c7cb42 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1671,7 +1671,9 @@ export default async function loadConfig( if (userConfig.experimental?.useLightningcss) { const { loadBindings } = require('../build/swc') as typeof import('../build/swc') - const isLightningSupported = (await loadBindings())?.css?.lightning + const isLightningSupported = ( + await loadBindings(userConfig.experimental?.useWasmBinary) + )?.css?.lightning if (!isLightningSupported) { curLog.warn( diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index 943ad56abfbfb..160d5e1ec759b 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -24,7 +24,7 @@ import type { Project, Entrypoints, } from '../../build/swc/types' -import { createDefineEnv } from '../../build/swc' +import { createDefineEnv, getBindingsSync } from '../../build/swc' import * as Log from '../../build/output/log' import { BLOCKED_PAGES } from '../../shared/lib/constants' import { @@ -193,10 +193,7 @@ export async function createHotReloaderTurbopack( const buildId = 'development' const { nextConfig, dir: projectPath } = opts - const { loadBindings } = - require('../../build/swc') as typeof import('../../build/swc') - - let bindings = await loadBindings() + const bindings = getBindingsSync() // For the debugging purpose, check if createNext or equivalent next instance setup in test cases // works correctly. Normally `run-test` hides output so only will be visible when `--debug` flag is used. diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 4395c844d92b8..af3a3a9edcd63 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -8,6 +8,7 @@ import type { PropagateToWorkersField } from './types' import type { NextJsHotReloaderInterface } from '../../dev/hot-reloader-types' import { createDefineEnv } from '../../../build/swc' +import { installBindings } from '../../../build/swc/install-bindings' import fs from 'fs' import url from 'url' import path from 'path' @@ -1280,7 +1281,7 @@ export async function setupDevBundler(opts: SetupOpts) { const isSrcDir = path .relative(opts.dir, opts.pagesDir || opts.appDir || '') .startsWith('src') - + await installBindings(opts.nextConfig.experimental?.useWasmBinary) const result = await startWatcher({ ...opts, isSrcDir, diff --git a/test/unit/next-swc.test.ts b/test/unit/next-swc.test.ts index 46663d9639c01..527f406767227 100644 --- a/test/unit/next-swc.test.ts +++ b/test/unit/next-swc.test.ts @@ -1,5 +1,6 @@ /* eslint-env jest */ import { transform } from 'next/dist/build/swc' +import { installBindings } from 'next/dist/build/swc/install-bindings' import path from 'path' import fsp from 'fs/promises' @@ -11,6 +12,9 @@ const swc = async (code) => { const trim = (s) => s.join('\n').trim().replace(/^\s+/gm, '') describe('next/swc', () => { + beforeAll(async () => { + await installBindings() + }) describe('hook_optimizer', () => { it('should leave alone array destructuring of hooks', async () => { const output = await swc( diff --git a/test/unit/parse-page-static-info.test.ts b/test/unit/parse-page-static-info.test.ts index df004408407f3..73f2f95c65937 100644 --- a/test/unit/parse-page-static-info.test.ts +++ b/test/unit/parse-page-static-info.test.ts @@ -1,4 +1,5 @@ import { getPagesPageStaticInfo } from 'next/dist/build/analysis/get-page-static-info' +import { installBindings } from 'next/dist/build/swc/install-bindings' import { PAGE_TYPES } from 'next/dist/lib/page-types' import { join } from 'path' @@ -9,6 +10,9 @@ function createNextConfig() { } describe('parse page static info', () => { + beforeAll(async () => { + await installBindings() + }) it('should parse nodejs runtime correctly', async () => { const { runtime, getServerSideProps, getStaticProps } = await getPagesPageStaticInfo({