mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 23:22:10 +08:00
131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
import { existsSync } from 'node:fs';
|
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
import { init, parse } from 'es-module-lexer';
|
|
import findCacheDirectory from 'find-cache-dir';
|
|
import MagicString from 'magic-string';
|
|
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 destructured from
|
|
* global variables that are added in runtime.js.
|
|
*
|
|
* @example
|
|
*
|
|
* ```js
|
|
* import {
|
|
* useMemo as useMemo2,
|
|
* useEffect as useEffect2,
|
|
* } from 'storybook/internal/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;
|
|
const { mergeAlias } = await import('vite');
|
|
|
|
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 = findCacheDirectory({
|
|
name: 'sb-vite-plugin-externals',
|
|
create: true,
|
|
}) as string;
|
|
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 });
|
|
if (!existsSync(externalCachePath)) {
|
|
const directory = dirname(externalCachePath);
|
|
await mkdir(directory, { recursive: true });
|
|
}
|
|
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: null,
|
|
};
|
|
},
|
|
} 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]
|
|
);
|
|
}
|