Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vite: Replace vite-plugin-externals with custom plugin #20698

Merged
merged 18 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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