feat: Use Vite import in prod for client components
This commit is contained in:
parent
8392037e68
commit
8279d4c558
|
@ -1,7 +1,6 @@
|
|||
import {createElement, Fragment, ReactElement} from 'react';
|
||||
import {wrapPromise} from '../../utilities';
|
||||
import importDevClientComponent from './import-dev';
|
||||
import {isDev} from './is-dev';
|
||||
import importClientComponent from './client-imports';
|
||||
|
||||
const cache = new Map();
|
||||
const moduleCache = new Map();
|
||||
|
@ -151,14 +150,7 @@ async function eagerLoadModules(manifest: WireManifest) {
|
|||
return moduleCache.get(module.id);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const mod = isDev
|
||||
? // In dev, use Vite import strategy to make sure
|
||||
// files are not duplicated in browser.
|
||||
await importDevClientComponent(module.id)
|
||||
: // In prod, RSC provides the asset URL
|
||||
// so it can be downloaded directly.
|
||||
await import(/* @vite-ignore */ module.id);
|
||||
const mod = await importClientComponent(module.id);
|
||||
|
||||
moduleCache.set(module.id, mod);
|
||||
return mod;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export const isDev = false;
|
|
@ -2,8 +2,7 @@ import React, {Suspense} from 'react';
|
|||
import {screen, render, waitFor} from '@testing-library/react';
|
||||
import {convertHydrationResponseToReactComponents} from '../Cache.client';
|
||||
|
||||
jest.mock('../is-dev');
|
||||
jest.mock('../import-dev');
|
||||
jest.mock('../client-imports');
|
||||
|
||||
it('handles DOM elements', async () => {
|
||||
const tuples = [['$', 'div', null, {children: 'hello'}]];
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
// Move this to a different file so it can be mocked in Jest
|
||||
// until import.meta is supported.
|
||||
|
||||
// @ts-ignore
|
||||
export const isDev = import.meta.env.DEV;
|
|
@ -1,18 +1,10 @@
|
|||
import {proxyClientComponent} from '../server-components';
|
||||
|
||||
const root = '/path/to/';
|
||||
const src = `export default function() {}`;
|
||||
const FAKE_FILE_PATH = 'full/path/to/Counter.client.jsx';
|
||||
const getFileFromClientManifest = async (id: string) => FAKE_FILE_PATH;
|
||||
|
||||
it('wraps default exports for dev', async () => {
|
||||
expect(
|
||||
await proxyClientComponent({
|
||||
id: '/path/to/Counter.client.jsx',
|
||||
getFileFromClientManifest,
|
||||
root,
|
||||
src,
|
||||
isBuild: false,
|
||||
src: `export default function() {}`,
|
||||
})
|
||||
).toBe(`import {wrapInClientMarker} from '@shopify/hydrogen/marker';
|
||||
import Counter from '/path/to/Counter.client.jsx?no-proxy';
|
||||
|
@ -21,30 +13,11 @@ export default wrapInClientMarker({ name: 'Counter', id: '/path/to/Counter.clien
|
|||
`);
|
||||
});
|
||||
|
||||
it('wraps default exports for build', async () => {
|
||||
expect(
|
||||
await proxyClientComponent({
|
||||
id: '/path/to/Counter.client.jsx',
|
||||
getFileFromClientManifest,
|
||||
root,
|
||||
src,
|
||||
isBuild: true,
|
||||
})
|
||||
).toBe(`import {wrapInClientMarker} from '@shopify/hydrogen/marker';
|
||||
import Counter from '/path/to/Counter.client.jsx?no-proxy';
|
||||
|
||||
export default wrapInClientMarker({ name: 'Counter', id: '/${FAKE_FILE_PATH}', component: Counter, named: false });
|
||||
`);
|
||||
});
|
||||
|
||||
it('wraps named exports', async () => {
|
||||
expect(
|
||||
await proxyClientComponent({
|
||||
id: '/path/to/Counter.client.jsx',
|
||||
getFileFromClientManifest,
|
||||
root,
|
||||
src: `export function Counter() {}\nexport const Clicker = () => {};`,
|
||||
isBuild: false,
|
||||
})
|
||||
).toBe(`import {wrapInClientMarker} from '@shopify/hydrogen/marker';
|
||||
import * as namedImports from '/path/to/Counter.client.jsx?no-proxy';
|
||||
|
@ -58,10 +31,7 @@ it('combines default and named exports', async () => {
|
|||
expect(
|
||||
await proxyClientComponent({
|
||||
id: '/path/to/Counter.client.jsx',
|
||||
getFileFromClientManifest,
|
||||
root,
|
||||
src: `export default function() {}\nexport const Clicker = () => {};`,
|
||||
isBuild: false,
|
||||
})
|
||||
).toBe(`import {wrapInClientMarker} from '@shopify/hydrogen/marker';
|
||||
import Counter, * as namedImports from '/path/to/Counter.client.jsx?no-proxy';
|
||||
|
@ -75,10 +45,7 @@ it('does not wrap non-component exports', async () => {
|
|||
expect(
|
||||
await proxyClientComponent({
|
||||
id: '/path/to/Counter.client.jsx',
|
||||
getFileFromClientManifest,
|
||||
root,
|
||||
src: `export default function() {}\nexport const MyFragment = 'fragment myFragment on MyQuery { id }';`,
|
||||
isBuild: false,
|
||||
})
|
||||
).toBe(`import {wrapInClientMarker} from '@shopify/hydrogen/marker';
|
||||
import Counter from '/path/to/Counter.client.jsx?no-proxy';
|
||||
|
@ -92,10 +59,7 @@ it('can export non-component only', async () => {
|
|||
expect(
|
||||
await proxyClientComponent({
|
||||
id: '/path/to/Counter.client.jsx',
|
||||
getFileFromClientManifest,
|
||||
root,
|
||||
src: `export const LocalizationContext = {}; export const useMyStuff = () => {}; export const MY_CONSTANT = 42;`,
|
||||
isBuild: false,
|
||||
})
|
||||
).toBe(`export * from '/path/to/Counter.client.jsx?no-proxy';\n`);
|
||||
});
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import type {Plugin, ResolvedConfig} from 'vite';
|
||||
import path from 'path';
|
||||
import {promises as fs} from 'fs';
|
||||
import glob from 'fast-glob';
|
||||
import {proxyClientComponent} from '../server-components';
|
||||
|
||||
export default () => {
|
||||
let config: ResolvedConfig;
|
||||
|
||||
let clientManifest: any;
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-react-server-components-shim',
|
||||
|
||||
|
@ -79,22 +76,12 @@ export default () => {
|
|||
}
|
||||
},
|
||||
|
||||
async load(id, options) {
|
||||
if (config.command === 'build' && id.includes('/Hydration/import-dev')) {
|
||||
// Manual tree-shaking
|
||||
return {code: 'export default null', moduleSideEffects: false};
|
||||
}
|
||||
|
||||
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 await proxyClientComponent({
|
||||
id,
|
||||
isBuild: config.command === 'build',
|
||||
getFileFromClientManifest,
|
||||
root: config.root,
|
||||
});
|
||||
return proxyClientComponent({id});
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -106,12 +93,12 @@ export default () => {
|
|||
* 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 (import-dev.ts) in
|
||||
* 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/import-dev')) {
|
||||
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');
|
||||
|
@ -159,45 +146,4 @@ export default () => {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getFileFromClientManifest(manifestId: string) {
|
||||
const manifest = await getClientManifest();
|
||||
|
||||
const fileName = '/' + manifestId.split('/').pop()!;
|
||||
const matchingKey = Object.keys(manifest).find((key) =>
|
||||
key.endsWith(fileName)
|
||||
);
|
||||
|
||||
if (!matchingKey) {
|
||||
throw new Error(
|
||||
`Could not find a matching entry in the manifest for: ${manifestId}`
|
||||
);
|
||||
}
|
||||
|
||||
return manifest[matchingKey].file;
|
||||
}
|
||||
|
||||
async function getClientManifest() {
|
||||
if (config.command !== 'build') {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (clientManifest) return clientManifest;
|
||||
|
||||
try {
|
||||
const manifest = JSON.parse(
|
||||
await fs.readFile(
|
||||
path.resolve(config.root, './dist/client/manifest.json'),
|
||||
'utf-8'
|
||||
)
|
||||
);
|
||||
|
||||
clientManifest = manifest;
|
||||
|
||||
return manifest;
|
||||
} catch (e) {
|
||||
console.error(`Failed to load client manifest:`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,40 +6,22 @@ import MagicString from 'magic-string';
|
|||
export async function proxyClientComponent({
|
||||
id,
|
||||
src,
|
||||
isBuild,
|
||||
getFileFromClientManifest,
|
||||
root,
|
||||
}: {
|
||||
id: string;
|
||||
src?: string;
|
||||
isBuild?: boolean;
|
||||
getFileFromClientManifest: (id: string) => Promise<string>;
|
||||
root: string;
|
||||
}) {
|
||||
const [rawId] = id.split('?');
|
||||
const manifestId = rawId.replace(root, '');
|
||||
const defaultComponentName = rawId.split('/').pop()?.split('.').shift()!;
|
||||
const defaultComponentName = id.split('/').pop()?.split('.').shift()!;
|
||||
|
||||
// Modify the import ID to avoid infinite wraps
|
||||
const importFrom = `${rawId}?no-proxy`;
|
||||
const importFrom = `${id}?no-proxy`;
|
||||
|
||||
await init;
|
||||
|
||||
/**
|
||||
* Determine the id of the chunk to be imported. If we're building
|
||||
* the production bundle, we need to reference the chunk generated
|
||||
* during the client manifest. Otherwise, we can pass the normalizedId
|
||||
* and Vite's dev server will load it as expected.
|
||||
*/
|
||||
const assetId = isBuild
|
||||
? '/' + (await getFileFromClientManifest(manifestId))
|
||||
: rawId;
|
||||
|
||||
if (!src) {
|
||||
src = await fs.readFile(rawId, 'utf-8');
|
||||
src = await fs.readFile(id, 'utf-8');
|
||||
}
|
||||
|
||||
const {code} = await transformWithEsbuild(src, rawId);
|
||||
const {code} = await transformWithEsbuild(src, id);
|
||||
const [, exportStatements] = parse(code);
|
||||
const hasDefaultExport = exportStatements.includes('default');
|
||||
|
||||
|
@ -93,7 +75,7 @@ export async function proxyClientComponent({
|
|||
if (hasDefaultExport) {
|
||||
s.append(
|
||||
generateComponentExport({
|
||||
id: assetId,
|
||||
id,
|
||||
componentName: defaultComponentName,
|
||||
isDefault: true,
|
||||
})
|
||||
|
@ -103,7 +85,7 @@ export async function proxyClientComponent({
|
|||
namedImports.components.forEach((name) =>
|
||||
s.append(
|
||||
generateComponentExport({
|
||||
id: assetId,
|
||||
id,
|
||||
componentName: name,
|
||||
isDefault: false,
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue