next.js/packages/next/src/build/swc/index.ts
index.ts1857 lines56.6 KB
import path from 'path'
import { pathToFileURL } from 'url'
import { resolveCacheHandlerPathToFilesystem } from '../../lib/format-dynamic-import-path'
import { arch, platform } from 'os'
import { platformArchTriples } from 'next/dist/compiled/@napi-rs/triples'
import * as Log from '../output/log'
import type {
  NextConfigComplete,
  TurbopackLoaderBuiltinCondition,
  TurbopackLoaderItem,
  TurbopackRuleCondition,
  TurbopackRuleConfigCollection,
  TurbopackRuleConfigItem,
} from '../../server/config-shared'
import { type DefineEnvOptions, getDefineEnv } from '../define-env'
import type {
  NapiPartialProjectOptions,
  NapiProjectOptions,
  NapiSourceDiagnostic,
  NapiCodeFrameLocation,
  NapiCodeFrameOptions,
} from './generated-native'
import type {
  Binding,
  CompilationEvent,
  DefineEnv,
  Endpoint,
  HmrChunkNames,
  Lockfile,
  NodeJsHmrUpdate,
  PartialProjectOptions,
  Project,
  ProjectOptions,
  RawEntrypoints,
  Route,
  TurboEngineOptions,
  TurbopackResult,
  TurbopackStackFrame,
  Update,
  UpdateMessage,
  WrittenEndpoint,
} from './types'
import { runLoaderWorkerPool } from './loaderWorkerPool'

export enum HmrTarget {
  Client = 'client',
  Server = 'server',
}

type RawBindings = typeof import('./generated-native')
type RawWasmBindings = typeof import('./generated-wasm') & {
  default?(): Promise<typeof import('./generated-wasm')>
}

const nextVersion = process.env.__NEXT_VERSION as string

const ArchName = arch()
const PlatformName = platform()

function infoLog(...args: any[]) {
  if (process.env.NEXT_PRIVATE_BUILD_WORKER) {
    return
  }
  if (process.env.DEBUG) {
    Log.info(...args)
  }
}

/**
 * Based on napi-rs's target triples, returns triples that have corresponding next-swc binaries.
 */
export function getSupportedArchTriples(): Record<string, any> {
  const { darwin, win32, linux, freebsd, android } = platformArchTriples

  return {
    darwin,
    win32: {
      arm64: win32.arm64,
      ia32: win32.ia32.filter((triple) => triple.abi === 'msvc'),
      x64: win32.x64.filter((triple) => triple.abi === 'msvc'),
    },
    linux: {
      // linux[x64] includes `gnux32` abi, with x64 arch.
      x64: linux.x64.filter((triple) => triple.abi !== 'gnux32'),
      arm64: linux.arm64,
      // This target is being deprecated, however we keep it in `knownDefaultWasmFallbackTriples` for now
      arm: linux.arm,
    },
    // Below targets are being deprecated, however we keep it in `knownDefaultWasmFallbackTriples` for now
    freebsd: {
      x64: freebsd.x64,
    },
    android: {
      arm64: android.arm64,
      arm: android.arm,
    },
  }
}

const triples = (() => {
  const supportedArchTriples = getSupportedArchTriples()
  const targetTriple = supportedArchTriples[PlatformName]?.[ArchName]

  // If we have supported triple, return it right away
  if (targetTriple) {
    return targetTriple
  }

  // If there isn't corresponding target triple in `supportedArchTriples`, check if it's excluded from original raw triples
  // Otherwise, it is completely unsupported platforms.
  let rawTargetTriple = platformArchTriples[PlatformName]?.[ArchName]

  if (rawTargetTriple) {
    Log.warn(
      `next-swc does not have native bindings support for target triple ${rawTargetTriple}. Native features like Turbopack will not be available.`
    )
  } else {
    Log.warn(
      `next-swc does not have native bindings for platform ${PlatformName}/${ArchName}. Native features like Turbopack will not be available.`
    )
  }

  return []
})()

// Allow to specify an absolute path to the custom turbopack binary to load.
// If one of env variables is set, `loadNative` will try to use specified
// binary instead. This is thin, naive interface
// - `loadBindings` will not validate neither path nor the binary.
//
// Note these are internal flag: there's no stability, feature guarantee.
const __INTERNAL_CUSTOM_TURBOPACK_BINDINGS =
  process.env.__INTERNAL_CUSTOM_TURBOPACK_BINDINGS

function checkVersionMismatch(pkgData: any) {
  const version = pkgData.version

  if (version && version !== nextVersion) {
    Log.warn(
      `Mismatching @next/swc version, detected: ${version} while Next.js is on ${nextVersion}. Please ensure these match`
    )
  }
}

// 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 = [
  'x86_64-unknown-freebsd',
  'aarch64-linux-android',
  'arm-linux-androideabi',
  'armv7-unknown-linux-gnueabihf',
  'i686-pc-windows-msvc',
  // WOA targets are TBD, while current userbase is small we may support it in the future
  //'aarch64-pc-windows-msvc',
]

// The last attempt's error code returned when cjs require to native bindings fails.
// If node.js throws an error without error code, this should be `unknown` instead of undefined.
// For the wasm-first targets (`knownDefaultWasmFallbackTriples`) this will be `unsupported_target`.
let lastNativeBindingsLoadErrorCode:
  | 'unknown'
  | 'unsupported_target'
  | string
  | undefined = undefined
// Used to cache racing calls to `loadBindings`
let pendingBindings: Promise<Binding> | undefined
// The cached loaded bindings
let loadedBindings: Binding | undefined = undefined
let downloadWasmPromise: any
let swcTraceFlushGuard: any
let downloadNativeBindingsPromise: Promise<void> | undefined = undefined

export const lockfilePatchPromise: { cur?: Promise<void> } = {}

/** 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
}

/**
 * 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.
 *
 * This function is `async` as wasm requires an asynchronous import in browsers.
 */
