2022-04-13 10:50:36 +02:00
|
|
|
import { builtinModules } from 'module'
|
|
|
|
|
2022-01-27 16:48:09 +01:00
|
|
|
import { parse } from '../../swc'
|
2022-04-14 00:35:11 +02:00
|
|
|
import {
|
|
|
|
buildExports,
|
|
|
|
createClientComponentFilter,
|
|
|
|
createServerComponentFilter,
|
2022-04-17 03:38:56 +02:00
|
|
|
isNextBuiltinClientComponent,
|
2022-04-14 00:35:11 +02:00
|
|
|
} from './utils'
|
2021-10-26 18:50:56 +02:00
|
|
|
|
2022-04-17 03:38:56 +02:00
|
|
|
function createFlightServerRequest(
|
|
|
|
request: string,
|
|
|
|
options?: { client: 1 | undefined }
|
|
|
|
) {
|
2022-04-13 10:50:36 +02:00
|
|
|
return `next-flight-server-loader?${JSON.stringify(options)}!${request}`
|
2022-04-07 21:45:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function hasFlightLoader(request: string, type: 'client' | 'server') {
|
|
|
|
return request.includes(`next-flight-${type}-loader`)
|
|
|
|
}
|
|
|
|
|
2022-03-25 22:17:33 +01:00
|
|
|
async function parseModuleInfo({
|
2022-02-28 16:00:28 +01:00
|
|
|
resourcePath,
|
|
|
|
source,
|
|
|
|
isClientCompilation,
|
|
|
|
isServerComponent,
|
|
|
|
isClientComponent,
|
2022-04-07 21:45:33 +02:00
|
|
|
resolver,
|
2022-02-28 16:00:28 +01:00
|
|
|
}: {
|
|
|
|
resourcePath: string
|
|
|
|
source: string
|
|
|
|
isClientCompilation: boolean
|
|
|
|
isServerComponent: (name: string) => boolean
|
|
|
|
isClientComponent: (name: string) => boolean
|
2022-04-07 21:45:33 +02:00
|
|
|
resolver: (req: string) => Promise<string>
|
2022-02-28 16:00:28 +01:00
|
|
|
}): Promise<{
|
2022-01-14 14:01:00 +01:00
|
|
|
source: string
|
2022-04-07 21:45:33 +02:00
|
|
|
imports: string[]
|
2022-03-25 22:17:33 +01:00
|
|
|
isEsm: boolean
|
2022-04-01 18:13:38 +02:00
|
|
|
__N_SSP: boolean
|
|
|
|
pageRuntime: 'edge' | 'nodejs' | null
|
2022-01-14 14:01:00 +01:00
|
|
|
}> {
|
2022-03-28 13:34:32 +02:00
|
|
|
const ast = await parse(source, {
|
|
|
|
filename: resourcePath,
|
|
|
|
isModule: 'unknown',
|
|
|
|
})
|
|
|
|
const { type, body } = ast
|
2021-10-26 18:50:56 +02:00
|
|
|
let transformedSource = ''
|
|
|
|
let lastIndex = 0
|
2022-04-07 21:45:33 +02:00
|
|
|
let imports = []
|
2022-04-01 18:13:38 +02:00
|
|
|
let __N_SSP = false
|
|
|
|
let pageRuntime = null
|
2022-04-13 10:50:36 +02:00
|
|
|
let isBuiltinModule
|
|
|
|
let isNodeModuleImport
|
2022-04-01 18:13:38 +02:00
|
|
|
|
2022-03-28 13:34:32 +02:00
|
|
|
const isEsm = type === 'Module'
|
2022-03-02 19:29:54 +01:00
|
|
|
|
2022-04-13 10:50:36 +02:00
|
|
|
async function getModuleType(path: string) {
|
|
|
|
const isBuiltinModule_ = builtinModules.includes(path)
|
|
|
|
const resolvedPath = isBuiltinModule_ ? path : await resolver(path)
|
|
|
|
|
2022-04-17 03:38:56 +02:00
|
|
|
const isNodeModuleImport_ =
|
|
|
|
/[\\/]node_modules[\\/]/.test(resolvedPath) &&
|
|
|
|
// exclude next built-in modules
|
|
|
|
!isNextBuiltinClientComponent(resolvedPath)
|
2022-04-13 10:50:36 +02:00
|
|
|
|
|
|
|
return [isBuiltinModule_, isNodeModuleImport_] as const
|
|
|
|
}
|
|
|
|
|
|
|
|
function addClientImport(path: string) {
|
|
|
|
if (isServerComponent(path) || hasFlightLoader(path, 'server')) {
|
|
|
|
// If it's a server component, we recursively import its dependencies.
|
|
|
|
imports.push(path)
|
|
|
|
} else if (isClientComponent(path)) {
|
|
|
|
// Client component.
|
|
|
|
imports.push(path)
|
|
|
|
} else {
|
|
|
|
// Shared component.
|
2022-04-17 03:38:56 +02:00
|
|
|
imports.push(createFlightServerRequest(path, { client: 1 }))
|
2022-04-13 10:50:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-26 18:50:56 +02:00
|
|
|
for (let i = 0; i < body.length; i++) {
|
|
|
|
const node = body[i]
|
|
|
|
switch (node.type) {
|
2022-04-01 18:13:38 +02:00
|
|
|
case 'ImportDeclaration':
|
2021-10-26 18:50:56 +02:00
|
|
|
const importSource = node.source.value
|
2022-04-13 10:50:36 +02:00
|
|
|
|
|
|
|
;[isBuiltinModule, isNodeModuleImport] = await getModuleType(
|
|
|
|
importSource
|
|
|
|
)
|
2022-04-07 21:45:33 +02:00
|
|
|
|
|
|
|
// matching node_module package but excluding react cores since react is required to be shared
|
|
|
|
const isReactImports = [
|
|
|
|
'react',
|
|
|
|
'react/jsx-runtime',
|
|
|
|
'react/jsx-dev-runtime',
|
|
|
|
].includes(importSource)
|
|
|
|
|
2021-10-26 18:50:56 +02:00
|
|
|
if (!isClientCompilation) {
|
2022-02-28 16:00:28 +01:00
|
|
|
// Server compilation for .server.js.
|
|
|
|
if (isServerComponent(importSource)) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const importDeclarations = source.substring(
|
|
|
|
lastIndex,
|
2022-03-01 19:27:59 +01:00
|
|
|
node.source.span.start
|
2022-02-28 16:00:28 +01:00
|
|
|
)
|
|
|
|
|
2022-03-01 19:27:59 +01:00
|
|
|
if (isClientComponent(importSource)) {
|
|
|
|
transformedSource += importDeclarations
|
2022-04-07 21:45:33 +02:00
|
|
|
transformedSource += JSON.stringify(
|
|
|
|
`next-flight-client-loader!${importSource}`
|
|
|
|
)
|
|
|
|
imports.push(importSource)
|
2022-03-01 19:27:59 +01:00
|
|
|
} else {
|
2022-04-07 21:45:33 +02:00
|
|
|
// A shared component. It should be handled as a server component.
|
2022-04-13 10:50:36 +02:00
|
|
|
const serverImportSource =
|
|
|
|
isReactImports || isBuiltinModule
|
|
|
|
? importSource
|
2022-04-17 03:38:56 +02:00
|
|
|
: createFlightServerRequest(importSource)
|
2022-02-28 16:00:28 +01:00
|
|
|
transformedSource += importDeclarations
|
2022-04-07 21:45:33 +02:00
|
|
|
transformedSource += JSON.stringify(serverImportSource)
|
|
|
|
|
|
|
|
// TODO: support handling RSC components from node_modules
|
|
|
|
if (!isNodeModuleImport) {
|
|
|
|
imports.push(importSource)
|
|
|
|
}
|
2021-11-03 03:14:14 +01:00
|
|
|
}
|
2021-11-30 23:54:47 +01:00
|
|
|
} else {
|
2022-04-13 10:50:36 +02:00
|
|
|
// For now we assume there is no .client.js inside node_modules.
|
|
|
|
// TODO: properly handle this.
|
|
|
|
if (isNodeModuleImport || isBuiltinModule) continue
|
|
|
|
addClientImport(importSource)
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|
|
|
|
|
2022-03-01 19:27:59 +01:00
|
|
|
lastIndex = node.source.span.end
|
2022-02-28 16:00:28 +01:00
|
|
|
break
|
2022-04-01 18:13:38 +02:00
|
|
|
case 'ExportDeclaration':
|
|
|
|
if (isClientCompilation) {
|
|
|
|
// Keep `__N_SSG` and `__N_SSP` exports.
|
|
|
|
if (node.declaration?.type === 'VariableDeclaration') {
|
|
|
|
for (const declaration of node.declaration.declarations) {
|
|
|
|
if (declaration.type === 'VariableDeclarator') {
|
|
|
|
if (declaration.id?.type === 'Identifier') {
|
|
|
|
const value = declaration.id.value
|
|
|
|
if (value === '__N_SSP') {
|
|
|
|
__N_SSP = true
|
|
|
|
} else if (value === 'config') {
|
|
|
|
const props = declaration.init.properties
|
|
|
|
const runtimeKeyValue = props.find(
|
|
|
|
(prop: any) => prop.key.value === 'runtime'
|
|
|
|
)
|
|
|
|
const runtime = runtimeKeyValue?.value?.value
|
|
|
|
if (runtime === 'nodejs' || runtime === 'edge') {
|
|
|
|
pageRuntime = runtime
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break
|
2022-04-13 10:50:36 +02:00
|
|
|
case 'ExportNamedDeclaration':
|
|
|
|
if (isClientCompilation) {
|
|
|
|
if (node.source) {
|
|
|
|
// export { ... } from '...'
|
|
|
|
const path = node.source.value
|
|
|
|
;[isBuiltinModule, isNodeModuleImport] = await getModuleType(path)
|
|
|
|
if (!isBuiltinModule && !isNodeModuleImport) {
|
|
|
|
addClientImport(path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break
|
2021-10-26 18:50:56 +02:00
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isClientCompilation) {
|
2022-01-14 14:01:00 +01:00
|
|
|
transformedSource += source.substring(lastIndex)
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|
|
|
|
|
2022-04-01 18:13:38 +02:00
|
|
|
return { source: transformedSource, imports, isEsm, __N_SSP, pageRuntime }
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export default async function transformSource(
|
|
|
|
this: any,
|
|
|
|
source: string
|
|
|
|
): Promise<string> {
|
2022-04-17 03:38:56 +02:00
|
|
|
const { client: isClientCompilation } = this.getOptions()
|
2022-04-07 21:45:33 +02:00
|
|
|
const { resourcePath, resolve: resolveFn, context } = this
|
|
|
|
|
|
|
|
const resolver = (req: string): Promise<string> => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
resolveFn(context, req, (err: any, result: string) => {
|
|
|
|
if (err) return reject(err)
|
|
|
|
resolve(result)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2021-10-26 18:50:56 +02:00
|
|
|
|
|
|
|
if (typeof source !== 'string') {
|
|
|
|
throw new Error('Expected source to have been transformed to a string.')
|
|
|
|
}
|
|
|
|
|
2022-04-17 03:38:56 +02:00
|
|
|
const isServerComponent = createServerComponentFilter()
|
|
|
|
const isClientComponent = createClientComponentFilter()
|
2022-04-07 21:45:33 +02:00
|
|
|
const hasAppliedFlightServerLoader = this.loaders.some((loader: any) => {
|
|
|
|
return hasFlightLoader(loader.path, 'server')
|
|
|
|
})
|
|
|
|
const isServerExt = isServerComponent(resourcePath)
|
2022-02-28 16:00:28 +01:00
|
|
|
|
|
|
|
if (!isClientCompilation) {
|
|
|
|
// We only apply the loader to server components, or shared components that
|
|
|
|
// are imported by a server component.
|
2022-04-07 21:45:33 +02:00
|
|
|
if (!isServerExt && !hasAppliedFlightServerLoader) {
|
2022-02-28 16:00:28 +01:00
|
|
|
return source
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-25 22:17:33 +01:00
|
|
|
const {
|
|
|
|
source: transformedSource,
|
|
|
|
imports,
|
|
|
|
isEsm,
|
2022-04-01 18:13:38 +02:00
|
|
|
__N_SSP,
|
|
|
|
pageRuntime,
|
2022-03-25 22:17:33 +01:00
|
|
|
} = await parseModuleInfo({
|
2022-03-02 19:29:54 +01:00
|
|
|
resourcePath,
|
|
|
|
source,
|
|
|
|
isClientCompilation,
|
|
|
|
isServerComponent,
|
|
|
|
isClientComponent,
|
2022-04-07 21:45:33 +02:00
|
|
|
resolver,
|
2022-03-02 19:29:54 +01:00
|
|
|
})
|
2022-01-14 14:01:00 +01:00
|
|
|
|
|
|
|
/**
|
2022-02-08 14:16:46 +01:00
|
|
|
* For .server.js files, we handle this loader differently.
|
2022-01-14 14:01:00 +01:00
|
|
|
*
|
2022-02-08 14:16:46 +01:00
|
|
|
* Server compilation output:
|
2022-03-02 19:29:54 +01:00
|
|
|
* (The content of the Server Component module will be kept.)
|
2022-04-05 21:46:17 +02:00
|
|
|
* export const __next_rsc__ = { __webpack_require__, _: () => { ... }, server: true }
|
2022-01-14 14:01:00 +01:00
|
|
|
*
|
2022-02-08 14:16:46 +01:00
|
|
|
* Client compilation output:
|
2022-03-02 19:29:54 +01:00
|
|
|
* (The content of the Server Component module will be removed.)
|
2022-04-05 21:46:17 +02:00
|
|
|
* export const __next_rsc__ = { __webpack_require__, _: () => { ... }, server: false }
|
2022-01-14 14:01:00 +01:00
|
|
|
*/
|
|
|
|
|
2022-03-25 22:17:33 +01:00
|
|
|
const rscExports: any = {
|
|
|
|
__next_rsc__: `{
|
|
|
|
__webpack_require__,
|
2022-04-07 21:45:33 +02:00
|
|
|
_: () => {
|
|
|
|
${imports
|
2022-04-08 12:53:02 +02:00
|
|
|
.map(
|
|
|
|
(importSource) =>
|
|
|
|
`import(/* webpackMode: "eager" */ ${JSON.stringify(
|
|
|
|
importSource
|
|
|
|
)});`
|
|
|
|
)
|
2022-04-07 21:45:33 +02:00
|
|
|
.join('\n')}
|
|
|
|
},
|
|
|
|
server: ${isServerExt ? 'true' : 'false'}
|
2022-03-25 22:17:33 +01:00
|
|
|
}`,
|
|
|
|
}
|
2022-02-11 19:30:39 +01:00
|
|
|
|
|
|
|
if (isClientCompilation) {
|
2022-04-01 18:13:38 +02:00
|
|
|
rscExports.default = 'function RSC() {}'
|
|
|
|
|
|
|
|
if (pageRuntime === 'edge') {
|
|
|
|
// Currently for the Edge runtime, we treat all RSC pages as SSR pages.
|
|
|
|
rscExports.__N_SSP = 'true'
|
|
|
|
} else {
|
|
|
|
if (__N_SSP) {
|
|
|
|
rscExports.__N_SSP = 'true'
|
|
|
|
} else {
|
|
|
|
// Server component pages are always considered as SSG by default because
|
|
|
|
// the flight data is needed for client navigation.
|
|
|
|
rscExports.__N_SSG = 'true'
|
|
|
|
}
|
|
|
|
}
|
2022-02-11 19:30:39 +01:00
|
|
|
}
|
2022-01-14 14:01:00 +01:00
|
|
|
|
2022-03-25 22:17:33 +01:00
|
|
|
const output = transformedSource + '\n' + buildExports(rscExports, isEsm)
|
|
|
|
return output
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|