Skip to content

Commit

Permalink
Merge pull request #25923 from storybookjs/tom/add-vite-stats-json
Browse files Browse the repository at this point in the history
Vite: Add a `rollup-plugin-webpack-stats` to allow stats from preview builds
  • Loading branch information
tmeasday authored Feb 8, 2024
2 parents d77745e + b629fa7 commit 44ed235
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 0 deletions.
24 changes: 24 additions & 0 deletions code/builders/builder-vite/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import type { Options } from '@storybook/types';
import { commonConfig } from './vite-config';

import { sanitizeEnvVars } from './envs';
import type { WebpackStatsPlugin } from './plugins';
import type { InlineConfig } from 'vite';
import { logger } from '@storybook/node-logger';
import { hasVitePlugins } from './utils/has-vite-plugins';
import { withoutVitePlugins } from './utils/without-vite-plugins';

function findPlugin(config: InlineConfig, name: string) {
return config.plugins?.find((p) => p && 'name' in p && p.name === name);
}

export async function build(options: Options) {
const { build: viteBuild, mergeConfig } = await import('vite');
Expand All @@ -28,5 +37,20 @@ export async function build(options: Options) {
}).build;

const finalConfig = await presets.apply('viteFinal', config, options);

const turbosnapPluginName = 'rollup-plugin-turbosnap';
const hasTurbosnapPlugin =
finalConfig.plugins && hasVitePlugins(finalConfig.plugins, [turbosnapPluginName]);
if (hasTurbosnapPlugin) {
logger.warn(`Found '${turbosnapPluginName}' which is now included by default in Storybook 8.`);
logger.warn(
`Removing from your plugins list. Ensure you pass \`--webpack-stats-json\` to generate stats.`
);
finalConfig.plugins = await withoutVitePlugins(finalConfig.plugins, [turbosnapPluginName]);
}

await viteBuild(await sanitizeEnvVars(options, finalConfig));

const statsPlugin = findPlugin(finalConfig, 'rollup-plugin-webpack-stats') as WebpackStatsPlugin;
return statsPlugin?.storybookGetStats();
}
1 change: 1 addition & 0 deletions code/builders/builder-vite/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './strip-story-hmr-boundaries';
export * from './code-generator-plugin';
export * from './csf-plugin';
export * from './external-globals-plugin';
export * from './webpack-stats-plugin';
116 changes: 116 additions & 0 deletions code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// This plugin is a direct port of https://github.com/IanVS/vite-plugin-turbosnap

import type { BuilderStats } from '@storybook/types';
import path from 'path';
import type { Plugin } from 'vite';

/*
* Reason, Module are copied from chromatic types
* https://github.com/chromaui/chromatic-cli/blob/145a5e295dde21042e96396c7e004f250d842182/bin-src/types.ts#L265-L276
*/
interface Reason {
moduleName: string;
}
interface Module {
id: string | number;
name: string;
modules?: Array<Pick<Module, 'name'>>;
reasons?: Reason[];
}

type WebpackStatsPluginOptions = {
workingDir: string;
};

/**
* Strips off query params added by rollup/vite to ids, to make paths compatible for comparison with git.
*/
function stripQueryParams(filePath: string): string {
return filePath.split('?')[0];
}

/**
* We only care about user code, not node_modules, vite files, or (most) virtual files.
*/
function isUserCode(moduleName: string) {
return Boolean(
moduleName &&
!moduleName.startsWith('vite/') &&
!moduleName.startsWith('\x00') &&
!moduleName.startsWith('\u0000') &&
moduleName !== 'react/jsx-runtime' &&
!moduleName.match(/node_modules\//)
);
}

export type WebpackStatsPlugin = Plugin & { storybookGetStats: () => BuilderStats };

export function pluginWebpackStats({ workingDir }: WebpackStatsPluginOptions): WebpackStatsPlugin {
/**
* Convert an absolute path name to a path relative to the vite root, with a starting `./`
*/
function normalize(filename: string) {
// Do not try to resolve virtual files
if (filename.startsWith('/virtual:')) {
return filename;
}
// Otherwise, we need them in the format `./path/to/file.js`.
else {
const relativePath = path.relative(workingDir, stripQueryParams(filename));
// This seems hacky, got to be a better way to add a `./` to the start of a path.
return `./${relativePath}`;
}
}

/**
* Helper to create Reason objects out of a list of string paths
*/
function createReasons(importers?: readonly string[]): Reason[] {
return (importers || []).map((i) => ({ moduleName: normalize(i) }));
}

/**
* Helper function to build a `Module` given a filename and list of files that import it
*/
function createStatsMapModule(filename: string, importers?: readonly string[]): Module {
return {
id: filename,
name: filename,
reasons: createReasons(importers),
};
}

const statsMap = new Map<string, Module>();

return {
name: 'storybook:rollup-plugin-webpack-stats',
// We want this to run after the vite build plugins (https://vitejs.dev/guide/api-plugin.html#plugin-ordering)
enforce: 'post',
moduleParsed: function (mod) {
if (isUserCode(mod.id)) {
mod.importedIds
.concat(mod.dynamicallyImportedIds)
.filter((name) => isUserCode(name))
.forEach((depIdUnsafe) => {
const depId = normalize(depIdUnsafe);
if (statsMap.has(depId)) {
const m = statsMap.get(depId);
if (m) {
m.reasons = (m.reasons ?? [])
.concat(createReasons([mod.id]))
.filter((r) => r.moduleName !== depId);
statsMap.set(depId, m);
}
} else {
statsMap.set(depId, createStatsMapModule(depId, [mod.id]));
}
});
}
},

storybookGetStats() {
const stats = { modules: Array.from(statsMap.values()) };
return { ...stats, toJson: () => stats };
},
};
}
2 changes: 2 additions & 0 deletions code/builders/builder-vite/src/vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
injectExportOrderPlugin,
stripStoryHMRBoundary,
externalGlobalsPlugin,
pluginWebpackStats,
} from './plugins';

import type { BuilderOptions } from './types';
Expand Down Expand Up @@ -112,6 +113,7 @@ export async function pluginConfig(options: Options) {
},
},
await externalGlobalsPlugin(externals),
pluginWebpackStats({ workingDir: process.cwd() }),
] as PluginOption[];

// TODO: framework doesn't exist, should move into framework when/if built
Expand Down

0 comments on commit 44ed235

Please sign in to comment.