export async function loadBindings(
  useWasmBinary: boolean = false
): Promise<Binding> {
  if (loadedBindings) {
    return loadedBindings
  }
  if (pendingBindings) {
    return pendingBindings
  }

  // Increase Rust stack size as some npm packages being compiled need more than the default.
  if (!process.env.RUST_MIN_STACK) {
    process.env.RUST_MIN_STACK = '8388608'
  }

  if (process.env.NEXT_TEST_WASM) {
    useWasmBinary = true
  }

  // rust needs stdout to be blocking, otherwise it will throw an error (on macOS at least) when writing a lot of data (logs) to it
  // see https://github.com/napi-rs/napi-rs/issues/1630
  // and https://github.com/nodejs/node/blob/main/doc/api/process.md#a-note-on-process-io
  if (process.stdout._handle != null) {
    // @ts-ignore
    process.stdout._handle.setBlocking?.(true)
  }
  if (process.stderr._handle != null) {
    // @ts-ignore
    process.stderr._handle.setBlocking?.(true)
  }

  pendingBindings = new Promise(async (resolve, reject) => {
    let attempts: any[] = []
    const disableWasmFallback = process.env.NEXT_DISABLE_SWC_WASM
    const unsupportedPlatform = triples.some(
      (triple: any) =>
        !!triple?.raw && knownDefaultWasmFallbackTriples.includes(triple.raw)
    )
    const isWebContainer = process.versions.webcontainer
    // Normal execution relies on the param `useWasmBinary` flag to load, but
    // in certain cases where there isn't a native binary we always load wasm fallback first.
    const shouldLoadWasmFallbackFirst =
      (!disableWasmFallback && useWasmBinary) ||
      unsupportedPlatform ||
      isWebContainer

    if (!unsupportedPlatform && useWasmBinary) {
      Log.warn(
        `experimental.useWasmBinary is not an option for supported platform ${PlatformName}/${ArchName} and will be ignored.`
      )
    }

    if (shouldLoadWasmFallbackFirst) {
      lastNativeBindingsLoadErrorCode = 'unsupported_target'
      const fallbackBindings = await tryLoadWasmWithFallback(attempts)
      if (fallbackBindings) {
        return resolve(fallbackBindings)
      }
    }

    // Trickle down loading `fallback` bindings:
    //
    // - First, try to load native bindings installed in node_modules.
    // - If that fails with `ERR_MODULE_NOT_FOUND`, treat it as case of https://github.com/npm/cli/issues/4828
    // that host system where generated package lock is not matching to the guest system running on, try to manually
    // download corresponding target triple and load it. This won't be triggered if native bindings are failed to load
    // with other reasons than `ERR_MODULE_NOT_FOUND`.
    // - Lastly, falls back to wasm binding where possible.
    try {
      return resolve(loadNative())
    } catch (a) {
      if (
        Array.isArray(a) &&
        a.every((m) => m.includes('it was not installed'))
      ) {
        let fallbackBindings = await tryLoadNativeWithFallback(attempts)

        if (fallbackBindings) {
          return resolve(fallbackBindings)
        }
      }

      attempts = attempts.concat(a)
    }

    // For these platforms we already tried to load wasm and failed, skip reattempt
    if (!shouldLoadWasmFallbackFirst && !disableWasmFallback) {
      const fallbackBindings = await tryLoadWasmWithFallback(attempts)
      if (fallbackBindings) {
        return resolve(fallbackBindings)
      }
    }

    await logLoadFailure(attempts, true)
    // Reject the promise to propagate the error (process.exit was removed to allow telemetry flush)
    reject(
      new Error(
        `Failed to load SWC binary for ${PlatformName}/${ArchName}, see more info here: https://nextjs.org/docs/messages/failed-loading-swc`
      )
    )
  })
  loadedBindings = await pendingBindings
  pendingBindings = undefined
  return loadedBindings
}

async function tryLoadNativeWithFallback(attempts: Array<string>) {
  const nativeBindingsDirectory = path.join(
    path.dirname(require.resolve('next/package.json')),
    'next-swc-fallback'
  )

  if (!downloadNativeBindingsPromise) {
    downloadNativeBindingsPromise = (
      require('../../lib/download-swc') as typeof import('../../lib/download-swc')
    ).downloadNativeNextSwc(
      nextVersion,
      nativeBindingsDirectory,
      triples.map((triple: any) => triple.platformArchABI)
    )
  }
  await downloadNativeBindingsPromise

  try {
    return loadNative(nativeBindingsDirectory)
  } catch (a: any) {
    attempts.push(...[].concat(a))
  }

  return undefined
}

// helper for loadBindings
async function tryLoadWasmWithFallback(
  attempts: any[]
): Promise<Binding | undefined> {
  try {
    let bindings = await loadWasm('')
    ;(
      require('../../telemetry/events/swc-load-failure') as typeof import('../../telemetry/events/swc-load-failure')
    ).eventSwcLoadFailure({
      wasm: 'enabled',
      nativeBindingsErrorCode: lastNativeBindingsLoadErrorCode,
    })
    return bindings
  } catch (a: any) {
    attempts.push(...[].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 = (
        require('../../lib/download-swc') as typeof import('../../lib/download-swc')
      ).downloadWasmSwc(nextVersion, wasmDirectory)
    }
    await downloadWasmPromise
    let bindings = await loadWasm(wasmDirectory)
    ;(
      require('../../telemetry/events/swc-load-failure') as typeof import('../../telemetry/events/swc-load-failure')
    ).eventSwcLoadFailure({
      wasm: 'fallback',
      nativeBindingsErrorCode: lastNativeBindingsLoadErrorCode,
    })

    // 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: any) {
    attempts.push(...[].concat(a))
  }
}

function loadBindingsSync(): Binding {
  let attempts: any[] = []
  try {
    return loadNative()
  } catch (a) {
    attempts = attempts.concat(a)
  }

  // Fire-and-forget telemetry logging (loadBindingsSync must remain synchronous)
  // Worker error handler will await telemetry.flush() before exit
  logLoadFailure(attempts)

  throw new Error('Failed to load bindings', { cause: attempts })
}

let loggingLoadFailure = false

/**
 * Logs SWC load failure telemetry and error messages.
 *
 * Note: Does NOT call process.exit() - errors must propagate to caller's error handler
 * which will await telemetry.flush() before exit (critical for worker threads with async telemetry).
 */
async function logLoadFailure(attempts: any, 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)
  }

  await (
    require('../../telemetry/events/swc-load-failure') as typeof import('../../telemetry/events/swc-load-failure')
  ).eventSwcLoadFailure({
    wasm: triedWasm ? 'failed' : undefined,
    nativeBindingsErrorCode: lastNativeBindingsLoadErrorCode,
  })
  if (!lockfilePatchPromise.cur) {
    lockfilePatchPromise.cur = (
      require('../../lib/patch-incorrect-lockfile') as typeof import('../../lib/patch-incorrect-lockfile')
    )
      .patchIncorrectLockfile(process.cwd())
      .catch(console.error)
  }
  await lockfilePatchPromise.cur

  Log.error(
    `Failed to load SWC binary for ${PlatformName}/${ArchName}, see more info here: https://nextjs.org/docs/messages/failed-loading-swc`
  )
}

type RustifiedEnv = { name: string; value: string }[]
type RustifiedOptionEnv = { name: string; value: string | undefined }[]

