next.js/test/production/app-dir/actions-tree-shaking/client-actions-tree-shaking/client-actions-tree-shaking.test.ts
client-actions-tree-shaking.test.ts126 lines4.1 KB
import { join } from 'path'
import { nextTestSetup } from 'e2e-utils'
import { getClientReferenceManifest, retry } from 'next-test-utils'
import { parseRelativeUrl } from 'next/dist/shared/lib/router/utils/parse-relative-url'

function getServerReferenceIdsFromBundle(source: string): string[] {
  // Reference IDs are strings with [0-9a-f] that are at least 32 characters long.
  // We use RegExp to find them in the bundle.
  const referenceIds = source.matchAll(/"([0-9a-f]{32,})"/g) || []
  return [...referenceIds].map(([, id]) => id)
}

describe('app-dir - client-actions-tree-shaking', () => {
  const { next } = nextTestSetup({
    files: __dirname,
  })

  const logs: string[] = []

  beforeAll(() => {
    const onLog = (log: string) => {
      logs.push(log.trim())
    }
    next.on('stdout', onLog)
    next.on('stderr', onLog)
  })

  afterEach(async () => {
    logs.length = 0
  })

  /**
   * Parses the client reference manifest for a given route and returns the client chunks
   */
  function getClientChunks(route: string): Array<string> {
    const clientManifest = getClientReferenceManifest(next, route)
    const chunks = new Set<string>()
    if (process.env.IS_TURBOPACK_TEST) {
      // These only exist for turbopack and are encoded as files
      // entryJSFiles is a map of module name to a set of chunks relative to `.next`
      for (const entries of Object.values(clientManifest.entryJSFiles)) {
        for (const chunk of entries) {
          chunks.add(chunk)
        }
      }
      // clientModules is a mapping from module name to a set of URLs
      // So strip that prefix and add it to the chunks
      for (const clientModule of Object.values(clientManifest.clientModules)) {
        for (const chunk of clientModule.chunks) {
          chunks.add(parseRelativeUrl(chunk).pathname.replace('/_next/', ''))
        }
      }
    } else {
      // webpack doens't use entryJSFiles, so we need to use clientModules but the format is different.
      // chunks is a sequence of 'chunk-id', chunk-path pairs, so we need to skip the chunk-id
      for (const clientModule of Object.values(clientManifest.clientModules)) {
        for (let i = 1; i < clientModule.chunks.length; i += 2) {
          chunks.add(clientModule.chunks[i])
        }
      }
    }
    return Array.from(chunks)
  }

  it('should not bundle unused server reference id in client bundles', async () => {
    const bundle1Files = getClientChunks('/route-1/page')
    const bundle2Files = getClientChunks('/route-2/page')
    const bundle3Files = getClientChunks('/route-3/page')

    const bundle1Contents = await Promise.all(
      bundle1Files.map((file: string) =>
        next.readFile(join(next.distDir, file))
      )
    )
    const bundle2Contents = await Promise.all(
      bundle2Files.map((file: string) =>
        next.readFile(join(next.distDir, file))
      )
    )
    const bundle3Contents = await Promise.all(
      bundle3Files.map((file: string) =>
        next.readFile(join(next.distDir, file))
      )
    )

    const bundle1Ids = bundle1Contents.flatMap((file: string) =>
      getServerReferenceIdsFromBundle(file)
    )
    const bundle2Ids = bundle2Contents.flatMap((file: string) =>
      getServerReferenceIdsFromBundle(file)
    )
    const bundle3Ids = bundle3Contents.flatMap((file: string) =>
      getServerReferenceIdsFromBundle(file)
    )

    // Bundle 1 and 2 should only have one ID.
    expect(bundle1Ids).toHaveLength(1)
    expect(bundle2Ids).toHaveLength(1)
    expect(bundle1Ids[0]).not.toEqual(bundle2Ids[0])

    // Bundle 3 should have no IDs.
    expect(bundle3Ids).toHaveLength(0)
  })

  // Test the application
  it('should trigger actions correctly', async () => {
    const browser = await next.browser('/route-1')
    await browser.elementById('submit').click()

    await retry(() => {
      expect(logs).toEqual(
        expect.arrayContaining([expect.stringContaining('This is action foo')])
      )
    })

    const browser2 = await next.browser('/route-2')
    await browser2.elementById('submit').click()

    await retry(() => {
      expect(logs).toEqual(
        expect.arrayContaining([expect.stringContaining('This is action bar')])
      )
    })
  })
})
Quest for Codev2.0.0
/
SIGN IN