next.js/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts
server-source-maps.test.ts965 lines41.8 KB
import * as path from 'path'
import { nextTestSetup } from 'e2e-utils'
import stripAnsi from 'strip-ansi'
import { retry } from 'next-test-utils'

function normalizeCliOutput(output: string) {
  return (
    stripAnsi(output)
      // TODO(veil): Should not appear in sourcemapped stackframes.
      .replaceAll('webpack:///', 'bundler:///')
      .replaceAll(/at [a-zA-Z] \(/g, 'at <mangled> (')
  )
}

describe('app-dir - server source maps', () => {
  const dependencies = {
    // `link:` simulates a package in a monorepo
    'internal-pkg': `link:./internal-pkg`,
    'external-pkg': `file:./external-pkg`,
  }
  const { skipped, next, isNextDev, isTurbopack, isRspack } = nextTestSetup({
    dependencies,
    files: path.join(__dirname, 'fixtures/default'),
    // Deploy tests don't have access to runtime logs.
    // Manually verify that the runtime logs match.
    skipDeployment: true,
  })

  if (skipped) return

  it('logged errors have a sourcemapped stack with a codeframe', async () => {
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      await next.render('/rsc-error-log')

      await retry(() => {
        expect(next.cliOutput.slice(outputIndex)).toContain(
          'Error: rsc-error-log'
        )
      })
      expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
        'Error: rsc-error-log' +
          '\n    at logError (app/rsc-error-log/page.js:4:17)' +
          '\n    at Page (app/rsc-error-log/page.js:9:3)' +
          '\n  2 |' +
          '\n  3 | function logError() {' +
          "\n> 4 |   const error = new Error('rsc-error-log')" +
          '\n    |                 ^' +
          '\n  5 |   console.error(error)' +
          '\n  6 | }' +
          '\n  7 |' +
          '\n'
      )
    } else {
      if (isTurbopack) {
        // TODO(veil): Sourcemap names
        // TODO(veil): relative paths in webpack
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '(app/rsc-error-log/page.js:4:17)'
        )
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '' +
            "\n> 4 |   const error = new Error('rsc-error-log')" +
            '\n    |                 ^'
        )
      } else {
        // TODO(veil): line/column numbers are flaky in Webpack
      }
    }
  })

  it('logged errors have a sourcemapped `cause`', async () => {
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      await next.render('/rsc-error-log-cause')

      await retry(() => {
        expect(next.cliOutput.slice(outputIndex)).toContain(
          'Error: rsc-error-log-cause'
        )
      })
      expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
        'Error: rsc-error-log-cause' +
          '\n    at logError (app/rsc-error-log-cause/page.js:2:17)' +
          '\n    at Page (app/rsc-error-log-cause/page.js:8:3)' +
          '\n  1 | function logError(cause) {' +
          "\n> 2 |   const error = new Error('rsc-error-log-cause', { cause })" +
          '\n    |                 ^' +
          '\n  3 |   console.error(error)' +
          '\n  4 | }' +
          '\n  5 | {' +
          '\n  [cause]: Error: Boom' +
          '\n      at Page (app/rsc-error-log-cause/page.js:7:17)' +
          '\n     5 |' +
          '\n     6 | export default function Page() {' +
          "\n  >  7 |   const error = new Error('Boom')" +
          '\n       |                 ^' +
          '\n     8 |   logError(error)' +
          '\n     9 |   return null' +
          '\n    10 | }' +
          '\n'
      )
    } else {
      if (isTurbopack) {
        // TODO(veil): Sourcemap names
        // TODO(veil): relative paths
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '(app/rsc-error-log-cause/page.js:2:17)'
        )
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '(app/rsc-error-log-cause/page.js:7:17)'
        )
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '' +
            "\n> 2 |   const error = new Error('rsc-error-log-cause', { cause })" +
            '\n    |                 ^'
        )
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '' +
            "\n  >  7 |   const error = new Error('Boom')" +
            '\n       |                 ^'
        )
      } else {
        // TODO(veil): line/column numbers are flaky in Webpack
      }
    }
  })

  it('logged errors collapse deeply nested causes at depth 2', async () => {
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      await next.render('/rsc-error-log-nested')

      await retry(() => {
        expect(next.cliOutput.slice(outputIndex)).toContain(
          'Error: rsc-error-log-nested'
        )
      })
      expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
        'Error: rsc-error-log-nested' +
          '\n    at logError (app/rsc-error-log-nested/page.js:6:18)' +
          '\n    at Page (app/rsc-error-log-nested/page.js:11:3)' +
          "\n  4 |   const depth2 = new Error('Depth 2 error', { cause: depth3 })" +
          "\n  5 |   const depth1 = new Error('Depth 1 error', { cause: depth2 })" +
          "\n> 6 |   const depth0 = new Error('rsc-error-log-nested', { cause: depth1 })" +
          '\n    |                  ^' +
          '\n  7 |   console.error(depth0)' +
          '\n  8 | }' +
          '\n  9 | {' +
          '\n  [cause]: Error: Depth 1 error' +
          '\n      at logError (app/rsc-error-log-nested/page.js:5:18)' +
          '\n      at Page (app/rsc-error-log-nested/page.js:11:3)' +
          "\n    3 |   const depth3 = new Error('Depth 3 error', { cause: depth4 })" +
          "\n    4 |   const depth2 = new Error('Depth 2 error', { cause: depth3 })" +
          "\n  > 5 |   const depth1 = new Error('Depth 1 error', { cause: depth2 })" +
          '\n      |                  ^' +
          "\n    6 |   const depth0 = new Error('rsc-error-log-nested', { cause: depth1 })" +
          '\n    7 |   console.error(depth0)' +
          '\n    8 | } {' +
          '\n    [cause]: Error: Depth 2 error' +
          '\n        at logError (app/rsc-error-log-nested/page.js:4:18)' +
          '\n        at Page (app/rsc-error-log-nested/page.js:11:3)' +
          "\n      2 |   const depth4 = new Error('Depth 4 error')" +
          "\n      3 |   const depth3 = new Error('Depth 3 error', { cause: depth4 })" +
          "\n    > 4 |   const depth2 = new Error('Depth 2 error', { cause: depth3 })" +
          '\n        |                  ^' +
          "\n      5 |   const depth1 = new Error('Depth 1 error', { cause: depth2 })" +
          "\n      6 |   const depth0 = new Error('rsc-error-log-nested', { cause: depth1 })" +
          '\n      7 |   console.error(depth0) {' +
          '\n      [cause]: [Error]' +
          '\n    }' +
          '\n  }' +
          '\n}'
      )
      // Verify depth 3+ are NOT shown (truncated to [Error])
      expect(
        normalizeCliOutput(next.cliOutput.slice(outputIndex))
      ).not.toContain('Error: Depth 3 error')
      expect(
        normalizeCliOutput(next.cliOutput.slice(outputIndex))
      ).not.toContain('Error: Depth 4 error')
    } else {
      if (isTurbopack) {
        // TODO(veil): Sourcemap names
        // TODO(veil): relative paths in production
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '(app/rsc-error-log-nested/page.js:6:18)'
        )
        expect(normalizeCliOutput(next.cliOutput)).toContain('[cause]: [Error]')
        expect(normalizeCliOutput(next.cliOutput)).not.toContain(
          'Error: Depth 3 error'
        )
        expect(normalizeCliOutput(next.cliOutput)).not.toContain(
          'Error: Depth 4 error'
        )
      } else {
        // TODO(veil): line/column numbers are flaky in Webpack
      }
    }
  })

  it('logged errors include `[errors]` for AggregateError', async () => {
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      await next.render('/rsc-error-log-aggregate')

      await retry(() => {
        expect(next.cliOutput.slice(outputIndex)).toContain(
          'AggregateError: rsc-error-log-aggregate'
        )
      })
      expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
        'AggregateError: rsc-error-log-aggregate' +
          '\n    at logError (app/rsc-error-log-aggregate/page.js:6:26)' +
          '\n    at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
          "\n  4 |   const error2 = new TypeError('Error 2')" +
          "\n  5 |   const rootError = new Error('Root error')" +
          '\n> 6 |   const aggregateError = new AggregateError(' +
          '\n    |                          ^' +
          '\n  7 |     [error1, error2],' +
          "\n  8 |     'rsc-error-log-aggregate'," +
          '\n  9 |     { cause: rootError } {' +
          '\n  [cause]: Error: Root error' +
          '\n      at logError (app/rsc-error-log-aggregate/page.js:5:21)' +
          '\n      at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
          "\n    3 |   const error1 = new Error('Error 1')" +
          "\n    4 |   const error2 = new TypeError('Error 2')" +
          "\n  > 5 |   const rootError = new Error('Root error')" +
          '\n      |                     ^' +
          '\n    6 |   const aggregateError = new AggregateError(' +
          '\n    7 |     [error1, error2],' +
          "\n    8 |     'rsc-error-log-aggregate',," +
          '\n  [errors]: [' +
          '\n    Error: Error 1' +
          '\n        at logError (app/rsc-error-log-aggregate/page.js:3:18)' +
          '\n        at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
          '\n      1 | /* global AggregateError */' +
          '\n      2 | function logError() {' +
          "\n    > 3 |   const error1 = new Error('Error 1')" +
          '\n        |                  ^' +
          "\n      4 |   const error2 = new TypeError('Error 2')" +
          "\n      5 |   const rootError = new Error('Root error')" +
          '\n      6 |   const aggregateError = new AggregateError(,' +
          '\n    TypeError: Error 2' +
          '\n        at logError (app/rsc-error-log-aggregate/page.js:4:18)' +
          '\n        at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
          '\n      2 | function logError() {' +
          "\n      3 |   const error1 = new Error('Error 1')" +
          "\n    > 4 |   const error2 = new TypeError('Error 2')" +
          '\n        |                  ^' +
          "\n      5 |   const rootError = new Error('Root error')" +
          '\n      6 |   const aggregateError = new AggregateError(' +
          '\n      7 |     [error1, error2],' +
          '\n  ]' +
          '\n}'
      )
    } else {
      if (isTurbopack) {
        // TODO(veil): Sourcemap names
        // TODO(veil): relative paths in production
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '(app/rsc-error-log-aggregate/page.js:6:26)'
        )
        expect(normalizeCliOutput(next.cliOutput)).toContain('[errors]:')
        expect(normalizeCliOutput(next.cliOutput)).toContain('[cause]:')
      } else {
        // TODO(veil): line/column numbers are flaky in Webpack
      }
    }
  })

  it('logged errors in client components during ssr have a sourcemapped stack with a codeframe', async () => {
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      await next.render('/ssr-error-log')

      await retry(() => {
        expect(next.cliOutput.slice(outputIndex)).toContain(
          'Error: ssr-error-log'
        )
      })
      expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
        'Error: ssr-error-log' +
          '\n    at logError (app/ssr-error-log/page.js:4:17)' +
          '\n    at Page (app/ssr-error-log/page.js:9:3)' +
          '\n  2 |' +
          '\n  3 | function logError() {' +
          "\n> 4 |   const error = new Error('ssr-error-log')" +
          '\n    |                 ^' +
          '\n  5 |   console.error(error)' +
          '\n  6 | }' +
          '\n  7 |' +
          '\n'
      )
    } else {
      if (isTurbopack) {
        // TODO(veil): Sourcemap names
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          'Error: ssr-error-log' +
            '\n    at <unknown> (app/ssr-error-log/page.js:4:17)' +
            '\n  2 |' +
            '\n  3 | function logError() {' +
            "\n> 4 |   const error = new Error('ssr-error-log')" +
            '\n    |                 ^' +
            '\n  5 |   console.error(error)' +
            '\n  6 | }' +
            '\n  7 |' +
            '\n'
        )
      } else {
        // TODO(veil): line/column numbers are flaky in Webpack
      }
    }
  })

  it('stack frames are ignore-listed in ssr', async () => {
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      const browser = await next.browser('/ssr-error-log-ignore-listed')

      await retry(() => {
        expect(next.cliOutput.slice(outputIndex)).toContain(
          'Error: ssr-error-log-ignore-listed'
        )
      })
      expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
        isTurbopack
          ? 'Error: ssr-error-log-ignore-listed' +
              '\n    at logError (app/ssr-error-log-ignore-listed/page.js:9:17)' +
              '\n    at runWithInternalIgnored (app/ssr-error-log-ignore-listed/page.js:19:13)' +
              '\n    at runWithExternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:18:29)' +
              '\n    at runWithExternal (app/ssr-error-log-ignore-listed/page.js:17:32)' +
              '\n    at runWithInternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:16:18)' +
              // Realpath does not point into node_modules so we don't ignore it.
              '\n    at runInternalSourceMapped (internal-pkg/sourcemapped.ts:5:10)' +
              '\n    at runWithInternal (app/ssr-error-log-ignore-listed/page.js:15:28)' +
              // Realpath does not point into node_modules so we don't ignore it.
              '\n    at runInternal (internal-pkg/index.js:2:10)' +
              '\n    at Page (app/ssr-error-log-ignore-listed/page.js:14:14)' +
              '\n   7 |' +
              '\n'
          : 'Error: ssr-error-log-ignore-listed' +
              '\n    at logError (app/ssr-error-log-ignore-listed/page.js:9:17)' +
              '\n    at runWithInternalIgnored (app/ssr-error-log-ignore-listed/page.js:19:13)' +
              // TODO(veil-NDX-910): Webpacks's sourcemap loader drops `ignoreList`
              // TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
              // Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
              '\n    at runInternalIgnored (webpack-internal:/(ssr)/internal-pkg/ignored.ts:6:10)' +
              '\n    at runWithExternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:18:29)' +
              '\n    at runWithExternal (app/ssr-error-log-ignore-listed/page.js:17:32)' +
              '\n    at runWithInternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:16:18)' +
              // TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
              // Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
              // Realpath does not point into node_modules so we don't ignore it.
              '\n    at runInternalSourceMapped (webpack-internal:/(ssr)/internal-pkg/sourcemapped.ts:5:10)' +
              '\n    at runWithInternal (app/ssr-error-log-ignore-listed/page.js:15:28)' +
              // Realpath does not point into node_modules so we don't ignore it.
              '\n    at runInternal (internal-pkg/index.js:2:10)' +
              '\n    at Page (app/ssr-error-log-ignore-listed/page.js:14:14)' +
              '\n   7 |' +
              '\n'
      )
      if (isTurbopack) {
        // TODO(veil): Turbopack errors because it thinks the sources are not part of the project.
        await expect(browser).toDisplayCollapsedRedbox(`
         {
           "description": "ssr-error-log-ignore-listed",
           "environmentLabel": null,
           "label": "Console Error",
           "source": "app/ssr-error-log-ignore-listed/page.js (9:17) @ logError
         >  9 |   const error = new Error('ssr-error-log-ignore-listed')
              |                 ^",
           "stack": [
             "logError app/ssr-error-log-ignore-listed/page.js (9:17)",
             "runWithInternalIgnored app/ssr-error-log-ignore-listed/page.js (19:13)",
             "runWithExternalSourceMapped app/ssr-error-log-ignore-listed/page.js (18:29)",
             "runWithExternal app/ssr-error-log-ignore-listed/page.js (17:32)",
             "runWithInternalSourceMapped app/ssr-error-log-ignore-listed/page.js (16:18)",
             "runInternalSourceMapped internal-pkg/sourcemapped.ts (5:10)",
             "runWithInternal app/ssr-error-log-ignore-listed/page.js (15:28)",
             "runInternal internal-pkg/index.js (2:10)",
             "Page app/ssr-error-log-ignore-listed/page.js (14:14)",
           ],
         }
        `)
      } else {
        // TODO(veil-NDX-910): Webpacks's sourcemap loader drops `ignoreList`
        // TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
        await expect(browser).toDisplayCollapsedRedbox(`
         {
           "description": "ssr-error-log-ignore-listed",
           "environmentLabel": null,
           "label": "Console Error",
           "source": "app/ssr-error-log-ignore-listed/page.js (9:17) @ logError
         >  9 |   const error = new Error('ssr-error-log-ignore-listed')
              |                 ^",
           "stack": [
             "logError app/ssr-error-log-ignore-listed/page.js (9:17)",
             "runWithInternalIgnored app/ssr-error-log-ignore-listed/page.js (19:13)",
             "runInternalIgnored ignored.ts (6:10)",
             "runWithExternalSourceMapped app/ssr-error-log-ignore-listed/page.js (18:29)",
             "runWithExternal app/ssr-error-log-ignore-listed/page.js (17:32)",
             "runWithInternalSourceMapped app/ssr-error-log-ignore-listed/page.js (16:18)",
             "runInternalSourceMapped sourcemapped.ts (5:10)",
             "runWithInternal app/ssr-error-log-ignore-listed/page.js (15:28)",
             "runInternal internal-pkg/index.js (2:10)",
             "Page app/ssr-error-log-ignore-listed/page.js (14:14)",
           ],
         }
        `)
      }
    } else {
      if (isTurbopack) {
        // TODO(veil): Sourcemap names
        // TODO(veil): relative paths
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '(app/ssr-error-log-ignore-listed/page.js:9:17)'
        )
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '\n' +
            ">  9 |   const error = new Error('ssr-error-log-ignore-listed')\n" +
            '     |                 ^\n'
        )
      } else {
        // TODO(veil): line/column numbers are flaky in Webpack
      }
    }
  })

  it('stack frames are ignore-listed in rsc', async () => {
    const outputIndex = next.cliOutput.length
    await next.render('/rsc-error-log-ignore-listed')

    if (isNextDev) {
      await retry(() => {
        expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
          'Error: rsc-error-log-ignore-listed'
        )
      })
      expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
        isTurbopack
          ? 'Error: rsc-error-log-ignore-listed' +
              '\n    at logError (app/rsc-error-log-ignore-listed/page.js:8:17)' +
              '\n    at runWithInternalIgnored (app/rsc-error-log-ignore-listed/page.js:18:13)' +
              '\n    at runWithExternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:17:29)' +
              '\n    at runWithExternal (app/rsc-error-log-ignore-listed/page.js:16:32)' +
              '\n    at runWithInternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:15:18)' +
              // Realpath does not point into node_modules so we don't ignore it.
              '\n    at runInternalSourceMapped (internal-pkg/sourcemapped.ts:5:10)' +
              '\n    at runWithInternal (app/rsc-error-log-ignore-listed/page.js:14:28)' +
              // Realpath does not point into node_modules so we don't ignore it.
              '\n    at runInternal (internal-pkg/index.js:2:10)' +
              '\n    at Page (app/rsc-error-log-ignore-listed/page.js:13:14)' +
              '\n   6 |' +
              '\n'
          : 'Error: rsc-error-log-ignore-listed' +
              '\n    at logError (app/rsc-error-log-ignore-listed/page.js:8:17)' +
              '\n    at runWithInternalIgnored (app/rsc-error-log-ignore-listed/page.js:18:13)' +
              // TODO(veil): Webpacks's sourcemap loader drops `ignoreList`
              // TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
              // Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
              '\n    at runInternalIgnored (webpack-internal:/(rsc)/internal-pkg/ignored.ts:6:10)' +
              '\n    at runWithExternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:17:29)' +
              '\n    at runWithExternal (app/rsc-error-log-ignore-listed/page.js:16:32)' +
              '\n    at runWithInternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:15:18)' +
              // TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
              // Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
              // Realpath does not point into node_modules so we don't ignore it.
              '\n    at runInternalSourceMapped (webpack-internal:/(rsc)/internal-pkg/sourcemapped.ts:5:10)' +
              '\n    at runWithInternal (app/rsc-error-log-ignore-listed/page.js:14:28)' +
              // Realpath does not point into node_modules so we don't ignore it.
              '\n    at runInternal (internal-pkg/index.js:2:10)' +
              '\n    at Page (app/rsc-error-log-ignore-listed/page.js:13:14)' +
              '\n   6 |' +
              '\n'
      )
    } else {
      if (isTurbopack) {
        // TODO(veil): Sourcemap names
        // TODO(veil): relative paths
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          'at <unknown> (app/rsc-error-log-ignore-listed/page.js:8:17)'
        )
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '' +
            "\n>  8 |   const error = new Error('rsc-error-log-ignore-listed')" +
            '\n     |                 ^'
        )
      } else {
        // TODO(veil): line/column numbers are flaky in Webpack
      }
    }
  })

  it('thrown SSR errors', async () => {
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      const browser = await next.browser('/ssr-throw')

      await retry(() => {
        expect(next.cliOutput.slice(outputIndex)).toContain('Error: ssr-throw')
      })

      const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
      expect(cliOutput).toContain(
        '⨯ Error: ssr-throw' +
          '\n    at throwError (app/ssr-throw/Thrower.js:4:9)' +
          '\n    at Thrower (app/ssr-throw/Thrower.js:8:3)' +
          '\n  2 |' +
          '\n  3 | function throwError() {' +
          "\n> 4 |   throw new Error('ssr-throw')" +
          '\n    |         ^' +
          '\n  5 | }' +
          '\n  6 |' +
          '\n  7 | export function Thrower() { {' +
          "\n  digest: '"
      )
      expect(cliOutput).toMatch(/digest: '\d+'/)

      await expect(browser).toDisplayRedbox(`
       {
         "description": "ssr-throw",
         "environmentLabel": null,
         "label": "Runtime Error",
         "source": "app/ssr-throw/Thrower.js (4:9) @ throwError
       > 4 |   throw new Error('ssr-throw')
           |         ^",
         "stack": [
           "throwError app/ssr-throw/Thrower.js (4:9)",
           "Thrower app/ssr-throw/Thrower.js (8:3)",
           "Page app/ssr-throw/page.js (6:10)",
         ],
       }
      `)
    } else {
      // SSR errors are not logged because React retries them during hydration.
    }
  })

  it('logged errors preserve their name', async () => {
    let cliOutput = next.cliOutput
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      await next.render('/rsc-error-log-custom-name')
      cliOutput = next.cliOutput.slice(outputIndex)
    }

    await retry(() => {
      expect(cliOutput).toContain(
        // TODO: isNextDev ? 'UnnamedError: rsc-error-log-custom-name-Foo' : '[Error]: rsc-error-log-custom-name-Foo'
        isNextDev
          ? 'Error: rsc-error-log-custom-name-Foo'
          : 'Error: rsc-error-log-custom-name-Foo'
      )
    })

    expect(cliOutput).toContain(
      // TODO: isNextDev ? 'NamedError [MyError]: rsc-error-log-custom-name-Bar' : '[MyError]: rsc-error-log-custom-name-Bar'
      isNextDev
        ? 'Error [MyError]: rsc-error-log-custom-name-Bar'
        : 'Error [MyError]: rsc-error-log-custom-name-Bar'
    )
  })

  it('handles invalid sourcemaps gracefully', async () => {
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      await next.render('/bad-sourcemap')

      await retry(() => {
        expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
          'Error: bad-sourcemap'
        )
      })
      if (isTurbopack) {
        expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
          // Node.js is fine with invalid URLs in index maps apparently.
          '' +
            'Error: bad-sourcemap' +
            '\n    at logError (app/bad-sourcemap/custom:/[badhost]/app/bad-sourcemap/page.js:6:17)' +
            '\n    at Page (app/bad-sourcemap/custom:/[badhost]/app/bad-sourcemap/page.js:10:3)' +
            '\n'
        )
      } else {
        expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
          // Node.js is not fine with invalid URLs in vanilla source maps.
          // Feel free to adjust these locations. They're just here to showcase
          // sourcemapping is broken on invalid sources.
          '' +
            `\nwebpack-internal:///(rsc)/./app/bad-sourcemap/page.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: TypeError [ERR_INVALID_ARG_TYPE]: The "payload" argument must be of type object. Received null` +
            '\nError: bad-sourcemap' +
            '\n    at logError (webpack-internal:///(rsc)/./app/bad-sourcemap/page.js:12:19)' +
            '\n    at Page (webpack-internal:///(rsc)/./app/bad-sourcemap/page.js:15:5)'
        )
        // Expect the invalid sourcemap warning only once per render.
        // Dynamic I/O renders three times.
        // One from filterStackFrameDEV.
        // One from findSourceMapURLDEV.
        expect(
          normalizeCliOutput(next.cliOutput.slice(outputIndex)).split(
            'Invalid source map.'
          ).length - 1
        ).toEqual(3)
      }
    } else {
      // Bundlers silently drop invalid sourcemaps.
      expect(
        normalizeCliOutput(next.cliOutput).split('Invalid source map.').length -
          1
      ).toEqual(0)
    }
  })

  it('sourcemaps errors during module evaluation', async () => {
    const outputIndex = next.cliOutput.length
    const browser = await next.browser('/module-evaluation')

    if (isNextDev) {
      await retry(() => {
        expect(next.cliOutput.slice(outputIndex)).toContain(
          'Error: module-evaluation'
        )
      })
      const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
      if (isTurbopack) {
        expect(cliOutput).toContain(
          'Error: module-evaluation' +
            // TODO(veil): Should map to no name like you'd get with native stacks without a bundler.
            '\n    at module evaluation (app/module-evaluation/module.js:1:22)' +
            // TODO(veil): Added frames from bundler should be sourcemapped (https://linear.app/vercel/issue/NDX-509/)
            '\n    at module evaluation (app/module-evaluation/page.js:1:1)' +
            '\n    at module evaluation (.next'
        )
      } else {
        expect(cliOutput).toContain(
          'Error: module-evaluation' +
            // TODO(veil): Should map to no name like you'd get with native stacks without a bundler.
            // TODO(veil): Location should be sourcemapped
            '\n    at eval (app/module-evaluation/module.js:1:22)' +
            // TODO(veil): Added frames from bundler should be sourcemapped (https://linear.app/vercel/issue/NDX-509/)
            '\n    at <unknown> (rsc)/.'
        )
      }

      expect(cliOutput).toContain(
        '' +
          "\n> 1 | export const error = new Error('module-evaluation')" +
          '\n    |                      ^'
      )

      if (isTurbopack) {
        await expect(browser).toDisplayCollapsedRedbox(`
         {
           "description": "module-evaluation",
           "environmentLabel": "Prerender",
           "label": "Console Error",
           "source": "app/module-evaluation/module.js (1:22) @ module evaluation
         > 1 | export const error = new Error('module-evaluation')
             |                      ^",
           "stack": [
             "module evaluation app/module-evaluation/module.js (1:22)",
             "module evaluation app/module-evaluation/page.js (1:1)",
             "module evaluation app/module-evaluation/page.js (6:1)",
             "Page <anonymous>",
           ],
         }
        `)
      } else if (isRspack) {
        await expect(browser).toDisplayCollapsedRedbox(`
         {
           "description": "module-evaluation",
           "environmentLabel": "Prerender",
           "label": "Console Error",
           "source": "app/module-evaluation/module.js (1:22) @ eval
         > 1 | export const error = new Error('module-evaluation')
             |                      ^",
           "stack": [
             "eval app/module-evaluation/module.js (1:22)",
             "<FIXME-file-protocol>",
             "<FIXME-file-protocol>",
             "eval about:/Prerender/webpack-internal:///(rsc)/app/module-evaluation/page.js (5:60)",
             "<FIXME-file-protocol>",
             "<FIXME-file-protocol>",
             "Function.all <anonymous>",
             "Function.all <anonymous>",
             "Page <anonymous>",
           ],
         }
        `)
      } else {
        await expect(browser).toDisplayCollapsedRedbox(`
         {
           "description": "module-evaluation",
           "environmentLabel": "Prerender",
           "label": "Console Error",
           "source": "app/module-evaluation/module.js (1:22) @ eval
         > 1 | export const error = new Error('module-evaluation')
             |                      ^",
           "stack": [
             "eval app/module-evaluation/module.js (1:22)",
             "<FIXME-file-protocol>",
             "eval about:/Prerender/webpack-internal:///(rsc)/app/module-evaluation/page.js (5:65)",
             "<FIXME-file-protocol>",
             "Page <anonymous>",
           ],
         }
        `)
      }
    } else {
      if (isTurbopack) {
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '' +
            '\nError: module-evaluation' +
            // TODO(veil): Turbopack internals. Feel free to update. Tracked in https://linear.app/vercel/issue/NEXT-4362
            '\n    at module evaluation (app/module-evaluation/module.js:1:22)'
        )
        expect(normalizeCliOutput(next.cliOutput)).toContain(
          '' +
            "\n> 1 | export const error = new Error('module-evaluation')" +
            '\n    |                      ^'
        )
      } else {
        expect(
          normalizeCliOutput(next.cliOutput).replaceAll(
            /at \d+ /g,
            'at <WebpackModuleID> '
          )
        ).toContain(
          '' +
            '\nError: module-evaluation' +
            // TODO(veil): column numbers are flaky in Webpack
            '\n    at <WebpackModuleID> (bundler:///app/module-evaluation/module.js:1:'
        )
      }
    }
  })

  it('ignore-lists anonymous rsc stack frame sandwiches', async () => {
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      const browser = await next.browser('/rsc-anonymous-stack-frame-sandwich')

      // TODO(veil): Implement sandwich heuristic in `filterStackFrameDEV`
      if (isTurbopack) {
        await expect(browser).toDisplayCollapsedRedbox(`
         [
           {
             "description": "rsc-anonymous-stack-frame-sandwich: external",
             "environmentLabel": "Prerender",
             "label": "Console Error",
             "source": "app/rsc-anonymous-stack-frame-sandwich/page.js (5:29) @ Page
         > 5 |   runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')
             |                             ^",
             "stack": [
               "Set.forEach <anonymous>",
               "Set.forEach <anonymous>",
               "Page app/rsc-anonymous-stack-frame-sandwich/page.js (5:29)",
               "Page <anonymous>",
             ],
           },
           {
             "description": "rsc-anonymous-stack-frame-sandwich: internal",
             "environmentLabel": "Prerender",
             "label": "Console Error",
             "source": "app/rsc-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
         > 6 |   runHiddenSetOfSetsInternal('rsc-anonymous-stack-frame-sandwich: internal')
             |                             ^",
             "stack": [
               "Set.forEach <anonymous>",
               "Set.forEach <anonymous>",
               "Page app/rsc-anonymous-stack-frame-sandwich/page.js (6:29)",
               "Page <anonymous>",
             ],
           },
         ]
        `)
      } else if (isRspack) {
        // 2nd error from runHiddenSetOfSetsInternal hits https://linear.app/vercel/issue/NEXT-4412
        await expect(browser).toDisplayCollapsedRedbox(`
         [
           {
             "description": "rsc-anonymous-stack-frame-sandwich: external",
             "environmentLabel": "Prerender",
             "label": "Console Error",
             "source": "app/rsc-anonymous-stack-frame-sandwich/page.js (5:29) @ Page
         > 5 |   runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')
             |                             ^",
             "stack": [
               "Set.forEach <anonymous>",
               "Set.forEach <anonymous>",
               "Page app/rsc-anonymous-stack-frame-sandwich/page.js (5:29)",
               "Page <anonymous>",
             ],
           },
           {
             "description": "rsc-anonymous-stack-frame-sandwich: internal",
             "environmentLabel": "Prerender",
             "label": "Console Error",
             "source": "app/rsc-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
         > 6 |   runHiddenSetOfSetsInternal('rsc-anonymous-stack-frame-sandwich: internal')
             |                             ^",
             "stack": [
               "eval webpack-internal:/(ssr)/internal-pkg/ignored.ts (18:54)",
               "eval webpack-internal:/(ssr)/internal-pkg/ignored.ts (12:7)",
               "Set.forEach <anonymous>",
               "eval webpack-internal:/(ssr)/internal-pkg/ignored.ts (11:9)",
               "Set.forEach <anonymous>",
               "runSetOfSets webpack-internal:/(ssr)/internal-pkg/ignored.ts (10:13)",
               "runHiddenSetOfSets webpack-internal:/(ssr)/internal-pkg/ignored.ts (18:3)",
               "Page app/rsc-anonymous-stack-frame-sandwich/page.js (6:29)",
               "Page <anonymous>",
             ],
           },
         ]
        `)
      } else {
        // 2nd error from runHiddenSetOfSetsInternal hits https://linear.app/vercel/issue/NEXT-4412
        await expect(browser).toDisplayCollapsedRedbox(`
         [
           {
             "description": "rsc-anonymous-stack-frame-sandwich: external",
             "environmentLabel": "Prerender",
             "label": "Console Error",
             "source": "app/rsc-anonymous-stack-frame-sandwich/page.js (5:29) @ Page
         > 5 |   runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')
             |                             ^",
             "stack": [
               "Set.forEach <anonymous>",
               "Set.forEach <anonymous>",
               "Page app/rsc-anonymous-stack-frame-sandwich/page.js (5:29)",
               "Page <anonymous>",
             ],
           },
           {
             "description": "rsc-anonymous-stack-frame-sandwich: internal",
             "environmentLabel": "Prerender",
             "label": "Console Error",
             "source": "app/rsc-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
         > 6 |   runHiddenSetOfSetsInternal('rsc-anonymous-stack-frame-sandwich: internal')
             |                             ^",
             "stack": [
               "eval webpack-internal:/(rsc)/internal-pkg/ignored.ts (18:54)",
               "eval webpack-internal:/(rsc)/internal-pkg/ignored.ts (12:7)",
               "Set.forEach <anonymous>",
               "eval webpack-internal:/(rsc)/internal-pkg/ignored.ts (11:9)",
               "Set.forEach <anonymous>",
               "runSetOfSets webpack-internal:/(rsc)/internal-pkg/ignored.ts (10:13)",
               "runHiddenSetOfSets webpack-internal:/(rsc)/internal-pkg/ignored.ts (18:3)",
               "Page app/rsc-anonymous-stack-frame-sandwich/page.js (6:29)",
               "Page <anonymous>",
             ],
           },
         ]
        `)
      }

      expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
        '' +
          'Error: rsc-anonymous-stack-frame-sandwich: external' +
          '\n    at Page (app/rsc-anonymous-stack-frame-sandwich/page.js:5:29)' +
          '\n  3 |' +
          '\n  4 | export default function Page() {' +
          "\n> 5 |   runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')" +
          '\n    |                             ^'
      )
      // TODO: assert on 2nd error once that's bug free
    } else {
      // TODO(veil): assert on 1st error once cursor position is consistent
      // TODO(veil): assert on 2nd error once that's bug free
    }
  })

  it('ignore-lists anonymous ssr stack frame sandwiches', async () => {
    if (isNextDev) {
      const outputIndex = next.cliOutput.length
      const browser = await next.browser('/ssr-anonymous-stack-frame-sandwich')

      if (isTurbopack) {
        await expect(browser).toDisplayCollapsedRedbox(`
         [
           {
             "description": "ssr-anonymous-stack-frame-sandwich: external",
             "environmentLabel": null,
             "label": "Console Error",
             "source": "app/ssr-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
         > 6 |   runHiddenSetOfSetsExternal('ssr-anonymous-stack-frame-sandwich: external')
             |                             ^",
             "stack": [
               "Page app/ssr-anonymous-stack-frame-sandwich/page.js (6:29)",
             ],
           },
           {
             "description": "ignore-listed frames",
             "environmentLabel": null,
             "label": "Console Error",
             "source": "app/ssr-anonymous-stack-frame-sandwich/page.js (7:29) @ Page
         >  7 |   runHiddenSetOfSetsInternal('ssr-anonymous-stack-frame-sandwich: internal')
              |                             ^",
             "stack": [
               "<unknown> internal-pkg/sourcemapped.ts (18:43)",
               "<unknown> internal-pkg/sourcemapped.ts (11:7)",
               "Set.forEach <anonymous>",
               "<unknown> internal-pkg/sourcemapped.ts (10:9)",
               "Set.forEach <anonymous>",
               "runSetOfSets internal-pkg/sourcemapped.ts (9:13)",
               "runHiddenSetOfSets internal-pkg/sourcemapped.ts (17:3)",
               "Page app/ssr-anonymous-stack-frame-sandwich/page.js (7:29)",
             ],
           },
         ]
        `)
      } else {
        // 2nd error from runHiddenSetOfSetsInternal hits https://linear.app/vercel/issue/NEXT-4412
        await expect(browser).toDisplayCollapsedRedbox(`
         [
           {
             "description": "ssr-anonymous-stack-frame-sandwich: external",
             "environmentLabel": null,
             "label": "Console Error",
             "source": "app/ssr-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
         > 6 |   runHiddenSetOfSetsExternal('ssr-anonymous-stack-frame-sandwich: external')
             |                             ^",
             "stack": [
               "Page app/ssr-anonymous-stack-frame-sandwich/page.js (6:29)",
             ],
           },
           {
             "description": "ignore-listed frames",
             "environmentLabel": null,
             "label": "Console Error",
             "source": "app/ssr-anonymous-stack-frame-sandwich/page.js (7:29) @ Page
         >  7 |   runHiddenSetOfSetsInternal('ssr-anonymous-stack-frame-sandwich: internal')
              |                             ^",
             "stack": [
               "eval sourcemapped.ts (18:43)",
               "eval sourcemapped.ts (11:7)",
               "Set.forEach <anonymous>",
               "eval sourcemapped.ts (10:9)",
               "Set.forEach <anonymous>",
               "runSetOfSets sourcemapped.ts (9:13)",
               "runHiddenSetOfSets sourcemapped.ts (17:3)",
               "Page app/ssr-anonymous-stack-frame-sandwich/page.js (7:29)",
             ],
           },
         ]
        `)
      }

      expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
        '' +
          'Error: ssr-anonymous-stack-frame-sandwich: external' +
          '\n    at Page (app/ssr-anonymous-stack-frame-sandwich/page.js:6:29)' +
          '\n  4 |' +
          '\n  5 | export default function Page() {' +
          "\n> 6 |   runHiddenSetOfSetsExternal('ssr-anonymous-stack-frame-sandwich: external')" +
          '\n    |                             ^'
      )
      // TODO(veil): assert on 2nd error once that's bug free
    } else {
      // TODO(veil): assert on 1st error once cursor position is consistent
      // TODO(veil): assert on 2nd error once that's bug free
    }
  })
})
Quest for Codev2.0.0
/
SIGN IN