Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSC: handle commonjs in flight loader #35563

Merged
merged 4 commits into from Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 35 additions & 20 deletions packages/next/build/webpack/loaders/next-flight-client-loader.ts
Expand Up @@ -6,6 +6,7 @@
*/

import { parse } from '../../swc'
import { buildExports } from './utils'

function addExportNames(names: string[], node: any) {
switch (node.type) {
Expand Down Expand Up @@ -39,7 +40,7 @@ function addExportNames(names: string[], node: any) {
}
}

async function parseExportNamesInto(
async function parseModuleInfo(
resourcePath: string,
transformedSource: string,
names: Array<string>
Expand All @@ -56,7 +57,7 @@ async function parseExportNamesInto(
case 'ExportDefaultExpression':
case 'ExportDefaultDeclaration':
names.push('default')
continue
break
case 'ExportNamedDeclaration':
if (node.declaration) {
if (node.declaration.type === 'VariableDeclaration') {
Expand All @@ -74,12 +75,26 @@ async function parseExportNamesInto(
addExportNames(names, specificers[j].exported)
}
}
continue
break
case 'ExportDeclaration':
if (node.declaration?.identifier) {
addExportNames(names, node.declaration.identifier)
}
continue
break
case 'ExpressionStatement': {
const {
expression: { left },
} = node
// exports.xxx = xxx
if (
left.type === 'MemberExpression' &&
left?.object.type === 'Identifier' &&
left.object?.value === 'exports'
) {
addExportNames(names, left.property)
}
break
}
default:
break
}
Expand All @@ -98,28 +113,28 @@ export default async function transformSource(
}

const names: string[] = []
await parseExportNamesInto(resourcePath, transformedSource, names)
await parseModuleInfo(resourcePath, transformedSource, names)

// next.js/packages/next/<component>.js
if (/[\\/]next[\\/](link|image)\.js$/.test(resourcePath)) {
names.push('default')
}

let newSrc =
const moduleRefDef =
"const MODULE_REFERENCE = Symbol.for('react.module.reference');\n"
for (let i = 0; i < names.length; i++) {
const name = names[i]
if (name === 'default') {
newSrc += 'export default '
} else {
newSrc += 'export const ' + name + ' = '
}
newSrc += '{ $$typeof: MODULE_REFERENCE, filepath: '
newSrc += JSON.stringify(resourcePath)
newSrc += ', name: '
newSrc += JSON.stringify(name)
newSrc += '};\n'
}

return newSrc
const clientRefsExports = names.reduce((res: any, name) => {
const moduleRef =
'{ $$typeof: MODULE_REFERENCE, filepath: ' +
JSON.stringify(resourcePath) +
', name: ' +
JSON.stringify(name) +
' };\n'
res[name] = moduleRef
return res
}, {})

// still generate module references in ESM
const output = moduleRefDef + buildExports(clientRefsExports, true)
return output
}
30 changes: 20 additions & 10 deletions packages/next/build/webpack/loaders/next-flight-server-loader.ts
@@ -1,5 +1,6 @@
import { parse } from '../../swc'
import { getRawPageExtensions } from '../../utils'
import { buildExports, isEsmNodeType } from './utils'

const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif']

Expand All @@ -24,7 +25,7 @@ const createServerComponentFilter = (pageExtensions: string[]) => {
return (importSource: string) => regex.test(importSource)
}

async function parseImportsInfo({
async function parseModuleInfo({
resourcePath,
source,
isClientCompilation,
Expand All @@ -39,16 +40,18 @@ async function parseImportsInfo({
}): Promise<{
source: string
imports: string
isEsm: boolean
}> {
const ast = await parse(source, { filename: resourcePath, isModule: true })
const { body } = ast

let transformedSource = ''
let lastIndex = 0
let imports = ''
let isEsm = false

for (let i = 0; i < body.length; i++) {
const node = body[i]
isEsm = isEsm || isEsmNodeType(node.type)
switch (node.type) {
case 'ImportDeclaration': {
const importSource = node.source.value
Expand Down Expand Up @@ -117,7 +120,7 @@ async function parseImportsInfo({
transformedSource += source.substring(lastIndex)
}

return { source: transformedSource, imports }
return { source: transformedSource, imports, isEsm }
}

export default async function transformSource(
Expand Down Expand Up @@ -152,7 +155,11 @@ export default async function transformSource(
}
}

const { source: transformedSource, imports } = await parseImportsInfo({
const {
source: transformedSource,
imports,
isEsm,
} = await parseModuleInfo({
resourcePath,
source,
isClientCompilation,
Expand All @@ -172,14 +179,17 @@ export default async function transformSource(
* export const __next_rsc__ = { __webpack_require__, _: () => { ... } }
*/

let rscExports = `export const __next_rsc__={
__webpack_require__,
_: () => {${imports}}
}`
const rscExports: any = {
__next_rsc__: `{
__webpack_require__,
_: () => {\n${imports}\n}
}`,
}

if (isClientCompilation) {
rscExports += '\nexport default function RSC () {}'
rscExports['default'] = 'function RSC() {}'
}

return transformedSource + '\n' + rscExports
const output = transformedSource + '\n' + buildExports(rscExports, isEsm)
return output
}
21 changes: 21 additions & 0 deletions packages/next/build/webpack/loaders/utils.ts
@@ -0,0 +1,21 @@
export function buildExports(moduleExports: any, isESM: boolean) {
let ret = ''
Object.keys(moduleExports).forEach((key) => {
const exportExpression = isESM
? `export ${key === 'default' ? key : `const ${key} =`} ${
moduleExports[key]
}`
: `exports.${key} = ${moduleExports[key]}`

ret += exportExpression + '\n'
})
return ret
}

const esmNodeTypes = [
'ImportDeclaration',
'ExportNamedDeclaration',
'ExportDefaultExpression',
'ExportDefaultDeclaration',
]
export const isEsmNodeType = (type: string) => esmNodeTypes.includes(type)
@@ -0,0 +1,3 @@
exports.Cjs = function Cjs() {
return 'cjs-client'
}
@@ -0,0 +1,3 @@
exports.Cjs = function Cjs() {
return 'cjs-shared'
}
Expand Up @@ -4,6 +4,8 @@ import { a, b, c, d, e } from '../components/shared-exports'
import DefaultArrow, {
Named as ClientNamed,
} from '../components/client-exports.client'
import { Cjs as CjsShared } from '../components/cjs'
import { Cjs as CjsClient } from '../components/cjs.client'

export default function Page() {
return (
Expand All @@ -21,6 +23,12 @@ export default function Page() {
<div>
<ClientNamed />
</div>
<di>
<CjsShared />
</di>
<di>
<CjsClient />
</di>
</div>
)
}
Expand Up @@ -206,6 +206,8 @@ export default function (context, { runtime, env }) {
expect(hydratedContent).toContain('abcde')
expect(hydratedContent).toContain('default-export-arrow.client')
expect(hydratedContent).toContain('named.client')
expect(hydratedContent).toContain('cjs-shared')
expect(hydratedContent).toContain('cjs-client')
})

it('should handle 404 requests and missing routes correctly', async () => {
Expand Down