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 14 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
31 changes: 31 additions & 0 deletions code/lib/builder-vite/src/plugins/externals-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { rewriteImport } from './externals-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{Rain,Jour as Day,Nuit as Night,Sun}from '${packageName}'`,
Dschungelabenteuer marked this conversation as resolved.
Show resolved Hide resolved
output: `const {Rain,Jour: Day,Nuit: Night,Sun} = ${globals[packageName]}`,
},
{
globals,
packageName,
input: `const { Afternoon } = await import('${packageName}')`,
output: `const { Afternoon } = ${globals[packageName]}`,
},
Dschungelabenteuer marked this conversation as resolved.
Show resolved Hide resolved
];
Dschungelabenteuer marked this conversation as resolved.
Show resolved Hide resolved

test('rewriteImport', () => {
cases.forEach(({ input, output, globals: caseGlobals, packageName: casePackage }) => {
expect(rewriteImport<false>(input, caseGlobals, casePackage)).toStrictEqual(output);
});
});
111 changes: 111 additions & 0 deletions code/lib/builder-vite/src/plugins/externals-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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';
import { globals } from '@storybook/preview/globals';

type Globals = typeof globals & Record<string, string>;

const replacementMap = new Map([
['import ', 'const '],
['import{', 'const {'],
[' as ', ': '],
[' from ', ' = '],
['}from', '} ='],
]);
Dschungelabenteuer marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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 externalsPlugin() {
Dschungelabenteuer marked this conversation as resolved.
Show resolved Hide resolved
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(globals) as Array<keyof typeof globals>).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 = ${globals[externalKey]};`);
})
);

return {
resolve: {
alias: newAlias,
},
};
},
// Replace imports with variables destructured from global scope
async transform(code: string, id: string) {
const globalsList = Object.keys(globals);
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, globals, packageName);
src.update(startPosition, endPosition, transformedImport);
}
});

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

export function rewriteImport<T = true>(
importStatement: string,
globs: T extends true ? Globals : Record<string, string>,
packageName: keyof typeof globs & string
): string {
Dschungelabenteuer marked this conversation as resolved.
Show resolved Hide resolved
const lookup = [
...replacementMap.keys(),
`.${packageName}.`,
`await import\\(.${packageName}.\\)`,
];
const search = new RegExp(`(${lookup.join('|')})`, 'g');
return importStatement.replace(
search,
(match) => replacementMap.get(match) ?? globs[packageName]
);
}
6 changes: 3 additions & 3 deletions code/lib/builder-vite/src/vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ 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';
import {
codeGeneratorPlugin,
Expand All @@ -18,6 +16,8 @@ import {
mdxPlugin,
stripStoryHMRBoundary,
} from './plugins';

import { externalsPlugin } from './plugins/externals-plugin';
import type { BuilderOptions } from './types';

export type PluginConfigType = 'build' | 'development';
Expand Down Expand Up @@ -93,7 +93,7 @@ export async function pluginConfig(options: Options) {
}
},
},
viteExternalsPlugin(globals, { useWindow: false, disableInServe: true }),
await externalsPlugin(),
] 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