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

Handle async module for client components #39953

Merged
merged 9 commits into from Aug 29, 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
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -177,8 +177,8 @@
"react-17": "npm:react@17.0.2",
"react-dom": "18.2.0",
"react-dom-17": "npm:react-dom@17.0.2",
"react-dom-exp": "npm:react-dom@0.0.0-experimental-6ef466c68-20220816",
"react-exp": "npm:react@0.0.0-experimental-6ef466c68-20220816",
"react-dom-exp": "npm:react-dom@0.0.0-experimental-0de3ddf56-20220825",
"react-exp": "npm:react@0.0.0-experimental-0de3ddf56-20220825",
"react-ssr-prepass": "1.0.8",
"react-virtualized": "9.22.3",
"relay-compiler": "13.0.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/next/build/analysis/get-page-static-info.ts
Expand Up @@ -28,7 +28,7 @@ export interface PageStaticInfo {
* - Modules with `export function getStaticProps | getServerSideProps`
* - Modules with `export { getStaticProps | getServerSideProps } <from ...>`
*/
function checkExports(swcAST: any) {
export function checkExports(swcAST: any): { ssr: boolean; ssg: boolean } {
if (Array.isArray(swcAST?.body)) {
try {
for (const node of swcAST.body) {
Expand Down
177 changes: 0 additions & 177 deletions packages/next/build/webpack/loaders/next-flight-client-loader.ts

This file was deleted.

@@ -0,0 +1,35 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { checkExports } from '../../../analysis/get-page-static-info'
import { parse } from '../../../swc'

export default async function transformSource(
this: any,
source: string
): Promise<string> {
const { resourcePath } = this

const transformedSource = source
if (typeof transformedSource !== 'string') {
throw new Error('Expected source to have been transformed to a string.')
}

const swcAST = await parse(transformedSource, {
filename: resourcePath,
isModule: 'unknown',
})
const { ssg, ssr } = checkExports(swcAST)

const output = `
const { createProxy } = require("next/dist/build/webpack/loaders/next-flight-client-loader/module-proxy")\n
module.exports = createProxy(${JSON.stringify(
resourcePath
)}, { ssr: ${ssr}, ssg: ${ssg} })
`
return output
}
@@ -0,0 +1,119 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

// Modified from https://github.com/facebook/react/blob/main/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js

const MODULE_REFERENCE = Symbol.for('react.module.reference')
const PROMISE_PROTOTYPE = Promise.prototype

const proxyHandlers: ProxyHandler<object> = {
get: function (target: any, name: string, _receiver: any) {
switch (name) {
// These names are read by the Flight runtime if you end up using the exports object.
case '$$typeof':
// These names are a little too common. We should probably have a way to
// have the Flight runtime extract the inner target instead.
return target.$$typeof
case 'filepath':
return target.filepath
case 'name':
return target.name
case 'async':
return target.async
// We need to special case this because createElement reads it if we pass this
// reference.
case 'defaultProps':
return undefined
case '__esModule':
// Something is conditionally checking which export to use. We'll pretend to be
// an ESM compat module but then we'll check again on the client.
target.default = {
$$typeof: MODULE_REFERENCE,
filepath: target.filepath,
// This a placeholder value that tells the client to conditionally use the
// whole object or just the default export.
name: '',
async: target.async,

ssr: target.ssr,
ssg: target.ssg,
}
return true
case 'then':
if (!target.async) {
// If this module is expected to return a Promise (such as an AsyncModule) then
// we should resolve that with a client reference that unwraps the Promise on
// the client.
const then = function then(
resolve: (res: any) => void,
_reject: (err: any) => void
) {
const moduleReference: Record<string, any> = {
$$typeof: MODULE_REFERENCE,
filepath: target.filepath,
name: '*', // Represents the whole object instead of a particular import.
async: true,

ssr: target.ssr,
ssg: target.ssg,
}
return Promise.resolve(
resolve(new Proxy(moduleReference, proxyHandlers))
)
}
// If this is not used as a Promise but is treated as a reference to a `.then`
// export then we should treat it as a reference to that name.
then.$$typeof = MODULE_REFERENCE
then.filepath = target.filepath
// then.name is conveniently already "then" which is the export name we need.
// This will break if it's minified though.
return then
}
break

case 'ssg':
return target.ssg
case 'ssr':
return target.ssr
default:
break
}
let cachedReference = target[name]
if (!cachedReference) {
cachedReference = target[name] = {
$$typeof: MODULE_REFERENCE,
filepath: target.filepath,
name: name,
async: target.async,
}
}
return cachedReference
},
getPrototypeOf(_target: object) {
// Pretend to be a Promise in case anyone asks.
return PROMISE_PROTOTYPE
},
set: function () {
throw new Error('Cannot assign to a client module from a server module.')
},
}

export function createProxy(
moduleId: string,
{ ssr, ssg }: { ssr: boolean; ssg: boolean }
) {
const moduleReference = {
$$typeof: MODULE_REFERENCE,
filepath: moduleId,
name: '*', // Represents the whole object instead of a particular import.
async: false,

ssr,
ssg,
}
return new Proxy(moduleReference, proxyHandlers)
}