2022-01-27 16:48:09 +01:00
|
|
|
// TODO: add ts support for next-swc api
|
|
|
|
// @ts-ignore
|
|
|
|
import { parse } from '../../swc'
|
|
|
|
// @ts-ignore
|
|
|
|
import { getBaseSWCOptions } from '../../swc/options'
|
2021-10-26 18:50:56 +02:00
|
|
|
import { getRawPageExtensions } from '../../utils'
|
|
|
|
|
2022-02-28 16:00:28 +01:00
|
|
|
const getIsClientComponent =
|
|
|
|
(pageExtensions: string[]) => (importSource: string) => {
|
|
|
|
return new RegExp(`\\.client(\\.(${pageExtensions.join('|')}))?`).test(
|
|
|
|
importSource
|
|
|
|
)
|
|
|
|
}
|
2021-10-26 18:50:56 +02:00
|
|
|
|
2022-02-28 16:00:28 +01:00
|
|
|
const getIsServerComponent =
|
|
|
|
(pageExtensions: string[]) => (importSource: string) => {
|
|
|
|
return new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?`).test(
|
|
|
|
importSource
|
|
|
|
)
|
|
|
|
}
|
2021-11-30 23:54:47 +01:00
|
|
|
|
2021-10-26 18:50:56 +02:00
|
|
|
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)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-02-28 16:00:28 +01:00
|
|
|
async function parseImportsInfo({
|
|
|
|
resourcePath,
|
|
|
|
source,
|
|
|
|
imports,
|
|
|
|
isClientCompilation,
|
|
|
|
isServerComponent,
|
|
|
|
isClientComponent,
|
|
|
|
}: {
|
|
|
|
resourcePath: string
|
|
|
|
source: string
|
|
|
|
imports: Array<string>
|
|
|
|
isClientCompilation: boolean
|
|
|
|
isServerComponent: (name: string) => boolean
|
|
|
|
isClientComponent: (name: string) => boolean
|
|
|
|
}): Promise<{
|
2022-01-14 14:01:00 +01:00
|
|
|
source: string
|
|
|
|
defaultExportName: string
|
|
|
|
}> {
|
2022-01-27 16:48:09 +01:00
|
|
|
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
|
2021-10-26 18:50:56 +02:00
|
|
|
let transformedSource = ''
|
|
|
|
let lastIndex = 0
|
2022-01-27 16:48:09 +01:00
|
|
|
let defaultExportName
|
2021-10-26 18:50:56 +02:00
|
|
|
for (let i = 0; i < body.length; i++) {
|
|
|
|
const node = body[i]
|
|
|
|
switch (node.type) {
|
2022-01-14 14:01:00 +01:00
|
|
|
case 'ImportDeclaration': {
|
2021-10-26 18:50:56 +02:00
|
|
|
const importSource = node.source.value
|
|
|
|
if (!isClientCompilation) {
|
2022-02-28 16:00:28 +01:00
|
|
|
// Server compilation for .server.js.
|
|
|
|
if (isServerComponent(importSource)) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const importDeclarations = source.substring(
|
|
|
|
lastIndex,
|
|
|
|
node.source.span.start - beginPos
|
|
|
|
)
|
|
|
|
|
2021-11-03 03:14:14 +01:00
|
|
|
if (
|
|
|
|
!(
|
2022-02-28 16:00:28 +01:00
|
|
|
isClientComponent(importSource) ||
|
2021-11-03 03:14:14 +01:00
|
|
|
isNextComponent(importSource) ||
|
|
|
|
isImageImport(importSource)
|
|
|
|
)
|
|
|
|
) {
|
2022-02-28 16:00:28 +01:00
|
|
|
if (
|
|
|
|
['react/jsx-runtime', 'react/jsx-dev-runtime'].includes(
|
|
|
|
importSource
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// A shared component. It should be handled as a server
|
|
|
|
// component.
|
|
|
|
transformedSource += importDeclarations
|
|
|
|
transformedSource += JSON.stringify(`${importSource}?__sc_server__`)
|
|
|
|
} else {
|
|
|
|
// A client component. It should be loaded as module reference.
|
|
|
|
transformedSource += importDeclarations
|
|
|
|
transformedSource += JSON.stringify(`${importSource}?__sc_client__`)
|
|
|
|
imports.push(`require(${JSON.stringify(importSource)})`)
|
2021-11-03 03:14:14 +01:00
|
|
|
}
|
2021-11-30 23:54:47 +01:00
|
|
|
} 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 (
|
|
|
|
!(
|
2022-02-28 16:00:28 +01:00
|
|
|
isClientComponent(importSource) ||
|
|
|
|
isServerComponent(importSource) ||
|
2021-11-30 23:54:47 +01:00
|
|
|
// Special cases for Next.js APIs that are considered as client
|
|
|
|
// components:
|
|
|
|
isNextComponent(importSource) ||
|
|
|
|
isImageImport(importSource)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
continue
|
|
|
|
}
|
2022-02-28 16:00:28 +01:00
|
|
|
|
|
|
|
imports.push(`require(${JSON.stringify(importSource)})`)
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|
|
|
|
|
2022-01-27 16:48:09 +01:00
|
|
|
lastIndex = node.source.span.end - beginPos
|
2022-02-28 16:00:28 +01:00
|
|
|
break
|
2022-01-14 14:01:00 +01:00
|
|
|
}
|
|
|
|
case 'ExportDefaultDeclaration': {
|
2022-01-27 16:48:09 +01:00
|
|
|
const def = node.decl
|
2022-01-24 17:19:57 +01:00
|
|
|
if (def.type === 'Identifier') {
|
|
|
|
defaultExportName = def.name
|
2022-01-27 16:48:09 +01:00
|
|
|
} else if (def.type === 'FunctionExpression') {
|
|
|
|
defaultExportName = def.identifier.value
|
2022-01-24 17:19:57 +01:00
|
|
|
}
|
2022-01-14 14:01:00 +01:00
|
|
|
break
|
|
|
|
}
|
2022-02-11 19:30:39 +01:00
|
|
|
case 'ExportDefaultExpression':
|
|
|
|
const exp = node.expression
|
|
|
|
if (exp.type === 'Identifier') {
|
|
|
|
defaultExportName = exp.value
|
|
|
|
}
|
|
|
|
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-01-14 14:01:00 +01:00
|
|
|
return { source: transformedSource, defaultExportName }
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export default async function transformSource(
|
|
|
|
this: any,
|
|
|
|
source: string
|
|
|
|
): Promise<string> {
|
2022-02-28 16:00:28 +01:00
|
|
|
const { client: isClientCompilation, pageExtensions } = this.getOptions()
|
|
|
|
const { resourcePath, resourceQuery } = this
|
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-02-28 16:00:28 +01:00
|
|
|
// We currently assume that all components are shared components (unsuffixed)
|
|
|
|
// from node_modules.
|
2021-10-26 18:50:56 +02:00
|
|
|
if (resourcePath.includes('/node_modules/')) {
|
|
|
|
return source
|
|
|
|
}
|
|
|
|
|
2022-02-28 16:00:28 +01:00
|
|
|
const rawRawPageExtensions = getRawPageExtensions(pageExtensions)
|
|
|
|
const isServerComponent = getIsServerComponent(rawRawPageExtensions)
|
|
|
|
const isClientComponent = getIsClientComponent(rawRawPageExtensions)
|
|
|
|
|
|
|
|
if (!isClientCompilation) {
|
|
|
|
// We only apply the loader to server components, or shared components that
|
|
|
|
// are imported by a server component.
|
|
|
|
if (
|
|
|
|
!isServerComponent(resourcePath) &&
|
|
|
|
resourceQuery !== '?__sc_server__'
|
|
|
|
) {
|
|
|
|
return source
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-26 18:50:56 +02:00
|
|
|
const imports: string[] = []
|
2022-01-14 14:01:00 +01:00
|
|
|
const { source: transformedSource, defaultExportName } =
|
2022-02-28 16:00:28 +01:00
|
|
|
await parseImportsInfo({
|
2022-01-27 16:48:09 +01:00
|
|
|
resourcePath,
|
2022-01-14 14:01:00 +01:00
|
|
|
source,
|
|
|
|
imports,
|
|
|
|
isClientCompilation,
|
2022-02-28 16:00:28 +01:00
|
|
|
isServerComponent,
|
|
|
|
isClientComponent,
|
|
|
|
})
|
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:
|
|
|
|
* export default function ServerComponent() { ... }
|
|
|
|
* export const __rsc_noop__ = () => { ... }
|
|
|
|
* ServerComponent.__next_rsc__ = 1
|
|
|
|
* ServerComponent.__webpack_require__ = __webpack_require__
|
2022-01-14 14:01:00 +01:00
|
|
|
*
|
2022-02-08 14:16:46 +01:00
|
|
|
* Client compilation output:
|
|
|
|
* The function body of Server Component will be removed
|
2022-01-14 14:01:00 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
const noop = `export const __rsc_noop__=()=>{${imports.join(';')}}`
|
2022-02-11 19:30:39 +01:00
|
|
|
|
|
|
|
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__;`
|
|
|
|
}
|
|
|
|
}
|
2022-01-14 14:01:00 +01:00
|
|
|
|
|
|
|
const transformed = transformedSource + '\n' + noop + '\n' + defaultExportNoop
|
|
|
|
return transformed
|
2021-10-26 18:50:56 +02:00
|
|
|
}
|