next.js/test/e2e/app-dir/cache-components/my-adapter.mjs
my-adapter.mjs178 lines5.4 KB
/**
 * This adapter is not modifying outputs or the test
 * it is just adding additional assertions ensuring
 * we provide the expected outputs and file paths are valid
 */
import fs from 'fs'

// @ts-check
/** @type {import('next').NextAdapter } */
const myAdapter = {
  name: 'my-custom-adapter',
  modifyConfig: (config, { phase }) => {
    if (process.env.NODE_ENV !== 'production') return config
    if (typeof phase !== 'string') {
      throw new Error(`invalid phase value provided to modifyConfig ${phase}`)
    }
    console.log('called modify config in adapter with phase', phase)
    return config
  },
  onBuildComplete: async (ctx) => {
    console.log('onBuildComplete called')

    // Validate all output file paths exist on the filesystem
    const allOutputs = [
      ...ctx.outputs.pages,
      ...ctx.outputs.pagesApi,
      ...ctx.outputs.appPages,
      ...ctx.outputs.appRoutes,
      ...ctx.outputs.prerenders,
      ...ctx.outputs.staticFiles,
    ]

    if (ctx.outputs.middleware) {
      allOutputs.push(ctx.outputs.middleware)
    }

    const validationErrors = []

    // Check that all filePaths in outputs exist
    for (const output of allOutputs) {
      if (output.filePath) {
        try {
          await fs.promises.access(output.filePath, fs.constants.F_OK)
        } catch (err) {
          validationErrors.push(
            `Missing file for output ${output.id}: ${output.filePath}`
          )
        }
      }

      // Check fallback filePath for prerenders
      if (output.type === 'PRERENDER' && output.fallback) {
        if (output.fallback.filePath) {
          try {
            await fs.promises.access(
              output.fallback.filePath,
              fs.constants.F_OK
            )
          } catch (err) {
            validationErrors.push(
              `Missing fallback file for prerender ${output.id}: ${JSON.stringify(output, null, 2)}`
            )
          }
        }
      }

      // Check assets
      if (output.assets) {
        for (const [key, assetPath] of Object.entries(output.assets)) {
          try {
            await fs.promises.access(assetPath, fs.constants.F_OK)
          } catch (err) {
            validationErrors.push(
              `Missing asset file for output ${output.id} (${key}): ${assetPath}`
            )
          }
        }
      }

      // Check wasmAssets
      if (output.wasmAssets) {
        for (const [key, wasmPath] of Object.entries(output.wasmAssets)) {
          try {
            await fs.promises.access(wasmPath, fs.constants.F_OK)
          } catch (err) {
            validationErrors.push(
              `Missing wasm file for output ${output.id} (${key}): ${wasmPath}`
            )
          }
        }
      }
    }

    // Validate that segment routes are present in routing.dynamicRoutes
    // Segment routes match the pattern: .segments/.+.segment.rsc
    const segmentRoutes = ctx.routing.dynamicRoutes.filter((route) => {
      // Check if the source or destination contains segment routes
      return (
        route.sourceRegex.includes('.segments/') ||
        route.sourceRegex.includes('.segment.rsc')
      )
    })

    // Ensure we have segment routes when we have app pages
    if (ctx.outputs.appPages.length > 0) {
      if (segmentRoutes.length === 0) {
        validationErrors.push(
          'Expected segment routes in routing.dynamicRoutes when app pages exist'
        )
      } else {
        console.log(
          `Found ${segmentRoutes.length} segment routes in routing.dynamicRoutes`
        )
      }
    }

    // Validate that all appPages have matching .rsc and non .rsc pathnames
    const appPagePathnames = new Map()
    for (const appPage of ctx.outputs.appPages) {
      const pathname = appPage.pathname
      if (pathname.endsWith('.rsc')) {
        const basePathname = pathname.slice(0, -4) // Remove .rsc extension
        if (!appPagePathnames.has(basePathname)) {
          appPagePathnames.set(basePathname, { rsc: false, nonRsc: false })
        }
        appPagePathnames.get(basePathname).rsc = true
      } else {
        if (!appPagePathnames.has(pathname)) {
          appPagePathnames.set(pathname, { rsc: false, nonRsc: false })
        }
        appPagePathnames.get(pathname).nonRsc = true
      }
    }

    // Check that each pathname has both .rsc and non .rsc versions
    for (const [pathname, versions] of appPagePathnames.entries()) {
      if (!versions.rsc) {
        validationErrors.push(
          `App page ${pathname} is missing corresponding .rsc pathname`
        )
      }
      if (!versions.nonRsc) {
        validationErrors.push(
          `App page ${pathname}.rsc is missing corresponding non .rsc pathname`
        )
      }
    }

    if (appPagePathnames.size > 0) {
      console.log(
        `Validated ${appPagePathnames.size} app page pathname(s) have matching .rsc and non .rsc versions`
      )
    }

    if (validationErrors.length > 0) {
      console.error('Validation errors:')
      for (const error of validationErrors) {
        console.error(`  - ${error}`)
      }
      throw new Error(
        `Adapter validation failed with ${validationErrors.length} error(s)`
      )
    }

    console.log('Validation passed: All output files exist on filesystem')
    console.log(
      `Segment routes validated: ${segmentRoutes.length} routes found`
    )

    await fs.promises.writeFile(
      'build-complete.json',
      JSON.stringify(ctx, null, 2)
    )
  },
}

export default myAdapter
Quest for Codev2.0.0
/
SIGN IN