2020-11-17 12:46:27 +08:00
|
|
|
/**
|
2022-10-18 23:19:24 +08:00
|
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
2020-11-17 12:46:27 +08:00
|
|
|
*
|
|
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
|
|
|
|
*
|
|
|
|
|
* @flow
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const url = require('url');
|
|
|
|
|
|
2020-12-01 06:25:56 +08:00
|
|
|
const Module = require('module');
|
|
|
|
|
|
2020-11-17 12:46:27 +08:00
|
|
|
module.exports = function register() {
|
2023-01-28 09:08:26 +08:00
|
|
|
const CLIENT_REFERENCE = Symbol.for('react.client.reference');
|
2022-08-26 00:27:30 +08:00
|
|
|
const PROMISE_PROTOTYPE = Promise.prototype;
|
|
|
|
|
|
2023-01-28 09:08:26 +08:00
|
|
|
const deepProxyHandlers = {
|
2023-01-31 21:25:05 +08:00
|
|
|
get: function (target: Function, name: string, receiver: Proxy<Function>) {
|
2023-01-28 09:08:26 +08:00
|
|
|
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;
|
|
|
|
|
// Avoid this attempting to be serialized.
|
|
|
|
|
case 'toJSON':
|
|
|
|
|
return undefined;
|
|
|
|
|
case Symbol.toPrimitive:
|
|
|
|
|
// $FlowFixMe[prop-missing]
|
|
|
|
|
return Object.prototype[Symbol.toPrimitive];
|
|
|
|
|
case 'Provider':
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Cannot render a Client Context Provider on the Server. ` +
|
|
|
|
|
`Instead, you can export a Client Component wrapper ` +
|
|
|
|
|
`that itself renders a Client Context Provider.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let expression;
|
|
|
|
|
switch (target.name) {
|
|
|
|
|
case '':
|
|
|
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
|
|
|
expression = String(name);
|
|
|
|
|
break;
|
|
|
|
|
case '*':
|
|
|
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
|
|
|
expression = String(name);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
|
|
|
expression = String(target.name) + '.' + String(name);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Cannot access ${expression} on the server. ` +
|
|
|
|
|
'You cannot dot into a client module from a server component. ' +
|
|
|
|
|
'You can only pass the imported name through.',
|
|
|
|
|
);
|
|
|
|
|
},
|
2023-01-31 21:25:05 +08:00
|
|
|
set: function () {
|
2023-01-28 09:08:26 +08:00
|
|
|
throw new Error('Cannot assign to a client module from a server module.');
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2020-12-01 06:37:27 +08:00
|
|
|
const proxyHandlers = {
|
2023-01-31 21:25:05 +08:00
|
|
|
get: function (target: Function, name: string, receiver: Proxy<Function>) {
|
2020-12-01 06:37:27 +08:00
|
|
|
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;
|
2022-08-26 00:27:30 +08:00
|
|
|
case 'async':
|
|
|
|
|
return target.async;
|
2020-12-01 06:37:27 +08:00
|
|
|
// We need to special case this because createElement reads it if we pass this
|
|
|
|
|
// reference.
|
|
|
|
|
case 'defaultProps':
|
|
|
|
|
return undefined;
|
2023-01-28 09:08:26 +08:00
|
|
|
// Avoid this attempting to be serialized.
|
|
|
|
|
case 'toJSON':
|
|
|
|
|
return undefined;
|
|
|
|
|
case Symbol.toPrimitive:
|
|
|
|
|
// $FlowFixMe[prop-missing]
|
|
|
|
|
return Object.prototype[Symbol.toPrimitive];
|
2020-12-01 06:37:27 +08:00
|
|
|
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.
|
2023-01-28 09:08:26 +08:00
|
|
|
const moduleId = target.filepath;
|
|
|
|
|
target.default = Object.defineProperties(
|
2023-01-31 21:25:05 +08:00
|
|
|
(function () {
|
2023-01-28 09:08:26 +08:00
|
|
|
throw new Error(
|
|
|
|
|
`Attempted to call the default export of ${moduleId} from the server` +
|
|
|
|
|
`but it's on the client. It's not possible to invoke a client function from ` +
|
|
|
|
|
`the server, it can only be rendered as a Component or passed to props of a` +
|
|
|
|
|
`Client Component.`,
|
|
|
|
|
);
|
|
|
|
|
}: any),
|
|
|
|
|
{
|
|
|
|
|
// This a placeholder value that tells the client to conditionally use the
|
|
|
|
|
// whole object or just the default export.
|
|
|
|
|
name: {value: ''},
|
|
|
|
|
$$typeof: {value: CLIENT_REFERENCE},
|
|
|
|
|
filepath: {value: target.filepath},
|
|
|
|
|
async: {value: target.async},
|
|
|
|
|
},
|
|
|
|
|
);
|
2020-12-01 06:37:27 +08:00
|
|
|
return true;
|
2022-08-26 00:27:30 +08:00
|
|
|
case 'then':
|
2023-01-28 09:08:26 +08:00
|
|
|
if (target.then) {
|
|
|
|
|
// Use a cached value
|
|
|
|
|
return target.then;
|
|
|
|
|
}
|
2022-08-26 00:27:30 +08:00
|
|
|
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.
|
2023-01-28 09:08:26 +08:00
|
|
|
|
2023-02-03 04:30:59 +08:00
|
|
|
const clientReference = Object.defineProperties(({}: any), {
|
|
|
|
|
// Represents the whole Module object instead of a particular import.
|
|
|
|
|
name: {value: '*'},
|
|
|
|
|
$$typeof: {value: CLIENT_REFERENCE},
|
|
|
|
|
filepath: {value: target.filepath},
|
|
|
|
|
async: {value: true},
|
|
|
|
|
});
|
2023-01-28 09:08:26 +08:00
|
|
|
const proxy = new Proxy(clientReference, proxyHandlers);
|
|
|
|
|
|
|
|
|
|
// Treat this as a resolved Promise for React's use()
|
|
|
|
|
target.status = 'fulfilled';
|
|
|
|
|
target.value = proxy;
|
|
|
|
|
|
2023-01-10 04:46:48 +08:00
|
|
|
// $FlowFixMe[missing-local-annot]
|
2023-01-28 09:08:26 +08:00
|
|
|
const then = (target.then = Object.defineProperties(
|
|
|
|
|
(function then(resolve, reject: any) {
|
|
|
|
|
// Expose to React.
|
|
|
|
|
return Promise.resolve(
|
|
|
|
|
// $FlowFixMe[incompatible-call] found when upgrading Flow
|
|
|
|
|
resolve(proxy),
|
|
|
|
|
);
|
|
|
|
|
}: any),
|
|
|
|
|
// 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.
|
|
|
|
|
{
|
|
|
|
|
name: {value: 'then'},
|
|
|
|
|
$$typeof: {value: CLIENT_REFERENCE},
|
|
|
|
|
filepath: {value: target.filepath},
|
|
|
|
|
async: {value: false},
|
|
|
|
|
},
|
|
|
|
|
));
|
2022-08-26 00:27:30 +08:00
|
|
|
return then;
|
2023-01-28 09:08:26 +08:00
|
|
|
} else {
|
|
|
|
|
// Since typeof .then === 'function' is a feature test we'd continue recursing
|
|
|
|
|
// indefinitely if we return a function. Instead, we return an object reference
|
|
|
|
|
// if we check further.
|
|
|
|
|
return undefined;
|
2022-08-26 00:27:30 +08:00
|
|
|
}
|
2020-12-01 06:37:27 +08:00
|
|
|
}
|
|
|
|
|
let cachedReference = target[name];
|
|
|
|
|
if (!cachedReference) {
|
2023-01-28 09:08:26 +08:00
|
|
|
const reference = Object.defineProperties(
|
2023-01-31 21:25:05 +08:00
|
|
|
(function () {
|
2023-01-28 09:08:26 +08:00
|
|
|
throw new Error(
|
|
|
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
|
|
|
`Attempted to call ${String(name)}() from the server but ${String(
|
|
|
|
|
name,
|
|
|
|
|
)} is on the client. ` +
|
|
|
|
|
`It's not possible to invoke a client function from the server, it can ` +
|
|
|
|
|
`only be rendered as a Component or passed to props of a Client Component.`,
|
|
|
|
|
);
|
|
|
|
|
}: any),
|
|
|
|
|
{
|
|
|
|
|
name: {value: name},
|
|
|
|
|
$$typeof: {value: CLIENT_REFERENCE},
|
|
|
|
|
filepath: {value: target.filepath},
|
|
|
|
|
async: {value: target.async},
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
cachedReference = target[name] = new Proxy(
|
|
|
|
|
reference,
|
|
|
|
|
deepProxyHandlers,
|
|
|
|
|
);
|
2020-12-01 06:37:27 +08:00
|
|
|
}
|
|
|
|
|
return cachedReference;
|
|
|
|
|
},
|
2023-01-28 09:08:26 +08:00
|
|
|
getPrototypeOf(target: Function): Object {
|
2022-08-26 00:27:30 +08:00
|
|
|
// Pretend to be a Promise in case anyone asks.
|
|
|
|
|
return PROMISE_PROTOTYPE;
|
|
|
|
|
},
|
2023-01-31 21:25:05 +08:00
|
|
|
set: function () {
|
2020-12-01 06:37:27 +08:00
|
|
|
throw new Error('Cannot assign to a client module from a server module.');
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2022-10-05 01:25:16 +08:00
|
|
|
// $FlowFixMe[prop-missing] found when upgrading Flow
|
2023-01-31 21:25:05 +08:00
|
|
|
Module._extensions['.client.js'] = function (module, path) {
|
2023-01-28 09:08:26 +08:00
|
|
|
const moduleId: string = (url.pathToFileURL(path).href: any);
|
2023-02-03 04:30:59 +08:00
|
|
|
const clientReference = Object.defineProperties(({}: any), {
|
|
|
|
|
// Represents the whole Module object instead of a particular import.
|
|
|
|
|
name: {value: '*'},
|
|
|
|
|
$$typeof: {value: CLIENT_REFERENCE},
|
|
|
|
|
filepath: {value: moduleId},
|
|
|
|
|
async: {value: false},
|
|
|
|
|
});
|
2022-10-05 01:42:34 +08:00
|
|
|
// $FlowFixMe[incompatible-call] found when upgrading Flow
|
2023-01-28 09:08:26 +08:00
|
|
|
module.exports = new Proxy(clientReference, proxyHandlers);
|
2020-11-17 12:46:27 +08:00
|
|
|
};
|
2020-12-01 06:25:56 +08:00
|
|
|
|
2022-10-05 01:25:16 +08:00
|
|
|
// $FlowFixMe[prop-missing] found when upgrading Flow
|
2020-12-01 06:25:56 +08:00
|
|
|
const originalResolveFilename = Module._resolveFilename;
|
|
|
|
|
|
2022-10-05 01:25:16 +08:00
|
|
|
// $FlowFixMe[prop-missing] found when upgrading Flow
|
2023-01-10 04:46:48 +08:00
|
|
|
// $FlowFixMe[missing-this-annot]
|
2023-01-31 21:25:05 +08:00
|
|
|
Module._resolveFilename = function (request, parent, isMain, options) {
|
2020-12-10 11:55:38 +08:00
|
|
|
const resolved = originalResolveFilename.apply(this, arguments);
|
|
|
|
|
if (resolved.endsWith('.server.js')) {
|
2020-12-01 06:25:56 +08:00
|
|
|
if (
|
|
|
|
|
parent &&
|
|
|
|
|
parent.filename &&
|
|
|
|
|
!parent.filename.endsWith('.server.js')
|
|
|
|
|
) {
|
2020-12-10 11:55:38 +08:00
|
|
|
let reason;
|
|
|
|
|
if (request.endsWith('.server.js')) {
|
|
|
|
|
reason = `"${request}"`;
|
|
|
|
|
} else {
|
|
|
|
|
reason = `"${request}" (which expands to "${resolved}")`;
|
|
|
|
|
}
|
2020-12-01 06:25:56 +08:00
|
|
|
throw new Error(
|
2020-12-10 11:55:38 +08:00
|
|
|
`Cannot import ${reason} from "${parent.filename}". ` +
|
2020-12-01 06:25:56 +08:00
|
|
|
'By react-server convention, .server.js files can only be imported from other .server.js files. ' +
|
|
|
|
|
'That way nobody accidentally sends these to the client by indirectly importing it.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-10 11:55:38 +08:00
|
|
|
return resolved;
|
2020-12-01 06:25:56 +08:00
|
|
|
};
|
2020-11-17 12:46:27 +08:00
|
|
|
};
|