const acorn = require('acorn');
const url = require('url');
const Module = require('module');
module.exports = function register() {
const CLIENT_REFERENCE = Symbol.for('react.client.reference');
const SERVER_REFERENCE = Symbol.for('react.server.reference');
const PROMISE_PROTOTYPE = Promise.prototype;
const originalBind = Function.prototype.bind;
Function.prototype.bind = (function bind(this: any, self: any) {
const newFn = originalBind.apply(this, arguments);
if (this.$$typeof === SERVER_REFERENCE) {
const args = Array.prototype.slice.call(arguments, 1);
newFn.$$typeof = SERVER_REFERENCE;
newFn.$$id = this.$$id;
newFn.$$bound = this.$$bound ? this.$$bound.concat(args) : args;
}
return newFn;
}: any);
const deepProxyHandlers = {
get: function (target: Function, name: string, receiver: Proxy<Function>) {
switch (name) {
case '$$typeof':
return target.$$typeof;
case '$$id':
return target.$$id;
case '$$async':
return target.$$async;
case 'name':
return target.name;
case 'displayName':
return undefined;
case 'defaultProps':
return undefined;
case 'toJSON':
return undefined;
case Symbol.toPrimitive:
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.`,
);
}
const 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.',
);
},
set: function () {
throw new Error('Cannot assign to a client module from a server module.');
},
};
const proxyHandlers = {
get: function (
target: Function,
name: string,
receiver: Proxy<Function>,
): $FlowFixMe {
switch (name) {
case '$$typeof':
return target.$$typeof;
case '$$id':
return target.$$id;
case '$$async':
return target.$$async;
case 'name':
return target.name;
case 'defaultProps':
return undefined;
case 'toJSON':
return undefined;
case Symbol.toPrimitive:
return Object.prototype[Symbol.toPrimitive];
case '__esModule':
const moduleId = target.$$id;
target.default = Object.defineProperties(
(function () {
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),
{
$$typeof: {value: CLIENT_REFERENCE},
$$id: {value: target.$$id + '#'},
$$async: {value: target.$$async},
},
);
return true;
case 'then':
if (target.then) {
return target.then;
}
if (!target.$$async) {
const clientReference = Object.defineProperties(({}: any), {
$$typeof: {value: CLIENT_REFERENCE},
$$id: {value: target.$$id},
$$async: {value: true},
});
const proxy = new Proxy(clientReference, proxyHandlers);
target.status = 'fulfilled';
target.value = proxy;
const then = (target.then = Object.defineProperties(
(function then(resolve, reject: any) {
return Promise.resolve(resolve(proxy));
}: any),
{
$$typeof: {value: CLIENT_REFERENCE},
$$id: {value: target.$$id + '#then'},
$$async: {value: false},
},
));
return then;
} else {
return undefined;
}
}
let cachedReference = target[name];
if (!cachedReference) {
const reference = Object.defineProperties(
(function () {
throw new Error(
`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},
$$id: {value: target.$$id + '#' + name},
$$async: {value: target.$$async},
},
);
cachedReference = target[name] = new Proxy(
reference,
deepProxyHandlers,
);
}
return cachedReference;
},
getPrototypeOf(target: Function): Object {
return PROMISE_PROTOTYPE;
},
set: function (): empty {
throw new Error('Cannot assign to a client module from a server module.');
},
};
const originalCompile = Module.prototype._compile;
Module.prototype._compile = function (
this: any,
content: string,
filename: string,
): void {
if (
content.indexOf('use client') === -1 &&
content.indexOf('use server') === -1
) {
return originalCompile.apply(this, arguments);
}
let body;
try {
body = acorn.parse(content, {
ecmaVersion: '2024',
sourceType: 'source',
}).body;
} catch (x) {
console.error('Error parsing %s %s', url, x.message);
return originalCompile.apply(this, arguments);
}
let useClient = false;
let useServer = false;
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;
}
if (node.directive === 'use server') {
useServer = true;
}
}
if (!useClient && !useServer) {
return originalCompile.apply(this, arguments);
}
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},
$$id: {value: moduleId},
$$async: {value: false},
});
this.exports = new Proxy(clientReference, proxyHandlers);
}
if (useServer) {
originalCompile.apply(this, arguments);
const moduleId: string = (url.pathToFileURL(filename).href: any);
const exports = this.exports;
if (typeof exports === 'function') {
Object.defineProperties((exports: any), {
$$typeof: {value: SERVER_REFERENCE},
$$id: {value: moduleId},
$$bound: {value: null},
});
} 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},
$$id: {value: moduleId + '#' + key},
$$bound: {value: null},
});
}
}
}
}
};
};