export function createDefineEnv({
  isTurbopack,
  clientRouterFilters,
  config,
  dev,
  distDir,
  projectPath,
  fetchCacheKeyPrefix,
  hasRewrites,
  middlewareMatchers,
  rewrites,
}: Omit<
  DefineEnvOptions,
  'isClient' | 'isNodeOrEdgeCompilation' | 'isEdgeServer' | 'isNodeServer'
>): DefineEnv {
  let defineEnv: DefineEnv = {
    client: [],
    edge: [],
    nodejs: [],
  }

  for (const variant of Object.keys(defineEnv) as (keyof typeof defineEnv)[]) {
    defineEnv[variant] = rustifyOptionEnv(
      getDefineEnv({
        isTurbopack,
        clientRouterFilters,
        config,
        dev,
        distDir,
        projectPath,
        fetchCacheKeyPrefix,
        hasRewrites,
        isClient: variant === 'client',
        isEdgeServer: variant === 'edge',
        isNodeServer: variant === 'nodejs',
        middlewareMatchers,
        rewrites,
      })
    )
  }

  return defineEnv
}

function rustifyEnv(env: Record<string, string>): RustifiedEnv {
  return Object.entries(env)
    .filter(([_, value]) => value != null)
    .map(([name, value]) => ({
      name,
      value,
    }))
}

function rustifyOptionEnv(
  env: Record<string, string | undefined>
): RustifiedOptionEnv {
  return Object.entries(env).map(([name, value]) => ({
    name,
    value,
  }))
}

const normalizePathOnWindows = (p: string) =>
  path.sep === '\\' ? p.replace(/\\/g, '/') : p

