Skip to content

Commit

Permalink
Merge pull request #20698 from Dschungelabenteuer/fix-hmr-vite
Browse files Browse the repository at this point in the history
Vite: Replace vite-plugin-externals with custom plugin
  • Loading branch information
ndelangen committed Jan 27, 2023
2 parents 62ebc82 + a602c6a commit 95f4adc
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 27 deletions.
3 changes: 1 addition & 2 deletions code/lib/builder-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@
"glob-promise": "^4.2.0",
"magic-string": "^0.26.1",
"rollup": "^2.25.0 || ^3.3.0",
"slash": "^3.0.0",
"vite-plugin-externals": "^0.5.1"
"slash": "^3.0.0"
},
"devDependencies": {
"@types/express": "^4.17.13",
Expand Down
43 changes: 43 additions & 0 deletions code/lib/builder-vite/src/plugins/external-globals-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { rewriteImport } from './external-globals-plugin';

const packageName = '@storybook/package';
const globals = { [packageName]: '_STORYBOOK_PACKAGE_' };

const cases = [
{
globals,
packageName,
input: `import { Rain, Jour as Day, Nuit as Night, Sun } from "${packageName}"`,
output: `const { Rain, Jour: Day, Nuit: Night, Sun } = ${globals[packageName]}`,
},
{
globals,
packageName,
input: `import * as Foo from "${packageName}"`,
output: `const Foo = ${globals[packageName]}`,
},
{
globals,
packageName,
input: `import Foo from "${packageName}"`,
output: `const {default: Foo} = ${globals[packageName]}`,
},
{
globals,
packageName,
input: `import{Rain,Jour as Day,Nuit as Night,Sun}from'${packageName}'`,
output: `const {Rain,Jour: Day,Nuit: Night,Sun} =${globals[packageName]}`,
},
{
globals,
packageName,
input: `const { Afternoon } = await import('${packageName}')`,
output: `const { Afternoon } = ${globals[packageName]}`,
},
];

test('rewriteImport', () => {
cases.forEach(({ input, output, globals: caseGlobals, packageName: casePackage }) => {
expect(rewriteImport(input, caseGlobals, casePackage)).toStrictEqual(output);
});
});
119 changes: 119 additions & 0 deletions code/lib/builder-vite/src/plugins/external-globals-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { join } from 'node:path';
import { init, parse } from 'es-module-lexer';
import MagicString from 'magic-string';
import { emptyDir, ensureDir, ensureFile, writeFile } from 'fs-extra';
import { mergeAlias } from 'vite';
import type { Alias, Plugin } from 'vite';

const escapeKeys = (key: string) => key.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
const defaultImportRegExp = 'import ([^*{}]+) from';
const replacementMap = new Map([
['import ', 'const '],
['import{', 'const {'],
['* as ', ''],
[' as ', ': '],
[' from ', ' = '],
['}from', '} ='],
]);

/**
* This plugin swaps out imports of pre-bundled storybook preview modules for destructures from global
* variables that are added in runtime.mjs.
*
* For instance:
*
* ```js
* import { useMemo as useMemo2, useEffect as useEffect2 } from "@storybook/preview-api";
* ```
*
* becomes
*
* ```js
* const { useMemo: useMemo2, useEffect: useEffect2 } = __STORYBOOK_MODULE_PREVIEW_API__;
* ```
*
* It is based on existing plugins like https://github.com/crcong/vite-plugin-externals
* and https://github.com/eight04/rollup-plugin-external-globals, but simplified to meet our simple needs.
*/
export async function externalGlobalsPlugin(externals: Record<string, string>) {
await init;
return {
name: 'storybook:external-globals-plugin',
enforce: 'post',
// In dev (serve), we set up aliases to files that we write into node_modules/.cache.
async config(config, { command }) {
if (command !== 'serve') {
return undefined;
}
const newAlias = mergeAlias([], config.resolve?.alias) as Alias[];

const cachePath = join(process.cwd(), 'node_modules', '.cache', 'vite-plugin-externals');
await ensureDir(cachePath);
await emptyDir(cachePath);
await Promise.all(
(Object.keys(externals) as Array<keyof typeof externals>).map(async (externalKey) => {
const externalCachePath = join(cachePath, `${externalKey}.js`);
newAlias.push({ find: new RegExp(`^${externalKey}$`), replacement: externalCachePath });
await ensureFile(externalCachePath);
await writeFile(externalCachePath, `module.exports = ${externals[externalKey]};`);
})
);

return {
resolve: {
alias: newAlias,
},
};
},
// Replace imports with variables destructured from global scope
async transform(code: string, id: string) {
const globalsList = Object.keys(externals);
if (globalsList.every((glob) => !code.includes(glob))) return undefined;

const [imports] = parse(code);
const src = new MagicString(code);
imports.forEach(({ n: path, ss: startPosition, se: endPosition }) => {
const packageName = path;
if (packageName && globalsList.includes(packageName)) {
const importStatement = src.slice(startPosition, endPosition);
const transformedImport = rewriteImport(importStatement, externals, packageName);
src.update(startPosition, endPosition, transformedImport);
}
});

return {
code: src.toString(),
map: src.generateMap({
source: id,
includeContent: true,
hires: true,
}),
};
},
} satisfies Plugin;
}

function getDefaultImportReplacement(match: string) {
const matched = match.match(defaultImportRegExp);
return matched && `const {default: ${matched[1]}} =`;
}

function getSearchRegExp(packageName: string) {
const staticKeys = [...replacementMap.keys()].map(escapeKeys);
const packageNameLiteral = `.${packageName}.`;
const dynamicImportExpression = `await import\\(.${packageName}.\\)`;
const lookup = [defaultImportRegExp, ...staticKeys, packageNameLiteral, dynamicImportExpression];
return new RegExp(`(${lookup.join('|')})`, 'g');
}

export function rewriteImport(
importStatement: string,
globs: Record<string, string>,
packageName: string
): string {
const search = getSearchRegExp(packageName);
return importStatement.replace(
search,
(match) => replacementMap.get(match) ?? getDefaultImportReplacement(match) ?? globs[packageName]
);
}
1 change: 1 addition & 0 deletions code/lib/builder-vite/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './mdx-plugin';
export * from './strip-story-hmr-boundaries';
export * from './code-generator-plugin';
export * from './csf-plugin';
export * from './external-globals-plugin';
5 changes: 3 additions & 2 deletions code/lib/builder-vite/src/vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
UserConfig as ViteConfig,
InlineConfig,
} from 'vite';
import { viteExternalsPlugin } from 'vite-plugin-externals';
import { isPreservingSymlinks, getFrameworkName, getBuilderOptions } from '@storybook/core-common';
import { globals } from '@storybook/preview/globals';
import type { Options } from '@storybook/types';
Expand All @@ -17,7 +16,9 @@ import {
injectExportOrderPlugin,
mdxPlugin,
stripStoryHMRBoundary,
externalGlobalsPlugin,
} from './plugins';

