116 lines
6.4 KiB
JavaScript
116 lines
6.4 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.environmentRestrictedReactAPIsPlugin = environmentRestrictedReactAPIsPlugin;
|
|
const INVALID_SERVER_REACT_DOM_APIS = [
|
|
'findDOMNode',
|
|
'flushSync',
|
|
'unstable_batchedUpdates',
|
|
'useFormStatus',
|
|
'useFormState',
|
|
];
|
|
// From the React docs: https://github.com/vercel/next.js/blob/d43a387d271263f2c1c4da6b9db826e382fc489c/packages/next-swc/crates/next-custom-transforms/src/transforms/react_server_components.rs#L665-L681
|
|
const INVALID_SERVER_REACT_APIS = [
|
|
'Component',
|
|
'createContext',
|
|
'createFactory',
|
|
'PureComponent',
|
|
'useDeferredValue',
|
|
'useEffect',
|
|
'useImperativeHandle',
|
|
'useInsertionEffect',
|
|
'useLayoutEffect',
|
|
'useReducer',
|
|
'useRef',
|
|
'useState',
|
|
'useSyncExternalStore',
|
|
'useTransition',
|
|
'useOptimistic',
|
|
];
|
|
function isNodeModule(path) {
|
|
return path != null && /[\\/]node_modules[\\/]/.test(path);
|
|
}
|
|
// Restricts imports from `react` and `react-dom` when using React Server Components.
|
|
const FORBIDDEN_IMPORTS = {
|
|
react: INVALID_SERVER_REACT_APIS,
|
|
'react-dom': INVALID_SERVER_REACT_DOM_APIS,
|
|
};
|
|
function environmentRestrictedReactAPIsPlugin(api) {
|
|
const { types: t } = api;
|
|
return {
|
|
name: 'expo-environment-restricted-react-api-plugin',
|
|
visitor: {
|
|
ImportDeclaration(path, state) {
|
|
// Skip node_modules
|
|
if (isNodeModule(state.file.opts.filename)) {
|
|
return;
|
|
}
|
|
const sourceValue = path.node.source.value;
|
|
const forbiddenList = FORBIDDEN_IMPORTS[sourceValue];
|
|
if (forbiddenList) {
|
|
path.node.specifiers.forEach((specifier) => {
|
|
if (t.isImportSpecifier(specifier)) {
|
|
const importName = t.isStringLiteral(specifier.imported)
|
|
? specifier.imported.value
|
|
: specifier.imported.name;
|
|
// Check for both named and namespace imports
|
|
const isForbidden = forbiddenList.includes(importName);
|
|
if (isForbidden) {
|
|
if (['Component', 'PureComponent'].includes(importName)) {
|
|
// Add special handling for `Component` since it is different to a function API.
|
|
throw path.buildCodeFrameError(`Client-only "${sourceValue}" API "${importName}" cannot be imported in a React server component. Add the "use client" directive to the top of this file or one of the parent files to enable running this stateful code on a user's device.`);
|
|
}
|
|
else {
|
|
const forbiddenImports = path.scope.getData('forbiddenImports') ?? new Map();
|
|
if (!forbiddenImports.has(sourceValue))
|
|
forbiddenImports.set(sourceValue, new Set());
|
|
forbiddenImports.get(sourceValue).add(importName);
|
|
path.scope.setData('forbiddenImports', forbiddenImports);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
const importName = t.isStringLiteral(specifier.local)
|
|
? specifier.local
|
|
: specifier.local.name;
|
|
// Save namespace import for later checks in MemberExpression
|
|
path.scope.setData('importedNamespace', { [importName]: sourceValue });
|
|
}
|
|
});
|
|
}
|
|
},
|
|
// Match against `var _useState = useState(0),`
|
|
VariableDeclarator(path) {
|
|
const importedSpecifiers = path.scope.getData('forbiddenImports');
|
|
if (!importedSpecifiers)
|
|
return;
|
|
importedSpecifiers.forEach((forbiddenApis, importName) => {
|
|
if (t.isCallExpression(path.node.init) && t.isIdentifier(path.node.init.callee)) {
|
|
if (forbiddenApis.has(path.node.init.callee.name)) {
|
|
throw path.buildCodeFrameError(`Client-only "useState" API cannot be used in a React server component. Add the "use client" directive to the top of this file or one of the parent files to enable running this stateful code on a user's device.`);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
MemberExpression(path) {
|
|
const importedNamespaces = path.scope.getData('importedNamespace') || {};
|
|
Object.keys(importedNamespaces).forEach((namespace) => {
|
|
const library = importedNamespaces[namespace];
|
|
const forbiddenList = FORBIDDEN_IMPORTS[library];
|
|
const objectName = t.isIdentifier(path.node.object) ? path.node.object.name : null;
|
|
if (objectName === namespace &&
|
|
forbiddenList &&
|
|
t.isIdentifier(path.node.property) &&
|
|
forbiddenList.includes(path.node.property.name)) {
|
|
// Throw a special error for class components since it's not always clear why they cannot be used in RSC.
|
|
// e.g. https://x.com/Baconbrix/status/1749223042440392806?s=20
|
|
if (path.node.property.name === 'Component') {
|
|
throw path.buildCodeFrameError(`Class components cannot be used in a React server component due to their ability to contain stateful and interactive APIs that cannot be statically evaluated in non-interactive environments such as a server or at build-time. Migrate to a function component, or add the "use client" directive to the top of this file or one of the parent files to render this class component on a user's device.`);
|
|
}
|
|
throw path.buildCodeFrameError(`Client-only "${namespace}" API "${path.node.property.name}" cannot be used in a React server component. Add the "use client" directive to the top of this file or one of the parent files to enable running this stateful code on a user's device.`);
|
|
}
|
|
});
|
|
},
|
|
},
|
|
};
|
|
}
|