// TODO(sokra) Support wasm option.
function bindingToApi(
  binding: RawBindings,
  bindingPath: string,
  _wasm: boolean
): Binding['turbo']['createProject'] {
  type NativeFunction<T> = (
    callback: (err: Error, value: T) => void
  ) => Promise<{ __napiType: 'RootTask' }>

  type NapiEndpoint = { __napiType: 'Endpoint' }

  type NapiEntrypoints = {
    routes: NapiRoute[]
    middleware?: NapiMiddleware
    instrumentation?: NapiInstrumentation
    pagesDocumentEndpoint: NapiEndpoint
    pagesAppEndpoint: NapiEndpoint
    pagesErrorEndpoint: NapiEndpoint
  }

  type NapiMiddleware = {
    endpoint: NapiEndpoint
    isProxy: boolean
  }

  type NapiInstrumentation = {
    nodeJs: NapiEndpoint
    edge: NapiEndpoint
  }

  type NapiRoute = {
    pathname: string
  } & (
    | {
        type: 'page'
        htmlEndpoint: NapiEndpoint
        dataEndpoint: NapiEndpoint
      }
    | {
        type: 'page-api'
        endpoint: NapiEndpoint
      }
    | {
        type: 'app-page'
        pages: {
          originalName: string
          htmlEndpoint: NapiEndpoint
          rscEndpoint: NapiEndpoint
        }[]
      }
    | {
        type: 'app-route'
        originalName: string
        endpoint: NapiEndpoint
      }
    | {
        type: 'conflict'
      }
  )

  const cancel = new (class Cancel extends Error {})()

  /**
   * Utility function to ensure all variants of an enum are handled.
   */
  function invariant(
    never: never,
    computeMessage: (arg: any) => string
  ): never {
    throw new Error(`Invariant: ${computeMessage(never)}`)
  }

  /**
   * Calls a native function and streams the result.
   * If useBuffer is true, all values will be preserved, potentially buffered
   * if consumed slower than produced. Else, only the latest value will be
   * preserved.
   */
  function subscribe<T>(
    useBuffer: boolean,
    nativeFunction:
      | NativeFunction<T>
      | ((callback: (err: Error, value: T) => void) => Promise<void>)
  ): AsyncIterableIterator<T> {
    type BufferItem =
      | { err: Error; value: undefined }
      | { err: undefined; value: T }
    // A buffer of produced items. This will only contain values if the
    // consumer is slower than the producer.
    let buffer: BufferItem[] = []
    // A deferred value waiting for the next produced item. This will only
    // exist if the consumer is faster than the producer.
    let waiting:
      | {
          resolve: (value: T) => void
          reject: (error: Error) => void
        }
      | undefined
    let canceled = false

    // The native function will call this every time it emits a new result. We
    // either need to notify a waiting consumer, or buffer the new result until
    // the consumer catches up.
    function emitResult(err: Error | undefined, value: T | undefined) {
      if (waiting) {
        let { resolve, reject } = waiting
        waiting = undefined
        if (err) reject(err)
        else resolve(value!)
      } else {
        const item = { err, value } as BufferItem
        if (useBuffer) buffer.push(item)
        else buffer[0] = item
      }
    }

    async function* createIterator() {
      const task = await nativeFunction(emitResult)
      try {
        while (!canceled) {
          if (buffer.length > 0) {
            const item = buffer.shift()!
            if (item.err) throw item.err
            yield item.value
          } else {
            // eslint-disable-next-line no-loop-func
            yield new Promise<T>((resolve, reject) => {
              waiting = { resolve, reject }
            })
          }
        }
      } catch (e) {
        if (e === cancel) return
        throw e
      } finally {
        if (task) {
          binding.rootTaskDispose(task)
        }
      }
    }

    const iterator = createIterator()
    iterator.return = async () => {
      canceled = true
      if (waiting) waiting.reject(cancel)
      return { value: undefined, done: true } as IteratorReturnResult<never>
    }
    return iterator
  }

  async function rustifyProjectOptions(
    options: ProjectOptions
  ): Promise<NapiProjectOptions> {
    return {
      ...options,
      nextConfig: await serializeNextConfig(
        options.nextConfig,
        path.join(options.rootPath, options.projectPath)
      ),
      env: rustifyEnv(options.env),
    }
  }

  async function rustifyPartialProjectOptions(
    options: PartialProjectOptions
  ): Promise<NapiPartialProjectOptions> {
    return {
      ...options,
      nextConfig:
        options.nextConfig &&
        (await serializeNextConfig(
          options.nextConfig,
          path.join(options.rootPath, options.projectPath)
        )),
      env: options.env && rustifyEnv(options.env),
    }
  }

  class ProjectImpl implements Project {
    private readonly _nativeProject: { __napiType: 'Project' }

    constructor(nativeProject: { __napiType: 'Project' }) {
      this._nativeProject = nativeProject

      if (typeof binding.registerWorkerScheduler === 'function') {
        runLoaderWorkerPool(binding, bindingPath)
      }
    }

    async update(options: PartialProjectOptions) {
      await binding.projectUpdate(
        this._nativeProject,
        await rustifyPartialProjectOptions(options)
      )
    }

    async writeAnalyzeData(
      appDirOnly: boolean
    ): Promise<TurbopackResult<void>> {
      const napiResult = (await binding.projectWriteAnalyzeData(
        this._nativeProject,
        appDirOnly
      )) as TurbopackResult<void>
      return napiResult
    }

    async getAllCompilationIssues(): Promise<TurbopackResult<void>> {
      const napiResult = (await binding.projectGetAllCompilationIssues(
        this._nativeProject
      )) as TurbopackResult<void>
      return napiResult
    }

    async writeAllEntrypointsToDisk(
      appDirOnly: boolean
    ): Promise<TurbopackResult<Partial<RawEntrypoints>>> {
      const napiEndpoints = (await binding.projectWriteAllEntrypointsToDisk(
        this._nativeProject,
        appDirOnly
      )) as TurbopackResult<Partial<NapiEntrypoints>>

      if ('routes' in napiEndpoints) {
        return napiEntrypointsToRawEntrypoints(
          napiEndpoints as TurbopackResult<NapiEntrypoints>
        )
      } else {
        return {
          issues: napiEndpoints.issues,
          diagnostics: napiEndpoints.diagnostics,
        }
      }
    }

    entrypointsSubscribe() {
      const subscription = subscribe<TurbopackResult<NapiEntrypoints | {}>>(
        false,
        async (callback) =>
          binding.projectEntrypointsSubscribe(this._nativeProject, callback)
      )
      return (async function* () {
        for await (const entrypoints of subscription) {
          if ('routes' in (entrypoints as TurbopackResult<NapiEntrypoints>)) {
            yield napiEntrypointsToRawEntrypoints(
              entrypoints as TurbopackResult<NapiEntrypoints>
            )
          } else {
            yield {
              issues: entrypoints.issues,
              diagnostics: entrypoints.diagnostics,
            } as TurbopackResult<{}>
          }
        }
      })()
    }

    hmrEvents(
      chunkName: string,
      target: HmrTarget.Client
    ): AsyncIterableIterator<TurbopackResult<Update>>
    hmrEvents(
      chunkName: string,
      target: HmrTarget.Server
    ): AsyncIterableIterator<TurbopackResult<NodeJsHmrUpdate>>
    hmrEvents(chunkName: string, target: HmrTarget.Client | HmrTarget.Server) {
      return subscribe(true, async (callback) =>
        binding.projectHmrEvents(
          this._nativeProject,
          chunkName,
          target,
          callback
        )
      )
    }

    /**
     * Subscribe to the list of output chunk paths that can receive HMR updates.
     * Chunk paths are output file paths like "server/chunks/ssr/..._.js" for server
     * or "_next/static/chunks/app/page.js" for client.
     */
    hmrChunkNamesSubscribe(target: HmrTarget) {
      return subscribe<TurbopackResult<HmrChunkNames>>(
        false,
        async (callback) =>
          binding.projectHmrChunkNamesSubscribe(
            this._nativeProject,
            target,
            callback
          )
      )
    }

    traceSource(
      stackFrame: TurbopackStackFrame,
      currentDirectoryFileUrl: string
    ): Promise<TurbopackStackFrame | null> {
      return binding.projectTraceSource(
        this._nativeProject,
        stackFrame,
        currentDirectoryFileUrl
      )
    }

    getSourceForAsset(filePath: string): Promise<string | null> {
      return binding.projectGetSourceForAsset(this._nativeProject, filePath)
    }

    getSourceMap(filePath: string): Promise<string | null> {
      return binding.projectGetSourceMap(this._nativeProject, filePath)
    }

    getSourceMapSync(filePath: string): string | null {
      return binding.projectGetSourceMapSync(this._nativeProject, filePath)
    }

    updateInfoSubscribe(aggregationMs: number) {
      return subscribe<TurbopackResult<UpdateMessage>>(true, async (callback) =>
        binding.projectUpdateInfoSubscribe(
          this._nativeProject,
          aggregationMs,
          callback
        )
      )
    }

    compilationEventsSubscribe(eventTypes?: string[]) {
      return subscribe<TurbopackResult<CompilationEvent>>(
        true,
        async (callback) => {
          binding.projectCompilationEventsSubscribe(
            this._nativeProject,
            callback,
            eventTypes
          )
        }
      )
    }

    invalidateFileSystemCache(): Promise<void> {
      return binding.projectInvalidateFileSystemCache(this._nativeProject)
    }

    shutdown(): Promise<void> {
      return binding.projectShutdown(this._nativeProject)
    }

    onExit(): Promise<void> {
      return binding.projectOnExit(this._nativeProject)
    }
  }

  class EndpointImpl implements Endpoint {
    private readonly _nativeEndpoint: { __napiType: 'Endpoint' }

    constructor(nativeEndpoint: { __napiType: 'Endpoint' }) {
      this._nativeEndpoint = nativeEndpoint
    }

    async writeToDisk(): Promise<TurbopackResult<WrittenEndpoint>> {
      return (await binding.endpointWriteToDisk(
        this._nativeEndpoint
      )) as TurbopackResult<WrittenEndpoint>
    }

    async clientChanged(): Promise<AsyncIterableIterator<TurbopackResult>> {
      const clientSubscription = subscribe<TurbopackResult>(
        false,
        async (callback) =>
          binding.endpointClientChangedSubscribe(this._nativeEndpoint, callback)
      )
      await clientSubscription.next()
      return clientSubscription
    }

    async serverChanged(
      includeIssues: boolean
    ): Promise<AsyncIterableIterator<TurbopackResult>> {
      const serverSubscription = subscribe<TurbopackResult>(
        false,
        async (callback) =>
          binding.endpointServerChangedSubscribe(
            this._nativeEndpoint,
            includeIssues,
            callback
          )
      )
      await serverSubscription.next()
      return serverSubscription
    }
  }

  async function serializeNextConfig(
    nextConfig: NextConfigComplete,
    projectPath: string
  ): Promise<string> {
    // Avoid mutating the existing `nextConfig` object. NOTE: This is only a shallow clone.
    let nextConfigSerializable: Record<string, any> = { ...nextConfig }

    // These values are never read by Turbopack and are potentially non-serializable.
    nextConfigSerializable.exportPathMap = {}
    nextConfigSerializable.generateBuildId =
      nextConfigSerializable.generateBuildId && {}
    nextConfigSerializable.webpack = nextConfigSerializable.webpack && {}

    if (nextConfigSerializable.modularizeImports) {
      nextConfigSerializable.modularizeImports = Object.fromEntries(
        Object.entries<any>(nextConfigSerializable.modularizeImports).map(
          ([mod, config]) => [
            mod,
            {
              ...config,
              transform:
                typeof config.transform === 'string'
                  ? config.transform
                  : Object.entries(config.transform),
            },
          ]
        )
      )
    }

    // These are relative paths, but might be backslash-separated on Windows
    nextConfigSerializable.distDir = normalizePathOnWindows(
      nextConfigSerializable.distDir
    )
    nextConfigSerializable.distDirRoot = normalizePathOnWindows(
      nextConfigSerializable.distDirRoot
    )

    // loaderFile is an absolute path, we need it to be relative for turbopack.
    if (nextConfigSerializable.images.loaderFile) {
      nextConfigSerializable.images = {
        ...nextConfigSerializable.images,
        loaderFile:
          './' +
          normalizePathOnWindows(
            path.relative(projectPath, nextConfigSerializable.images.loaderFile)
          ),
      }
    }

    // cacheHandler can be an absolute path, we need it to be relative for turbopack.
    if (nextConfigSerializable.cacheHandler) {
      const resolvedCacheHandler = resolveCacheHandlerPathToFilesystem(
        nextConfigSerializable.cacheHandler
      )
      nextConfigSerializable.cacheHandler =
        './' +
        normalizePathOnWindows(
          path.isAbsolute(resolvedCacheHandler)
            ? path.relative(projectPath, resolvedCacheHandler)
            : resolvedCacheHandler
        )
    }
    if (nextConfigSerializable.cacheHandlers) {
      nextConfigSerializable.cacheHandlers = Object.fromEntries(
        Object.entries(
          nextConfigSerializable.cacheHandlers as Record<string, string>
        )
          .filter(([_, value]) => value != null)
          .map(([key, value]) => {
            const resolved = resolveCacheHandlerPathToFilesystem(value)
            return [
              key,
              './' +
                normalizePathOnWindows(
                  path.isAbsolute(resolved)
                    ? path.relative(projectPath, resolved)
                    : resolved
                ),
            ]
          })
      )
    }

    if (nextConfigSerializable.turbopack != null) {
      // clone to allow in-place mutations
      const turbopack = { ...nextConfigSerializable.turbopack }

      if (turbopack.rules) {
        turbopack.rules = serializeTurbopackRules(turbopack.rules)
      }

      // Serialize ignoreIssue rules: convert RegExp to {source, flags}
      if (turbopack.ignoreIssue) {
        function serializePatternField(
          value: string | RegExp,
          stringType: 'glob' | 'string'
        ) {
          if (value instanceof RegExp) {
            return {
              type: 'regex' as const,
              source: value.source,
              flags: value.flags,
            }
          }
          return { type: stringType, value }
        }

        turbopack.ignoreIssue = turbopack.ignoreIssue.map(
          (rule: {
            path: string | RegExp
            title?: string | RegExp
            description?: string | RegExp
          }) => ({
            path: serializePatternField(rule.path, 'glob'),
            title:
              rule.title != null
                ? serializePatternField(rule.title, 'string')
                : undefined,
            description:
              rule.description != null
                ? serializePatternField(rule.description, 'string')
                : undefined,
          })
        )
      }

      nextConfigSerializable.turbopack = turbopack
    }

    return JSON.stringify(nextConfigSerializable, null, 2)
  }

  type SerializedRuleCondition =
    | { all: SerializedRuleCondition[] }
    | { any: SerializedRuleCondition[] }
    | { not: SerializedRuleCondition }
    | TurbopackLoaderBuiltinCondition
    | {
        path?:
          | { type: 'regex'; value: { source: string; flags: string } }
          | { type: 'glob'; value: string }
        content?: { source: string; flags: string }
        query?:
          | { type: 'regex'; value: { source: string; flags: string } }
          | { type: 'constant'; value: string }
        contentType?:
          | { type: 'regex'; value: { source: string; flags: string } }
          | { type: 'glob'; value: string }
      }

  // converts regexes to a `RegexComponents` object so that it can be JSON-serialized when passed to
  // Turbopack
  function serializeRuleCondition(
    cond: TurbopackRuleCondition
  ): SerializedRuleCondition {
    function regexComponents(regex: RegExp) {
      return {
        source: regex.source,
        flags: regex.flags,
      }
    }

    if (typeof cond === 'string') {
      return cond
    } else if ('all' in cond) {
      return { ...cond, all: cond.all.map(serializeRuleCondition) }
    } else if ('any' in cond) {
      return { ...cond, any: cond.any.map(serializeRuleCondition) }
    } else if ('not' in cond) {
      return { ...cond, not: serializeRuleCondition(cond.not) }
    } else {
      return {
        ...cond,
        path:
          cond.path == null
            ? undefined
            : cond.path instanceof RegExp
              ? {
                  type: 'regex',
                  value: regexComponents(cond.path),
                }
              : { type: 'glob', value: cond.path },
        content: cond.content && regexComponents(cond.content),
        query:
          cond.query == null
            ? undefined
            : cond.query instanceof RegExp
              ? {
                  type: 'regex',
                  value: regexComponents(cond.query),
                }
              : { type: 'constant', value: cond.query },
        contentType:
          cond.contentType == null
            ? undefined
            : cond.contentType instanceof RegExp
              ? {
                  type: 'regex',
                  value: regexComponents(cond.contentType),
                }
              : { type: 'glob', value: cond.contentType },
      }
    }
  }

  // Note: Returns an updated `turbopackRules` with serialized conditions. Does not mutate in-place.
  function serializeTurbopackRules(
    turbopackRules: Record<string, TurbopackRuleConfigCollection>
  ): Record<string, any> {
    const serializedRules: Record<string, any> = {}
    for (const [glob, rule] of Object.entries(turbopackRules)) {
      if (Array.isArray(rule)) {
        serializedRules[glob] = rule.map((item) => {
          if (
            typeof item !== 'string' &&
            ('loaders' in item || 'type' in item || 'condition' in item)
          ) {
            return serializeConfigItem(item as TurbopackRuleConfigItem, glob)
          } else {
            checkLoaderItem(item as TurbopackLoaderItem, glob)
            return item
          }
        })
      } else {
        serializedRules[glob] = serializeConfigItem(rule, glob)
      }
    }

    return serializedRules

    function serializeConfigItem(
      rule: TurbopackRuleConfigItem,
      glob: string
    ): any {
      if (!rule) return rule
      if (rule.loaders) {
        for (const item of rule.loaders) {
          checkLoaderItem(item, glob)
        }
      }
      let serializedRule: any = rule
      if (rule.condition != null) {
        serializedRule = {
          ...rule,
          condition: serializeRuleCondition(rule.condition),
        }
      }
      return serializedRule
    }

    function checkLoaderItem(loaderItem: TurbopackLoaderItem, glob: string) {
      if (
        typeof loaderItem !== 'string' &&
        !(require('util') as typeof import('util')).isDeepStrictEqual(
          loaderItem,
          JSON.parse(JSON.stringify(loaderItem))
        )
      ) {
        throw new Error(
          `loader ${loaderItem.loader} for match "${glob}" does not have serializable options. ` +
            'Ensure that options passed are plain JavaScript objects and values.'
        )
      }
    }
  }

  function napiEntrypointsToRawEntrypoints(
    entrypoints: TurbopackResult<NapiEntrypoints>
  ): TurbopackResult<RawEntrypoints> {
    const routes = new Map()
    for (const { pathname, ...nativeRoute } of entrypoints.routes) {
      let route: Route
      const routeType = nativeRoute.type
      switch (routeType) {
        case 'page':
          route = {
            type: 'page',
            htmlEndpoint: new EndpointImpl(nativeRoute.htmlEndpoint),
            dataEndpoint: new EndpointImpl(nativeRoute.dataEndpoint),
          }
          break
        case 'page-api':
          route = {
            type: 'page-api',
            endpoint: new EndpointImpl(nativeRoute.endpoint),
          }
          break
        case 'app-page':
          route = {
            type: 'app-page',
            pages: nativeRoute.pages.map((page) => ({
              originalName: page.originalName,
              htmlEndpoint: new EndpointImpl(page.htmlEndpoint),
              rscEndpoint: new EndpointImpl(page.rscEndpoint),
            })),
          }
          break
        case 'app-route':
          route = {
            type: 'app-route',
            originalName: nativeRoute.originalName,
            endpoint: new EndpointImpl(nativeRoute.endpoint),
          }
          break
        case 'conflict':
          route = {
            type: 'conflict',
          }
          break
        default: {
          const _exhaustiveCheck: never = routeType
          invariant(
            nativeRoute,
            () => `Unknown route type: ${_exhaustiveCheck}`
          )
        }
      }
      routes.set(pathname, route)
    }
    const napiMiddlewareToMiddleware = (middleware: NapiMiddleware) => ({
      endpoint: new EndpointImpl(middleware.endpoint),
      isProxy: middleware.isProxy,
    })
    const middleware = entrypoints.middleware
      ? napiMiddlewareToMiddleware(entrypoints.middleware)
      : undefined
    const napiInstrumentationToInstrumentation = (
      instrumentation: NapiInstrumentation
    ) => ({
      nodeJs: new EndpointImpl(instrumentation.nodeJs),
      edge: new EndpointImpl(instrumentation.edge),
    })
    const instrumentation = entrypoints.instrumentation
      ? napiInstrumentationToInstrumentation(entrypoints.instrumentation)
      : undefined

    return {
      routes,
      middleware,
      instrumentation,
      pagesDocumentEndpoint: new EndpointImpl(
        entrypoints.pagesDocumentEndpoint
      ),
      pagesAppEndpoint: new EndpointImpl(entrypoints.pagesAppEndpoint),
      pagesErrorEndpoint: new EndpointImpl(entrypoints.pagesErrorEndpoint),
      issues: entrypoints.issues,
      diagnostics: entrypoints.diagnostics,
    }
  }

  return async function createProject(
    options: ProjectOptions,
    turboEngineOptions,
    callbacks?: import('./types').TurbopackProjectCallbacks
  ) {
    return new ProjectImpl(
      await binding.projectNew(
        await rustifyProjectOptions(options),
        turboEngineOptions || {},
        {
          throwTurbopackInternalError: (
            require('../../shared/lib/turbopack/internal-error') as typeof import('../../shared/lib/turbopack/internal-error')
          ).throwTurbopackInternalError,
          onBeforeDeferredEntries: callbacks?.onBeforeDeferredEntries,
        }
      )
    )
  }
}

