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
|
|
|
|
|
*/
|
|
|
|
|
|
[Flight Plugin] Scan for "use client" (#26474)
## Summary
Our toy webpack plugin for Server Components is pretty broken right now
because, now that `.client.js` convention is gone, it ends up adding
every single JS file it can find (including `node_modules`) as a
potential async dependency. Instead, it should only look for files with
the `'use client'` directive.
The ideal way is to implement this by bundling the RSC graph first.
Then, we would know which `'use client'` files were actually discovered
— and so there would be no point to scanning the disk for them. That's
how Next.js bundler does it.
We're not doing that here.
This toy plugin is very simple, and I'm not planning to do heavy
lifting. I'm just bringing it up to date with the convention. The change
is that we now read every file we discover (alas), bail if it has no
`'use client'`, and parse it if it does (to verify it's actually used as
a directive). I've changed to use `acorn-loose` because it's forgiving
of JSX (and likely TypeScript/Flow). Otherwise, this wouldn't work on
uncompiled source.
## Test plan
Verified I can get our initial Server Components Demo running after this
change. Previously, it would get stuck compiling and then emit thousands
of errors.
Also confirmed the fixture still works. (It doesn’t work correctly on
the first load after dev server starts, but that’s already the case on
main so seems unrelated.)
2023-03-31 05:05:03 +08:00
|
|
|
const acorn = require('acorn-loose');
|
2023-02-08 01:09:29 +08:00
|
|
|
|
2020-11-17 12:46:27 +08:00
|
|
|
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');
|
2023-02-10 08:45:05 +08:00
|
|
|
const SERVER_REFERENCE = Symbol.for('react.server.reference');
|
2022-08-26 00:27:30 +08:00
|
|
|
const PROMISE_PROTOTYPE = Promise.prototype;
|
|
|
|
|
|
2023-02-10 08:45:05 +08:00
|
|
|
// Patch bind on the server to ensure that this creates another
|
|
|
|
|
// bound server reference with the additional arguments.
|
|
|
|
|
const originalBind = Function.prototype.bind;
|
|
|
|
|
/*eslint-disable no-extend-native */
|
|
|
|
|
Function.prototype.bind = (function bind(this: any, self: any) {
|
|
|
|
|
// $FlowFixMe[unsupported-syntax]
|
|
|
|
|
const newFn = originalBind.apply(this, arguments);
|
|
|
|
|
if (this.$$typeof === SERVER_REFERENCE) {
|
|
|
|
|
// $FlowFixMe[method-unbinding]
|
|
|
|
|
const args = Array.prototype.slice.call(arguments, 1);
|
|
|
|
|
newFn.$$typeof = SERVER_REFERENCE;
|
2023-03-05 08:51:34 +08:00
|
|
|
newFn.$$id = this.$$id;
|
2023-03-09 12:45:55 +08:00
|
|
|
newFn.$$bound = this.$$bound ? this.$$bound.concat(args) : args;
|
2023-02-10 08:45:05 +08:00
|
|
|
}
|
|
|
|
|
return newFn;
|
|
|
|
|
}: any);
|
|
|
|
|
|
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;
|
2023-03-05 08:51:34 +08:00
|
|
|
case '$$id':
|
|
|
|
|
return target.$$id;
|
|
|
|
|
case '$$async':
|
|
|
|
|
return target.$$async;
|
2023-01-28 09:08:26 +08:00
|
|
|
case 'name':
|
|
|
|
|
return target.name;
|
2023-02-11 00:23:48 +08:00
|
|
|
case 'displayName':
|
|
|
|
|
return undefined;
|
2023-01-28 09:08:26 +08:00
|
|
|
// 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.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-03-05 08:51:34 +08:00
|
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
|
|
|
const expression = String(target.name) + '.' + String(name);
|
2023-01-28 09:08:26 +08:00
|
|
|
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-02-10 06:07:39 +08:00
|
|
|
get: function (
|
|
|
|
|
target: Function,
|
|
|
|
|
name: string,
|
|
|
|
|
receiver: Proxy<Function>,
|
|
|
|
|
): $FlowFixMe {
|
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':
|
|
|
|
|
return target.$$typeof;
|
2023-03-05 08:51:34 +08:00
|
|
|
case '$$id':
|
|
|
|
|
return target.$$id;
|
|
|
|
|
case '$$async':
|
|
|
|
|
return target.$$async;
|
2020-12-01 06:37:27 +08:00
|
|
|
case 'name':
|
|
|
|
|
return target.name;
|
|
|
|
|
// 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-03-05 08:51:34 +08:00
|
|
|
const moduleId = target.$$id;
|
2023-01-28 09:08:26 +08:00
|
|
|
target.default = Object.defineProperties(
|
2023-01-31 21:25:05 +08:00
|
|
|
(function () {
|
2023-01-28 09:08:26 +08:00
|
|
|
throw new Error(
|
2023-02-06 23:50:52 +08:00
|
|
|
`Attempted to call the default export of ${moduleId} from the server ` +
|
2023-01-28 09:08:26 +08:00
|
|
|
`but it's on the client. It's not possible to invoke a client function from ` +
|
2023-02-06 23:50:52 +08:00
|
|
|
`the server, it can only be rendered as a Component or passed to props of a ` +
|
2023-01-28 09:08:26 +08:00
|
|
|
`Client Component.`,
|
|
|
|
|
);
|
|
|
|
|
}: any),
|
|
|
|
|
{
|
2023-03-05 08:51:34 +08:00
|
|
|
$$typeof: {value: CLIENT_REFERENCE},
|
2023-01-28 09:08:26 +08:00
|
|
|
// This a placeholder value that tells the client to conditionally use the
|
|
|
|
|
// whole object or just the default export.
|
2023-03-05 08:51:34 +08:00
|
|
|
$$id: {value: target.$$id + '#'},
|
|
|
|
|
$$async: {value: target.$$async},
|
2023-01-28 09:08:26 +08:00
|
|
|
},
|
|
|
|
|
);
|
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;
|
|
|
|
|
}
|
2023-03-05 08:51:34 +08:00
|
|
|
if (!target.$$async) {
|
2022-08-26 00:27:30 +08:00
|
|
|
// 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), {
|
|
|
|
|
$$typeof: {value: CLIENT_REFERENCE},
|
2023-03-05 08:51:34 +08:00
|
|
|
$$id: {value: target.$$id},
|
|
|
|
|
$$async: {value: true},
|
2023-02-03 04:30:59 +08:00
|
|
|
});
|
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;
|
|
|
|
|
|
|
|
|
|
const then = (target.then = Object.defineProperties(
|
|
|
|
|
(function then(resolve, reject: any) {
|
|
|
|
|
// Expose to React.
|
2023-02-10 06:07:39 +08:00
|
|
|
return Promise.resolve(resolve(proxy));
|
2023-01-28 09:08:26 +08:00
|
|
|
}: 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.
|
|
|
|
|
{
|
|
|
|
|
$$typeof: {value: CLIENT_REFERENCE},
|
2023-03-05 08:51:34 +08:00
|
|
|
$$id: {value: target.$$id + '#then'},
|
|
|
|
|
$$async: {value: false},
|
2023-01-28 09:08:26 +08:00
|
|
|
},
|
|
|
|
|
));
|
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},
|
2023-03-05 08:51:34 +08:00
|
|
|
$$id: {value: target.$$id + '#' + name},
|
|
|
|
|
$$async: {value: target.$$async},
|
2023-01-28 09:08:26 +08:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
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-02-10 06:07:39 +08:00
|
|
|
set: function (): empty {
|
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-02-08 01:09:29 +08:00
|
|
|
const originalCompile = Module.prototype._compile;
|
|
|
|
|
|
|
|
|
|
// $FlowFixMe[prop-missing] found when upgrading Flow
|
|
|
|
|
Module.prototype._compile = function (
|
|
|
|
|
this: any,
|
|
|
|
|
content: string,
|
|
|
|
|
filename: string,
|
|
|
|
|
): void {
|
|
|
|
|
// Do a quick check for the exact string. If it doesn't exist, don't
|
|
|
|
|
// bother parsing.
|
2023-02-10 08:45:05 +08:00
|
|
|
if (
|
|
|
|
|
content.indexOf('use client') === -1 &&
|
|
|
|
|
content.indexOf('use server') === -1
|
|
|
|
|
) {
|
2023-02-08 01:09:29 +08:00
|
|
|
return originalCompile.apply(this, arguments);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 08:24:16 +08:00
|
|
|
let body;
|
|
|
|
|
try {
|
|
|
|
|
body = acorn.parse(content, {
|
|
|
|
|
ecmaVersion: '2024',
|
|
|
|
|
sourceType: 'source',
|
|
|
|
|
}).body;
|
|
|
|
|
} catch (x) {
|
|
|
|
|
// eslint-disable-next-line react-internal/no-production-logging
|
|
|
|
|
console.error('Error parsing %s %s', url, x.message);
|
|
|
|
|
return originalCompile.apply(this, arguments);
|
|
|
|
|
}
|
2023-02-08 01:09:29 +08:00
|
|
|
|
|
|
|
|
let useClient = false;
|
2023-02-10 08:45:05 +08:00
|
|
|
let useServer = false;
|
2023-02-08 01:09:29 +08:00
|
|
|
for (let i = 0; i < body.length; i++) {
|
|
|
|
|
const node = body[i];
|
|
|
|
|
if (node.type !== 'ExpressionStatement' || !node.directive) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (node.directive === 'use client') {
|
|
|
|
|
useClient = true;
|
2023-02-10 08:45:05 +08:00
|
|
|
}
|
|
|
|
|
if (node.directive === 'use server') {
|
|
|
|
|
useServer = true;
|
2023-02-08 01:09:29 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-10 08:45:05 +08:00
|
|
|
if (!useClient && !useServer) {
|
2023-02-08 01:09:29 +08:00
|
|
|
return originalCompile.apply(this, arguments);
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-10 08:45:05 +08:00
|
|
|
if (useClient && useServer) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
'Cannot have both "use client" and "use server" directives in the same file.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (useClient) {
|
|
|
|
|
const moduleId: string = (url.pathToFileURL(filename).href: any);
|
|
|
|
|
const clientReference = Object.defineProperties(({}: any), {
|
|
|
|
|
$$typeof: {value: CLIENT_REFERENCE},
|
2023-03-05 08:51:34 +08:00
|
|
|
// Represents the whole Module object instead of a particular import.
|
|
|
|
|
$$id: {value: moduleId},
|
|
|
|
|
$$async: {value: false},
|
2023-02-10 08:45:05 +08:00
|
|
|
});
|
|
|
|
|
// $FlowFixMe[incompatible-call] found when upgrading Flow
|
|
|
|
|
this.exports = new Proxy(clientReference, proxyHandlers);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (useServer) {
|
|
|
|
|
originalCompile.apply(this, arguments);
|
|
|
|
|
|
|
|
|
|
const moduleId: string = (url.pathToFileURL(filename).href: any);
|
|
|
|
|
|
|
|
|
|
const exports = this.exports;
|
|
|
|
|
|
|
|
|
|
// This module is imported server to server, but opts in to exposing functions by
|
|
|
|
|
// reference. If there are any functions in the export.
|
|
|
|
|
if (typeof exports === 'function') {
|
|
|
|
|
// The module exports a function directly,
|
|
|
|
|
Object.defineProperties((exports: any), {
|
|
|
|
|
$$typeof: {value: SERVER_REFERENCE},
|
2023-03-05 08:51:34 +08:00
|
|
|
// Represents the whole Module object instead of a particular import.
|
|
|
|
|
$$id: {value: moduleId},
|
2023-03-09 12:45:55 +08:00
|
|
|
$$bound: {value: null},
|
2023-02-10 08:45:05 +08:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
const keys = Object.keys(exports);
|
|
|
|
|
for (let i = 0; i < keys.length; i++) {
|
|
|
|
|
const key = keys[i];
|
|
|
|
|
const value = exports[keys[i]];
|
|
|
|
|
if (typeof value === 'function') {
|
|
|
|
|
Object.defineProperties((value: any), {
|
|
|
|
|
$$typeof: {value: SERVER_REFERENCE},
|
2023-03-05 08:51:34 +08:00
|
|
|
$$id: {value: moduleId + '#' + key},
|
2023-03-09 12:45:55 +08:00
|
|
|
$$bound: {value: null},
|
2023-02-10 08:45:05 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-01 06:25:56 +08:00
|
|
|
};
|
2020-11-17 12:46:27 +08:00
|
|
|
};
|