feat: Use Vite import in prod for client components

This commit is contained in:
Fran Dios 2021-11-08 16:02:56 +09:00
parent 8392037e68
commit 8279d4c558
9 changed files with 14 additions and 137 deletions

View file

@ -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;

View file

@ -1 +0,0 @@
export const isDev = false;

View file

@ -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'}]];

View file

@ -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;

View file

@ -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`);
});

View file

@ -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);
}
}
};

View file

@ -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,
})