// helper for loadWasm
async function loadWasmRawBindings(importPath = ''): Promise<RawWasmBindings> {
  let attempts = []

  // Used by `run-tests` to force use of a locally-built wasm binary. This environment variable is
  // unstable and subject to change.
  const testWasmDir = process.env.NEXT_TEST_WASM_DIR

  if (testWasmDir) {
    // assume these are node.js bindings and don't need a call to `.default()`
    const rawBindings = await import(
      pathToFileURL(path.join(testWasmDir, 'wasm.js')).toString()
    )
    infoLog(`next-swc build: wasm build ${testWasmDir}`)
    return rawBindings
  } else {
    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')
        }
        const importedRawBindings = await import(
          pathToFileURL(pkgPath).toString()
        )
        let rawBindings
        if (pkg === '@next/swc-wasm-web') {
          // https://rustwasm.github.io/docs/wasm-bindgen/examples/without-a-bundler.html
          // `default` must be called to initialize the module
          rawBindings = await importedRawBindings.default!()
        } else {
          rawBindings = importedRawBindings
        }
        infoLog(`next-swc build: wasm build ${pkg}`)
        return rawBindings
      } catch (e: any) {
        // 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
}

// helper for tryLoadWasmWithFallback / loadBindings.
async function loadWasm(importPath = '') {
  const rawBindings = await loadWasmRawBindings(importPath)

  function removeUndefined(obj: any): any {
    // serde-wasm-bindgen expect that `undefined` values map to `()` in rust, but we want to treat
    // those fields as non-existent, so remove them before passing them to rust.
    //
    // The native (non-wasm) bindings use `JSON.stringify`, which strips undefined values.
    if (typeof obj !== 'object' || obj === null) {
      return obj
    }
    if (Array.isArray(obj)) {
      return obj.map(removeUndefined)
    }
    const newObj: { [key: string]: any } = {}
    for (const [k, v] of Object.entries(obj)) {
      if (typeof v !== 'undefined') {
        newObj[k] = removeUndefined(v)
      }
    }
    return newObj
  }

  // Note wasm binary does not support async intefaces yet, all async
  // interface coereces to sync interfaces.
  let wasmBindings = {
    css: {
      lightning: {
        transform: function (_options: any) {
          throw new Error(
            '`css.lightning.transform` is not supported by the wasm bindings.'
          )
        },
        transformStyleAttr: function (_options: any) {
          throw new Error(
            '`css.lightning.transformStyleAttr` is not supported by the wasm bindings.'
          )
        },
        featureNamesToMask: function (_names: string[]) {
          throw new Error(
            '`css.lightning.featureNamesToMask` is not supported by the wasm bindings.'
          )
        },
      },
    },
    isWasm: true,
    transform(src: string, options: any): Promise<any> {
      return rawBindings.transform(src.toString(), removeUndefined(options))
    },
    transformSync(src: string, options: any) {
      return rawBindings.transformSync(src.toString(), removeUndefined(options))
    },
    minify(src: string, options: any): Promise<any> {
      return rawBindings.minify(src.toString(), removeUndefined(options))
    },
    minifySync(src: string, options: any) {
      return rawBindings.minifySync(src.toString(), removeUndefined(options))
    },
    parse(src: string, options: any): Promise<any> {
      return rawBindings.parse(src.toString(), removeUndefined(options))
    },
    getTargetTriple() {
      return undefined
    },
    turbo: {
      createProject(
        _options: ProjectOptions,
        _turboEngineOptions?: TurboEngineOptions | undefined,
        _callbacks?: import('./types').TurbopackProjectCallbacks | undefined
      ): Promise<Project> {
        throw new Error(
          `Turbopack is not supported on this platform (${PlatformName}/${ArchName}) because native bindings are not available. ` +
            `Only WebAssembly (WASM) bindings were loaded, and Turbopack requires native bindings. ` +
            `Use the --webpack flag instead.`
        )
      },
      startTurbopackTraceServerHandle(
        _traceFilePath: string,
        _port: number | undefined
      ) {
        throw new Error(
          `Turbopack trace server is not supported on this platform (${PlatformName}/${ArchName}) because native bindings are not available. ` +
            `Only WebAssembly (WASM) bindings were loaded, and Turbopack requires native bindings.`
        )
      },
      queryTraceSpans(_handle: any, _options: any) {
        throw new Error(
          `Turbopack trace server is not supported on this platform (${PlatformName}/${ArchName}) because native bindings are not available. ` +
            `Only WebAssembly (WASM) bindings were loaded, and Turbopack requires native bindings.`
        )
      },
      databaseCompact(_path: string): Promise<void> {
        throw new Error(
          'Turbopack database compaction is not supported on this platform'
        )
      },
    },
    mdx: {
      compile(src: string, options: any) {
        return rawBindings.mdxCompile(
          src,
          removeUndefined(getMdxOptions(options))
        )
      },
      compileSync(src: string, options: any) {
        return rawBindings.mdxCompileSync(
          src,
          removeUndefined(getMdxOptions(options))
        )
      },
    },
    reactCompiler: {
      isReactCompilerRequired(_filename: string) {
        return Promise.resolve(true)
      },
    },
    rspack: {
      getModuleNamedExports(_resourcePath: string): Promise<string[]> {
        throw new Error(
          '`rspack.getModuleNamedExports` is not supported by the wasm bindings.'
        )
      },
      warnForEdgeRuntime(
        _source: string,
        _isProduction: boolean
      ): Promise<NapiSourceDiagnostic[]> {
        throw new Error(
          '`rspack.warnForEdgeRuntime` is not supported by the wasm bindings.'
        )
      },
    },
    expandNextJsTemplate(
      content: Buffer,
      templatePath: string,
      nextPackageDirPath: string,
      replacements: Record<`VAR_${string}`, string>,
      injections: Record<string, string>,
      imports: Record<string, string | null>
    ): string {
      return rawBindings.expandNextJsTemplate(
        content,
        templatePath,
        nextPackageDirPath,
        replacements,
        injections,
        imports
      )
    },
    codeFrameColumns(
      source: string,
      location: NapiCodeFrameLocation,
      options?: NapiCodeFrameOptions
    ): string | undefined {
      return rawBindings.codeFrameColumns(
        Buffer.from(source),
        location,
        options
      )
    },
    lockfileTryAcquire(_filePath: string, _content?: string | null) {
      throw new Error(
        '`lockfileTryAcquire` is not supported by the wasm bindings.'
      )
    },
    lockfileTryAcquireSync(_filePath: string, _content?: string | null) {
      throw new Error(
        '`lockfileTryAcquireSync` is not supported by the wasm bindings.'
      )
    },
    lockfileUnlock(_lockfile: Lockfile) {
      throw new Error('`lockfileUnlock` is not supported by the wasm bindings.')
    },
    lockfileUnlockSync(_lockfile: Lockfile) {
      throw new Error(
        '`lockfileUnlockSync` is not supported by the wasm bindings.'
      )
    },
  }
  return wasmBindings
}

