mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-06 07:21:16 +08:00
150 lines
4.9 KiB
TypeScript
150 lines
4.9 KiB
TypeScript
/* eslint-disable no-param-reassign */
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { mergeConfig } from 'vite';
|
|
import type { Plugin } from 'vite';
|
|
import { transformIframeHtml } from '../transform-iframe-html';
|
|
import { generateIframeScriptCode } from '../codegen-iframe-script';
|
|
import { generateModernIframeScriptCode } from '../codegen-modern-iframe-script';
|
|
import { generateImportFnScriptCode } from '../codegen-importfn-script';
|
|
import { generateVirtualStoryEntryCode, generatePreviewEntryCode } from '../codegen-entries';
|
|
import { generateAddonSetupCode } from '../codegen-set-addon-channel';
|
|
|
|
import type { ExtendedOptions } from '../types';
|
|
|
|
import {
|
|
virtualAddonSetupFile,
|
|
virtualFileId,
|
|
virtualPreviewFile,
|
|
virtualStoriesFile,
|
|
} from '../virtual-file-names';
|
|
|
|
export function codeGeneratorPlugin(options: ExtendedOptions): Plugin {
|
|
const iframePath = path.resolve(__dirname, '../..', 'input', 'iframe.html');
|
|
let iframeId: string;
|
|
|
|
// noinspection JSUnusedGlobalSymbols
|
|
return {
|
|
name: 'storybook:code-generator-plugin',
|
|
enforce: 'pre',
|
|
configureServer(server) {
|
|
// invalidate the whole vite-app.js script on every file change.
|
|
// (this might be a little too aggressive?)
|
|
server.watcher.on('change', () => {
|
|
const appModule = server.moduleGraph.getModuleById(virtualFileId);
|
|
if (appModule) {
|
|
server.moduleGraph.invalidateModule(appModule);
|
|
}
|
|
const storiesModule = server.moduleGraph.getModuleById(virtualStoriesFile);
|
|
if (storiesModule) {
|
|
server.moduleGraph.invalidateModule(storiesModule);
|
|
}
|
|
});
|
|
|
|
// Adding new story files is not covered by the change event above. So we need to detect this and trigger
|
|
// HMR to update the importFn.
|
|
server.watcher.on('add', (path) => {
|
|
// TODO maybe use the stories declaration in main
|
|
if (/\.stories\.([tj])sx?$/.test(path) || /\.(story|stories).mdx$/.test(path)) {
|
|
// We need to emit a change event to trigger HMR
|
|
server.watcher.emit('change', virtualStoriesFile);
|
|
}
|
|
});
|
|
},
|
|
config(config, { command }) {
|
|
// If we are building the static distribution, add iframe.html as an entry.
|
|
// In development mode, it's not an entry - instead, we use an express middleware
|
|
// to serve iframe.html. The reason is that Vite's dev server (at the time of writing)
|
|
// does not support virtual files as entry points.
|
|
if (command === 'build') {
|
|
if (!config.build) {
|
|
config.build = {};
|
|
}
|
|
config.build.rollupOptions = {
|
|
...config.build.rollupOptions,
|
|
input: iframePath,
|
|
};
|
|
}
|
|
|
|
// Detect if react 18 is installed. If not, alias it to a virtual placeholder file.
|
|
try {
|
|
require.resolve('react-dom/client', { paths: [config.root || process.cwd()] });
|
|
} catch (e) {
|
|
if (isNodeError(e) && e.code === 'MODULE_NOT_FOUND') {
|
|
config.resolve = mergeConfig(config.resolve ?? {}, {
|
|
alias: {
|
|
'react-dom/client': path.resolve(
|
|
__dirname,
|
|
'../..',
|
|
'input',
|
|
'react-dom-client-placeholder.js'
|
|
),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
},
|
|
configResolved(config) {
|
|
iframeId = `${config.root}/iframe.html`;
|
|
},
|
|
resolveId(source) {
|
|
if (source === virtualFileId) {
|
|
return virtualFileId;
|
|
}
|
|
if (source === iframePath) {
|
|
return iframeId;
|
|
}
|
|
if (source === virtualStoriesFile) {
|
|
return virtualStoriesFile;
|
|
}
|
|
if (source === virtualPreviewFile) {
|
|
return virtualPreviewFile;
|
|
}
|
|
if (source === virtualAddonSetupFile) {
|
|
return virtualAddonSetupFile;
|
|
}
|
|
return undefined;
|
|
},
|
|
async load(id) {
|
|
const storyStoreV7 = options.features?.storyStoreV7;
|
|
if (id === virtualStoriesFile) {
|
|
if (storyStoreV7) {
|
|
return generateImportFnScriptCode(options);
|
|
}
|
|
return generateVirtualStoryEntryCode(options);
|
|
}
|
|
|
|
if (id === virtualAddonSetupFile) {
|
|
return generateAddonSetupCode();
|
|
}
|
|
|
|
if (id === virtualPreviewFile && !storyStoreV7) {
|
|
return generatePreviewEntryCode(options);
|
|
}
|
|
|
|
if (id === virtualFileId) {
|
|
if (storyStoreV7) {
|
|
return generateModernIframeScriptCode(options);
|
|
}
|
|
return generateIframeScriptCode(options);
|
|
}
|
|
|
|
if (id === iframeId) {
|
|
return fs.readFileSync(path.resolve(__dirname, '../..', 'input', 'iframe.html'), 'utf-8');
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
async transformIndexHtml(html, ctx) {
|
|
if (ctx.path !== '/iframe.html') {
|
|
return undefined;
|
|
}
|
|
return transformIframeHtml(html, options);
|
|
},
|
|
};
|
|
}
|
|
|
|
// Refines an error received from 'catch' to be a NodeJS exception
|
|
const isNodeError = (error: unknown): error is NodeJS.ErrnoException => error instanceof Error;
|