rsnext/packages/next/build/webpack/loaders/next-flight-server-loader.ts
Shu Ding 931666dd3c
Fix uncaught error in getInitialProps when runtime is set to nodejs (#34228)
This PR ensures that the test "should render 500 error correctly" doesn't break when `runtime` is set to `nodejs` with `serverComponents` enabled.

This test case is now moved to the "basic" suite to ensure it doesn't break in both runtimes. And "should not bundle external imports into client builds for RSC" is enabled for the `nodejs` runtime too.

## Bug

- [ ] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`
2022-02-11 18:30:39 +00:00

182 lines
5.4 KiB
TypeScript

// TODO: add ts support for next-swc api
// @ts-ignore
import { parse } from '../../swc'
// @ts-ignore
import { getBaseSWCOptions } from '../../swc/options'
import { getRawPageExtensions } from '../../utils'
function isClientComponent(importSource: string, pageExtensions: string[]) {
return new RegExp(`\\.client(\\.(${pageExtensions.join('|')}))?`).test(
importSource
)
}
function isServerComponent(importSource: string, pageExtensions: string[]) {
return new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?`).test(
importSource
)
}
function isNextComponent(importSource: string) {
return (
importSource.includes('next/link') || importSource.includes('next/image')
)
}
export function isImageImport(importSource: string) {
// TODO: share extension with next/image
// TODO: add other static assets, jpeg -> jpg
return ['jpg', 'jpeg', 'png', 'webp', 'avif'].some((imageExt) =>
importSource.endsWith('.' + imageExt)
)
}
async function parseImportsInfo(
resourcePath: string,
source: string,
imports: Array<string>,
isClientCompilation: boolean,
pageExtensions: string[]
): Promise<{
source: string
defaultExportName: string
}> {
const opts = getBaseSWCOptions({
filename: resourcePath,
globalWindow: isClientCompilation,
})
const ast = await parse(source, { ...opts.jsc.parser, isModule: true })
const { body } = ast
const beginPos = ast.span.start
let transformedSource = ''
let lastIndex = 0
let defaultExportName
for (let i = 0; i < body.length; i++) {
const node = body[i]
switch (node.type) {
case 'ImportDeclaration': {
const importSource = node.source.value
if (!isClientCompilation) {
if (
!(
isClientComponent(importSource, pageExtensions) ||
isNextComponent(importSource) ||
isImageImport(importSource)
)
) {
continue
}
const importDeclarations = source.substring(
lastIndex,
node.source.span.start - beginPos
)
transformedSource += importDeclarations
transformedSource += JSON.stringify(`${node.source.value}?flight`)
} else {
// For the client compilation, we skip all modules imports but
// always keep client components in the bundle. All client components
// have to be imported from either server or client components.
if (
!(
isClientComponent(importSource, pageExtensions) ||
isServerComponent(importSource, pageExtensions) ||
// Special cases for Next.js APIs that are considered as client
// components:
isNextComponent(importSource) ||
isImageImport(importSource)
)
) {
continue
}
}
lastIndex = node.source.span.end - beginPos
imports.push(`require(${JSON.stringify(importSource)})`)
continue
}
case 'ExportDefaultDeclaration': {
const def = node.decl
if (def.type === 'Identifier') {
defaultExportName = def.name
} else if (def.type === 'FunctionExpression') {
defaultExportName = def.identifier.value
}
break
}
case 'ExportDefaultExpression':
const exp = node.expression
if (exp.type === 'Identifier') {
defaultExportName = exp.value
}
break
default:
break
}
}
if (!isClientCompilation) {
transformedSource += source.substring(lastIndex)
}
return { source: transformedSource, defaultExportName }
}
export default async function transformSource(
this: any,
source: string
): Promise<string> {
const { client: isClientCompilation, pageExtensions: pageExtensionsJson } =
this.getOptions()
const { resourcePath } = this
const pageExtensions = JSON.parse(pageExtensionsJson)
if (typeof source !== 'string') {
throw new Error('Expected source to have been transformed to a string.')
}
if (resourcePath.includes('/node_modules/')) {
return source
}
const imports: string[] = []
const { source: transformedSource, defaultExportName } =
await parseImportsInfo(
resourcePath,
source,
imports,
isClientCompilation,
getRawPageExtensions(pageExtensions)
)
/**
* For .server.js files, we handle this loader differently.
*
* Server compilation output:
* export default function ServerComponent() { ... }
* export const __rsc_noop__ = () => { ... }
* ServerComponent.__next_rsc__ = 1
* ServerComponent.__webpack_require__ = __webpack_require__
*
* Client compilation output:
* The function body of Server Component will be removed
*/
const noop = `export const __rsc_noop__=()=>{${imports.join(';')}}`
let defaultExportNoop = ''
if (isClientCompilation) {
defaultExportNoop = `export default function ${
defaultExportName || 'ServerComponent'
}(){}\n${defaultExportName || 'ServerComponent'}.__next_rsc__=1;`
} else {
if (defaultExportName) {
// It's required to have the default export for pages. For other components, it's fine to leave it as is.
defaultExportNoop = `${defaultExportName}.__next_rsc__=1;${defaultExportName}.__webpack_require__=__webpack_require__;`
}
}
const transformed = transformedSource + '\n' + noop + '\n' + defaultExportNoop
return transformed
}