/**
 * Loads the native (non-wasm) bindings. Prefer `loadBindings` over this API, as that includes a
 * wasm fallback.
 */
function loadNative(importPath?: string): Binding {
  if (loadedBindings) {
    return loadedBindings
  }

  if (process.env.NEXT_TEST_WASM) {
    throw new Error('cannot run loadNative when `NEXT_TEST_WASM` is set')
  }

  const customBindingsPath = !!__INTERNAL_CUSTOM_TURBOPACK_BINDINGS
    ? require.resolve(__INTERNAL_CUSTOM_TURBOPACK_BINDINGS)
    : null
  const customBindings: RawBindings =
    customBindingsPath != null ? require(customBindingsPath!) : null
  let bindings: RawBindings = customBindings
  let bindingsPath = customBindingsPath
  // callers expect that if loadNative throws, it's an array of strings.
  let attempts: any[] = []

  const NEXT_TEST_NATIVE_DIR = process.env.NEXT_TEST_NATIVE_DIR
  for (const triple of triples) {
    if (NEXT_TEST_NATIVE_DIR) {
      try {
        const bindingForTest = `${NEXT_TEST_NATIVE_DIR}/next-swc.${triple.platformArchABI}.node`
        // Use the binary directly to skip `pnpm pack` for testing as it's slow because of the large native binary.
        bindingsPath = require.resolve(bindingForTest)
        bindings = require(bindingsPath)
        infoLog(
          'next-swc build: local built @next/swc from NEXT_TEST_NATIVE_DIR'
        )
        break
      } catch (e: any) {
        attempts.push(
          `Failed to load triple ${triple.platformArchABI}: ${e.message ?? e}`
        )
      }
    } else if (process.env.NEXT_TEST_NATIVE_IGNORE_LOCAL_INSTALL !== 'true') {
      try {
        const normalBinding = `@next/swc/native/next-swc.${triple.platformArchABI}.node`
        bindings = require(normalBinding)
        bindingsPath = require.resolve(normalBinding)
        infoLog('next-swc build: local built @next/swc')
        break
      } catch (e) {}
    }
  }

  if (!bindings) {
    if (NEXT_TEST_NATIVE_DIR) {
      throw attempts
    }

    for (const triple of triples) {
      let pkg = importPath
        ? path.join(
            importPath,
            `@next/swc-${triple.platformArchABI}`,
            `next-swc.${triple.platformArchABI}.node`
          )
        : `@next/swc-${triple.platformArchABI}`
      try {
        bindings = require(pkg)
        bindingsPath = require.resolve(pkg)
        if (!importPath) {
          checkVersionMismatch(require(`${pkg}/package.json`))
        }
        break
      } catch (e: any) {
        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}`
          )
        }
        lastNativeBindingsLoadErrorCode = e?.code ?? 'unknown'
      }
    }
  }

  if (bindings) {
    loadedBindings = {
      isWasm: false,
      transform(src: string, options: any) {
        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: string, options: any) {
        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: string, options: any) {
        return bindings.minify(Buffer.from(src), toBuffer(options ?? {}))
      },

      minifySync(src: string, options: any) {
        return bindings.minifySync(Buffer.from(src), toBuffer(options ?? {}))
      },

      parse(src: string, options: any) {
        return bindings.parse(src, toBuffer(options ?? {}))
      },

      getTargetTriple: bindings.getTargetTriple,
      initCustomTraceSubscriber: bindings.initCustomTraceSubscriber,
      teardownTraceSubscriber: bindings.teardownTraceSubscriber,
      turbo: {
        createProject: bindingToApi(
          customBindings ?? bindings,
          customBindingsPath ?? bindingsPath!,
          false
        ),
        startTurbopackTraceServerHandle(traceFilePath, port) {
          return (customBindings ?? bindings).startTurbopackTraceServerHandle(
            traceFilePath,
            port
          )
        },
        queryTraceSpans(handle, options) {
          return (customBindings ?? bindings).queryTraceSpans(handle, options)
        },
        databaseCompact(dbPath: string) {
          return (customBindings ?? bindings).turbopackDatabaseCompact(dbPath)
        },
      },
      mdx: {
        compile(src: string, options: any) {
          return bindings.mdxCompile(src, toBuffer(getMdxOptions(options)))
        },
        compileSync(src: string, options: any) {
          bindings.mdxCompileSync(src, toBuffer(getMdxOptions(options)))
        },
      },
      css: {
        lightning: {
          transform(transformOptions: any) {
            return bindings.lightningCssTransform(transformOptions)
          },
          transformStyleAttr(transformAttrOptions: any) {
            return bindings.lightningCssTransformStyleAttribute(
              transformAttrOptions
            )
          },
          featureNamesToMask(names: string[]) {
            return bindings.lightningcssFeatureNamesToMaskNapi(names)
          },
        },
      },
      reactCompiler: {
        isReactCompilerRequired: (filename: string) => {
          return bindings.isReactCompilerRequired(filename)
        },
      },
      rspack: {
        getModuleNamedExports: function (
          resourcePath: string
        ): Promise<string[]> {
          return bindings.getModuleNamedExports(resourcePath)
        },
        warnForEdgeRuntime: function (
          source: string,
          isProduction: boolean
        ): Promise<NapiSourceDiagnostic[]> {
          return bindings.warnForEdgeRuntime(source, isProduction)
        },
      },
      expandNextJsTemplate(
        content: Buffer,
        templatePath: string,
        nextPackageDirPath: string,
        replacements: Record<`VAR_${string}`, string>,
        injections: Record<string, string>,
        imports: Record<string, string | null>
      ): string {
        return bindings.expandNextJsTemplate(
          content,
          templatePath,
          nextPackageDirPath,
          replacements,
          injections,
          imports
        )
      },
      lockfileTryAcquire(filePath: string, content?: string | null) {
        return bindings.lockfileTryAcquire(filePath, content)
      },
      lockfileTryAcquireSync(filePath: string, content?: string | null) {
        return bindings.lockfileTryAcquireSync(filePath, content)
      },
      lockfileUnlock(lockfile: Lockfile) {
        return bindings.lockfileUnlock(lockfile)
      },
      lockfileUnlockSync(lockfile: Lockfile) {
        return bindings.lockfileUnlockSync(lockfile)
      },
      codeFrameColumns(source, location, options) {
        // napi-rs translates Option::None as null but wasm-bindgen translates it to `null`
        // convert here for consistency
        return bindings.codeFrameColumns(source, location, options) ?? undefined
      },
    }
    return loadedBindings!
  }

  throw attempts
}

/// Build a mdx options object contains default values that
/// can be parsed with serde_wasm_bindgen.
function getMdxOptions(options: any = {}) {
  return {
    ...options,
    development: options.development ?? false,
    jsx: options.jsx ?? false,
    mdxType: options.mdxType ?? 'commonMark',
  }
}

function toBuffer(t: any) {
  return Buffer.from(JSON.stringify(t))
}

export async function transform(src: string, options?: any): Promise<any> {
  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 {
  const bindings = loadBindingsSync()
  return bindings.transformSync(src, options)
}

export function minify(
  src: string,
  options: any
): Promise<{ code: string; map: any }> {
  const bindings = getBindingsSync()
  return bindings.minify(src, options)
}

export function isReactCompilerRequired(filename: string): Promise<boolean> {
  const bindings = getBindingsSync()
  return bindings.reactCompiler.isReactCompilerRequired(filename)
}

export async function parse(src: string, options: any): Promise<any> {
  const bindings = getBindingsSync()
  const parserOptions = (
    require('./options') as typeof import('./options')
  ).getParserOptions(options)
  const parsed = await bindings.parse(src, parserOptions)
  return JSON.parse(parsed)
}

export function getBinaryMetadata() {
  return {
    target: loadedBindings?.getTargetTriple?.(),
  }
}

/**
 * Initialize trace subscriber to emit traces.
 *
 */
export function initCustomTraceSubscriber(traceFileName?: string) {
  if (!swcTraceFlushGuard) {
    // Wasm binary doesn't support trace emission
    swcTraceFlushGuard =
      getBindingsSync().initCustomTraceSubscriber?.(traceFileName)
  }
}

function once(fn: () => void): () => void {
  let executed = false

  return function (): void {
    if (!executed) {
      executed = true

      fn()
    }
  }
}

/**
 * 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 = once(() => {
  try {
    if (swcTraceFlushGuard) {
      getBindingsSync().teardownTraceSubscriber?.(swcTraceFlushGuard)
    }
  } catch (e) {
    // Suppress exceptions, this fn allows to fail to load native bindings
  }
})

export async function getModuleNamedExports(
  resourcePath: string
): Promise<string[]> {
  return getBindingsSync().rspack.getModuleNamedExports(resourcePath)
}

export async function warnForEdgeRuntime(
  source: string,
  isProduction: boolean
): Promise<NapiSourceDiagnostic[]> {
  return getBindingsSync().rspack.warnForEdgeRuntime(source, isProduction)
}
Quest for Codev2.0.0
/
SIGN IN