hydrogen/packages/hydrogen/src/framework/plugins/vite-plugin-react-server-components-shim.ts

127 lines
4.3 KiB
TypeScript

import type {Plugin, ResolvedConfig} from 'vite';
import path from 'path';
import {proxyClientComponent} from '../server-components';
export default () => {
let config: ResolvedConfig;
return {
name: 'vite-plugin-react-server-components-shim',
enforce: 'pre',
configResolved(_config) {
config = _config;
},
async resolveId(source, importer) {
if (!importer) return null;
/**
* Throw errors when non-Server Components try to load Server Components.
*/
if (
/\.server(\.(j|t)sx?)?$/.test(source) &&
!/\.server\.(j|t)sx?$/.test(importer) &&
// Ignore entrypoints, index re-exports, ClientMarker, handle-worker-event
!/(entry-server\.(j|t)sx?|index\.(html|js)|ClientMarker\.js|handle-worker-event\.js)(\?v=.*)?$/.test(
importer
)
) {
throw new Error(
`Cannot import ${source} from "${importer}". ` +
'By convention, Server Components can only be imported from other Server Component files. ' +
'That way nobody accidentally sends these to the client by indirectly importing it.'
);
}
/**
* Throw errors when Client Components try to load Hydrogen components from the
* server-only entrypoint.
*/
if (
/@shopify\/hydrogen$/.test(source) &&
/\.client\.(j|t)sx?$/.test(importer)
) {
throw new Error(
`Cannot import @shopify/hydrogen from "${importer}". ` +
'When using Hydrogen components within Client Components, use the `@shopify/hydrogen/client` entrypoint instead.'
);
}
},
load(id, options) {
if (!isSSR(options)) return null;
// Wrapped components won't match this becase they end in ?no-proxy
if (/\.client\.[jt]sx?$/.test(id)) {
return proxyClientComponent({id});
}
return null;
},
transform(code, id) {
/**
* In order to allow dynamic component imports from RSC, we use Vite's import.meta.glob.
* This replaces the glob import path placeholders in importer-dev.ts with resolved paths
* to all client components (both user and Hydrogen components).
*
* NOTE: Glob import paths MUST be relative to the importer file (client-imports.ts) in
* order to get the `?v=xxx` querystring from Vite added to the import URL.
* If the paths are relative to the root instead, Vite won't add the querystring
* and we will have duplicated files in the browser (with duplicated contexts, etc).
*/
if (id.includes('/Hydration/client-imports')) {
// eslint-disable-next-line node/no-missing-require
const hydrogenPath = path.dirname(require.resolve('@shopify/hydrogen'));
const importerPath = path.join(hydrogenPath, 'framework', 'Hydration');
const importerToRootPath = path.relative(importerPath, config.root);
const [importerToRootNested] =
importerToRootPath.match(/(\.\.\/)+(\.\.)?/) || [];
const userPrefix = path.normalize(
path.join(
importerPath,
importerToRootNested.replace(/\/?$/, path.sep)
)
);
const userGlob = path.join(
importerToRootPath,
'src',
'**/*.client.[jt]sx'
);
const libPrefix = hydrogenPath + path.sep;
const libGlob = path.join(
path.relative(importerPath, hydrogenPath),
'components',
'**/*.client.js'
);
return code
.replace('__USER_COMPONENTS_PREFIX__', userPrefix)
.replace('__USER_COMPONENTS_GLOB__', userGlob)
.replace('__LIB_COMPONENTS_PREFIX__', libPrefix)
.replace('__LIB_COMPONENTS_GLOB__', libGlob);
}
},
} as Plugin;
// Mitigation for upcoming minor Vite update
// https://github.com/vitejs/vite/pull/5253
// TO-DO: When the vite package is updated with the above Vite PR,
// clean up this function and treat `options` param as objects
// from this point forward
// Timeline: Targetting for Vite 2.7
function isSSR(options: undefined | boolean | {ssr: boolean}): boolean {
if (typeof options === 'boolean') {
return options;
}
if (typeof options === 'object') {
return !!options.ssr;
}
return false;
}
};