next.js/packages/next/src/build/webpack/loaders/next-root-params-loader.ts
next-root-params-loader.ts167 lines4.9 KB
import type { webpack } from 'next/dist/compiled/webpack/webpack'
import * as path from 'node:path'
import * as fs from 'node:fs/promises'
import { normalizeAppPath } from '../../../shared/lib/router/utils/app-paths'
import { ensureLeadingSlash } from '../../../shared/lib/page-path/ensure-leading-slash'
import { getSegmentParam } from '../../../shared/lib/router/utils/get-segment-param'

export type RootParamsLoaderOpts = {
  appDir: string
  pageExtensions: string[]
}

type CollectedRootParams = Set<string>

const rootParamsLoader: webpack.LoaderDefinitionFunction<RootParamsLoaderOpts> =
  async function () {
    const { appDir, pageExtensions } = this.getOptions()

    const allRootParams = await collectRootParamsFromFileSystem({
      appDir,
      pageExtensions,
    })
    // invalidate the result whenever a file/directory is added/removed inside the app dir or its subdirectories,
    // because that might mean that a root layout has been moved.
    this.addContextDependency(appDir)

    // If there's no root params, there's nothing to generate.
    if (allRootParams.size === 0) {
      return 'export {}'
    }

    // Generate a getter for each root param we found.
    const sortedRootParamNames = Array.from(allRootParams).sort()
    const content = [
      `import { getRootParam } from 'next/dist/server/request/root-params';`,
      ...sortedRootParamNames.map((paramName) => {
        return `export function ${paramName}() { return getRootParam('${paramName}'); }`
      }),
    ].join('\n')

    return content
  }

export default rootParamsLoader

async function collectRootParamsFromFileSystem(
  opts: Parameters<typeof findRootLayouts>[0]
) {
  return collectRootParams({
    appDir: opts.appDir,
    rootLayoutFilePaths: await findRootLayouts(opts),
  })
}

function collectRootParams({
  rootLayoutFilePaths,
  appDir,
}: {
  rootLayoutFilePaths: string[]
  appDir: string
}): CollectedRootParams {
  const allRootParams: CollectedRootParams = new Set()

  for (const rootLayoutFilePath of rootLayoutFilePaths) {
    const params = getParamsFromLayoutFilePath({
      appDir,
      layoutFilePath: rootLayoutFilePath,
    })
    for (const param of params) {
      allRootParams.add(param)
    }
  }

  return allRootParams
}

async function findRootLayouts({
  appDir,
  pageExtensions,
}: {
  appDir: string
  pageExtensions: string[]
}) {
  const layoutFilenameRegex = new RegExp(
    `^layout\\.(?:${pageExtensions.join('|')})$`
  )

  async function visit(directory: string): Promise<string[]> {
    let dir: Awaited<ReturnType<(typeof fs)['readdir']>>
    try {
      dir = await fs.readdir(directory, { withFileTypes: true })
    } catch (err) {
      // If the directory was removed before we managed to read it, just ignore it.
      if (
        err &&
        typeof err === 'object' &&
        'code' in err &&
        err.code === 'ENOENT'
      ) {
        return []
      }

      throw err
    }

    const subdirectories: string[] = []
    for (const entry of dir) {
      if (entry.isDirectory()) {
        // Directories that start with an underscore are excluded from routing, so we shouldn't look for layouts inside.
        if (entry.name[0] === '_') {
          continue
        }
        // Parallel routes cannot occur above a layout, so they can't contain a root layout.
        if (entry.name[0] === '@') {
          continue
        }

        const absolutePathname = path.join(directory, entry.name)
        subdirectories.push(absolutePathname)
      } else if (entry.isFile()) {
        if (layoutFilenameRegex.test(entry.name)) {
          // We found a root layout, so we're not going to recurse into subdirectories,
          // meaning that we can skip the rest of the entries.
          // Note that we don't need to track any of the subdirectories as dependencies --
          // changes in the subdirectories will only become relevant if this root layout is (re)moved,
          // in which case the loader will re-run, traverse deeper (because it no longer stops at this root layout)
          // and then track those directories as needed.
          const rootLayoutPath = path.join(directory, entry.name)
          return [rootLayoutPath]
        }
      }
    }

    if (subdirectories.length === 0) {
      return []
    }

    const subdirectoryRootLayouts = await Promise.all(
      subdirectories.map((subdirectory) => visit(subdirectory))
    )
    return subdirectoryRootLayouts.flat(1)
  }

  return visit(appDir)
}

function getParamsFromLayoutFilePath({
  appDir,
  layoutFilePath,
}: {
  appDir: string
  layoutFilePath: string
}): string[] {
  const rootLayoutPath = normalizeAppPath(
    ensureLeadingSlash(path.dirname(path.relative(appDir, layoutFilePath)))
  )
  const segments = rootLayoutPath.split('/')
  const paramNames: string[] = []
  for (const segment of segments) {
    const param = getSegmentParam(segment)
    if (param !== null) {
      paramNames.push(param.paramName)
    }
  }
  return paramNames
}
Quest for Codev2.0.0
/
SIGN IN