import type { BuilderOptions } from './types';

export type PluginConfigType = 'build' | 'development';
Expand Down Expand Up @@ -93,7 +94,7 @@ export async function pluginConfig(options: Options) {
}
},
},
viteExternalsPlugin(globals, { useWindow: false, disableInServe: true }),
await externalGlobalsPlugin(globals),
] as PluginOption[];

// TODO: framework doesn't exist, should move into framework when/if built
Expand Down
24 changes: 1 addition & 23 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5766,7 +5766,6 @@ __metadata:
slash: ^3.0.0
typescript: ~4.9.3
vite: ^4.0.4
vite-plugin-externals: ^0.5.1
peerDependencies:
"@preact/preset-vite": "*"
typescript: ">= 4.3.x"
Expand Down Expand Up @@ -9658,7 +9657,7 @@ __metadata:
languageName: node
linkType: hard

"acorn@npm:^8.1.0, acorn@npm:^8.4.0, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.6.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.1":
"acorn@npm:^8.1.0, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.6.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.1":
version: 8.8.1
resolution: "acorn@npm:8.8.1"
bin:
Expand Down Expand Up @@ -13997,13 +13996,6 @@ __metadata:
languageName: node
linkType: hard

"es-module-lexer@npm:^0.4.1":
version: 0.4.1
resolution: "es-module-lexer@npm:0.4.1"
checksum: 6463778f04367979d7770cefb1969b6bfc277319e8437a39718b3516df16b1b496b725ceec96a2d24975837a15cf4d56838f16d9c8c7640ad13ad9c8f93ad6fc
languageName: node
linkType: hard

"es-module-lexer@npm:^0.9.0, es-module-lexer@npm:^0.9.3":
version: 0.9.3
resolution: "es-module-lexer@npm:0.9.3"
Expand Down Expand Up @@ -28063,20 +28055,6 @@ __metadata:
languageName: node
linkType: hard

"vite-plugin-externals@npm:^0.5.1":
version: 0.5.1
resolution: "vite-plugin-externals@npm:0.5.1"
dependencies:
acorn: ^8.4.0
es-module-lexer: ^0.4.1
fs-extra: ^10.0.0
magic-string: ^0.25.7
peerDependencies:
vite: ">=2.0.0"
checksum: a8b07fc911efb0a0ed47e12c6dc8f71280c40d222ae9b9ffa5c238aa5427bfda1b13444b378bdb649734057a53574f362f4e9af3ef96180be8901af18cab2f78
languageName: node
linkType: hard

"vite-plugin-turbosnap@npm:^1.0.1":
version: 1.0.1
resolution: "vite-plugin-turbosnap@npm:1.0.1"
Expand Down

0 comments on commit 95f4adc

Please sign in to comment.