Merge pull request #19718 from storybookjs/norbert/sb-798-figure-out-plan-for-package-structure-rework

prebundle the preview
This commit is contained in:
Norbert de Langen 2022-11-22 15:40:05 +01:00 committed by GitHub
commit d491ad3a97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 1403 additions and 937 deletions

View File

@ -89,7 +89,7 @@
"devDependencies": {
"@storybook/jest": "^0.0.10",
"@storybook/testing-library": "0.0.14-next.0",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"formik": "^2.2.9",
"typescript": "^4.9.3"
},

View File

@ -47,7 +47,7 @@
"@storybook/node-logger": "7.0.0-alpha.52",
"@storybook/store": "7.0.0-alpha.52",
"@storybook/types": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"@types/react": "^16.14.34",
"@types/react-dom": "^16.9.14",
"@types/semver": "^7.3.4",

View File

@ -54,7 +54,7 @@
"@storybook/core-common": "7.0.0-alpha.52",
"@storybook/html": "7.0.0-alpha.52",
"@storybook/preset-html-webpack": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"global": "^4.4.0",
"react": "16.14.0",
"react-dom": "16.14.0"

View File

@ -65,7 +65,7 @@
"@storybook/node-logger": "7.0.0-alpha.52",
"@storybook/preset-react-webpack": "7.0.0-alpha.52",
"@storybook/react": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"find-up": "^5.0.0",
"fs-extra": "^9.0.1",
"image-size": "^1.0.0",

View File

@ -54,7 +54,7 @@
"@storybook/core-common": "7.0.0-alpha.52",
"@storybook/preact": "7.0.0-alpha.52",
"@storybook/preset-preact-webpack": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"react": "16.14.0",
"react-dom": "16.14.0"
},

View File

@ -67,7 +67,7 @@
"vite": "^3.1.3"
},
"devDependencies": {
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"typescript": "^4.9.3",
"vite": "^3.1.3"
},

View File

@ -54,7 +54,7 @@
"@storybook/builder-webpack5": "7.0.0-alpha.52",
"@storybook/preset-react-webpack": "7.0.0-alpha.52",
"@storybook/react": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0"
"@types/node": "^16.0.0"
},
"devDependencies": {
"jest-specific-snapshot": "^6.0.0",

View File

@ -54,7 +54,7 @@
"@storybook/core-common": "7.0.0-alpha.52",
"@storybook/preset-server-webpack": "7.0.0-alpha.52",
"@storybook/server": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"react": "16.14.0",
"react-dom": "16.14.0"
},

View File

@ -67,7 +67,7 @@
"vite": "^3.1.3"
},
"devDependencies": {
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"typescript": "^4.9.3",
"vite": "^3.1.3"
},

View File

@ -54,7 +54,7 @@
"@storybook/core-common": "7.0.0-alpha.52",
"@storybook/preset-vue-webpack": "7.0.0-alpha.52",
"@storybook/vue": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"react": "16.14.0",
"react-dom": "16.14.0"
},

View File

@ -65,7 +65,7 @@
"vue-docgen-api": "^4.40.0"
},
"devDependencies": {
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"typescript": "^4.9.3",
"vite": "^3.1.3"
},

View File

@ -54,7 +54,7 @@
"@storybook/core-common": "7.0.0-alpha.52",
"@storybook/preset-vue3-webpack": "7.0.0-alpha.52",
"@storybook/vue3": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"react": "16.14.0",
"react-dom": "16.14.0"
},

View File

@ -64,7 +64,7 @@
"vite": "3"
},
"devDependencies": {
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"typescript": "^4.9.3",
"vite": "^3.1.0"
},

View File

@ -57,7 +57,7 @@
"@storybook/core-common": "7.0.0-alpha.52",
"@storybook/preset-web-components-webpack": "7.0.0-alpha.52",
"@storybook/web-components": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"react": "16.14.0",
"react-dom": "16.14.0"
},

View File

@ -24,11 +24,6 @@
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
"./shortcut": {
"require": "./dist/shortcut.js",
"import": "./dist/shortcut.mjs",
"types": "./dist/shortcut.d.ts"
},
"./package.json": "./package.json"
},
"main": "dist/index.js",
@ -79,8 +74,7 @@
},
"bundler": {
"entries": [
"./src/index.tsx",
"./src/shortcut.ts"
"./src/index.tsx"
]
},
"gitHead": "d2494e3f51ce0f55bcb1ef693a6477c669fbe666"

View File

@ -1,8 +0,0 @@
export type KeyCollection = string[];
export function shortcutToHumanString(shortcut: KeyCollection): string;
export function eventToShortcut(e: KeyboardEvent): KeyCollection | null;
export function shortcutMatchesShortcut(
inputShortcut: KeyCollection,
shortcut: KeyCollection
): boolean;

View File

@ -1 +0,0 @@
export * from './dist/shortcut';

View File

@ -64,6 +64,7 @@ import * as url from './modules/url';
import * as version from './modules/versions';
// eslint-disable-next-line import/no-cycle
import * as globals from './modules/globals';
import { eventToShortcut, shortcutMatchesShortcut, shortcutToHumanString } from './lib/shortcut';
const { ActiveTabs } = layout;
@ -480,3 +481,5 @@ export function useArgTypes(): API_ArgTypes {
const current = useCurrentStory();
return (current?.type === 'story' && current.argTypes) || {};
}
export { eventToShortcut, shortcutToHumanString, shortcutMatchesShortcut };

View File

@ -125,8 +125,8 @@ const starter: StarterFunction = async function* starterGeneratorFn({
const coreDirOrigin = join(dirname(require.resolve('@storybook/manager/package.json')), 'dist');
router.use(`/sb-addons`, express.static(addonsDir));
router.use(`/sb-manager`, express.static(coreDirOrigin));
router.use(`/sb-addons`, express.static(addonsDir, { immutable: true, maxAge: '5m' }));
router.use(`/sb-manager`, express.static(coreDirOrigin, { immutable: true, maxAge: '5m' }));
const { cssFiles, jsFiles } = await readOrderedFiles(addonsDir, compilation?.outputFiles);

View File

@ -57,7 +57,7 @@ export const renderHTML = async (
refs: Promise<Record<string, Ref>>,
logLevel: Promise<string>,
docsOptions: Promise<DocsOptions>,
{ versionCheck, releaseNotesData, docsMode, previewUrl, serverChannelUrl }: Options
{ versionCheck, releaseNotesData, previewUrl, serverChannelUrl, configType }: Options
) => {
const customHeadRef = await customHead;
const titleRef = await title;
@ -71,6 +71,7 @@ export const renderHTML = async (
REFS: JSON.stringify(await refs, null, 2),
LOGLEVEL: JSON.stringify(await logLevel, null, 2),
DOCS_OPTIONS: JSON.stringify(await docsOptions, null, 2),
CONFIG_TYPE: JSON.stringify(await configType, null, 2),
// These two need to be double stringified because the UI expects a string
VERSIONCHECK: JSON.stringify(JSON.stringify(versionCheck), null, 2),
RELEASE_NOTES_DATA: JSON.stringify(JSON.stringify(releaseNotesData), null, 2),

View File

@ -8,6 +8,8 @@
<link rel="shortcut icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="./sb-preview/runtime.mjs" rel="preload" as="script">
<% if (typeof head !== 'undefined') { %> <%- head %> <% } %>
<style>
@ -33,10 +35,12 @@
</script>
<% } %>
<script src="./sb-manager/runtime.mjs" type="module"></script>
<script type="module">
import './sb-manager/runtime.mjs';
<% files.js.forEach(file => { %>
<script src="<%= file %>" type="module"></script>
<% }); %>
<% files.js.forEach(file => { %>
import './<%= file %>';
<% }); %>
</script>
</body>
</html>

View File

@ -14,17 +14,22 @@
window.STORIES = '[STORIES HERE]';
window.DOCS_OPTIONS = '[DOCS_OPTIONS HERE]';
window.SERVER_CHANNEL_URL = '[SERVER_CHANNEL_URL HERE]';
// We do this so that "module && module.hot" etc. in Storybook source code
// doesn't fail (it will simply be disabled)
window.module = undefined;
</script>
</script>
<!-- [HEAD HTML SNIPPET HERE] -->
</head>
<body>
<!-- [BODY HTML SNIPPET HERE] -->
<div id="storybook-root"></div>
<div id="storybook-docs"></div>
<script type="module" src="/virtual:/@storybook/builder-vite/vite-app.js"></script>
<script type="module">
/* eslint-disable import/no-absolute-path, import/extensions, import/no-unresolved */
import '/sb-preview/runtime.mjs';
import '/virtual:/@storybook/builder-vite/vite-app.js';
</script>
</body>
</html>

View File

@ -41,18 +41,22 @@
"prep": "../../../scripts/prepare/bundle.ts"
},
"dependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@joshwooding/vite-plugin-react-docgen-typescript": "0.0.5",
"@storybook/client-api": "7.0.0-alpha.52",
"@storybook/client-logger": "7.0.0-alpha.52",
"@storybook/core-common": "7.0.0-alpha.52",
"@storybook/mdx2-csf": "next",
"@storybook/node-logger": "7.0.0-alpha.52",
"@storybook/preview": "7.0.0-alpha.52",
"@storybook/preview-web": "7.0.0-alpha.52",
"@storybook/source-loader": "7.0.0-alpha.52",
"@storybook/types": "7.0.0-alpha.52",
"@vitejs/plugin-react": "^2.0.0",
"browser-assert": "^1.2.1",
"es-module-lexer": "^0.9.3",
"express": "^4.17.1",
"fs-extra": "^9.0.1",
"glob": "^7.2.0",
"glob-promise": "^4.2.0",
"magic-string": "^0.26.1",
@ -61,7 +65,7 @@
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"typescript": "^4.9.3",
"vite": "^3.1.3"
},

View File

@ -1,9 +1,11 @@
// noinspection JSUnusedGlobalSymbols
import * as fs from 'fs';
import * as fs from 'fs-extra';
import type { Builder, StorybookConfig as StorybookBaseConfig, Options } from '@storybook/types';
import type { RequestHandler, Request, Response } from 'express';
import type { RequestHandler } from 'express';
import type { InlineConfig, UserConfig, ViteDevServer } from 'vite';
import express from 'express';
import { dirname, join, parse } from 'path';
import { transformIframeHtml } from './transform-iframe-html';
import { createViteServer } from './vite-server';
import { build as viteBuild } from './build';
@ -44,7 +46,7 @@ function iframeMiddleware(options: ExtendedOptions, server: ViteDevServer): Requ
return;
}
const indexHtml = fs.readFileSync(
const indexHtml = await fs.readFile(
require.resolve('@storybook/builder-vite/input/iframe.html'),
'utf-8'
);
@ -75,12 +77,10 @@ export const start: ViteBuilder['start'] = async ({
}) => {
server = await createViteServer(options as ExtendedOptions, devServer);
// Just mock this endpoint (which is really Webpack-specific) so we don't get spammed with 404 in browser devtools
// TODO: we should either show some sort of progress from Vite, or just try to disable the whole Loader in the Manager UI.
router.get('/progress', (req: Request, res: Response) => {
res.header('Cache-Control', 'no-cache');
res.header('Content-Type', 'text/event-stream');
});
const previewResolvedDir = dirname(require.resolve('@storybook/preview/package.json'));
const previewDirOrigin = join(previewResolvedDir, 'dist');
router.use(`/sb-preview`, express.static(previewDirOrigin, { immutable: true, maxAge: '5m' }));
router.use(iframeMiddleware(options as ExtendedOptions, server));
router.use(server.middlewares);
@ -93,5 +93,23 @@ export const start: ViteBuilder['start'] = async ({
};
export const build: ViteBuilder['build'] = async ({ options }) => {
return viteBuild(options as ExtendedOptions);
const viteCompilation = viteBuild(options as ExtendedOptions);
const previewResolvedDir = dirname(require.resolve('@storybook/preview/package.json'));
const previewDirOrigin = join(previewResolvedDir, 'dist');
const previewDirTarget = join(options.outputDir || '', `sb-preview`);
const previewFiles = fs.copy(previewDirOrigin, previewDirTarget, {
filter: (src) => {
const { ext } = parse(src);
if (ext) {
return ext === '.mjs';
}
return true;
},
});
const [out] = await Promise.all([viteCompilation, previewFiles]);
return out;
};

View File

@ -1,6 +1,8 @@
import * as path from 'path';
import { normalizePath, resolveConfig } from 'vite';
import type { InlineConfig as ViteInlineConfig } from 'vite';
import type { InlineConfig as ViteInlineConfig, UserConfig } from 'vite';
import { globalExternals } from '@fal-works/esbuild-plugin-global-externals';
import { definitions } from '@storybook/preview/globals';
import { listStories } from './list-stories';
import type { ExtendedOptions } from './types';
@ -113,11 +115,16 @@ export async function getOptimizeDeps(config: ViteInlineConfig, options: Extende
const resolve = resolvedConfig.createResolver({ asSrc: false });
const include = await asyncFilter(INCLUDE_CANDIDATES, async (id) => Boolean(await resolve(id)));
return {
const optimizeDeps: UserConfig['optimizeDeps'] = {
// We don't need to resolve the glob since vite supports globs for entries.
entries: stories,
// We need Vite to precompile these dependencies, because they contain non-ESM code that would break
// if we served it directly to the browser.
include,
esbuildOptions: {
plugins: [globalExternals(definitions)],
},
};
return optimizeDeps;
}

View File

@ -101,6 +101,10 @@ export function codeGeneratorPlugin(options: ExtendedOptions): Plugin {
if (source === virtualAddonSetupFile) {
return virtualAddonSetupFile;
}
if (source === '/sb-preview/runtime.mjs') {
return '/sb-preview/runtime.mjs';
}
return undefined;
},
async load(id) {
@ -127,6 +131,11 @@ export function codeGeneratorPlugin(options: ExtendedOptions): Plugin {
return generateIframeScriptCode(options);
}
// This is handled by the express router, not vite
if (id === '/sb-preview/runtime.mjs') {
return '';
}
if (id === iframeId) {
return fs.readFileSync(
require.resolve('@storybook/builder-vite/input/iframe.html'),

View File

@ -5,6 +5,7 @@ import type {
InlineConfig as ViteInlineConfig,
PluginOption,
UserConfig as ViteConfig,
InlineConfig,
} from 'vite';
import viteReact from '@vitejs/plugin-react';
import { isPreservingSymlinks, getFrameworkName } from '@storybook/core-common';
@ -42,12 +43,13 @@ export async function commonConfig(
const { config: userConfig = {} } = (await loadConfigFromFile(configEnv)) ?? {};
const sbConfig = {
const sbConfig: InlineConfig = {
configFile: false,
cacheDir: 'node_modules/.cache/.vite-storybook',
root: path.resolve(options.configDir, '..'),
// Allow storybook deployed as subfolder. See https://github.com/storybookjs/builder-vite/issues/238
base: './',
plugins: await pluginConfig(options),
resolve: {
preserveSymlinks: isPreservingSymlinks(),

View File

@ -36,6 +36,7 @@
"types": "./dist/presets/preview-preset.d.ts"
},
"./templates/virtualModuleModernEntry.js.handlebars": "./templates/virtualModuleModernEntry.js.handlebars",
"./templates/preview.ejs": "./templates/preview.ejs",
"./package.json": "./package.json"
},
"main": "dist/index.js",
@ -65,18 +66,21 @@
"@storybook/core-events": "7.0.0-alpha.52",
"@storybook/core-webpack": "7.0.0-alpha.52",
"@storybook/node-logger": "7.0.0-alpha.52",
"@storybook/preview": "7.0.0-alpha.52",
"@storybook/preview-web": "7.0.0-alpha.52",
"@storybook/router": "7.0.0-alpha.52",
"@storybook/store": "7.0.0-alpha.52",
"@storybook/theming": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"@types/semver": "^7.3.4",
"babel-loader": "^8.3.0",
"babel-plugin-named-exports-order": "^0.0.2",
"browser-assert": "^1.2.1",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"css-loader": "^6.7.1",
"express": "^4.17.1",
"fork-ts-checker-webpack-plugin": "^7.2.8",
"fs-extra": "^9.0.1",
"global": "^4.4.0",
"html-webpack-plugin": "^5.5.0",
"path-browserify": "^1.0.1",
@ -93,10 +97,12 @@
"webpack-virtual-modules": "^0.4.3"
},
"devDependencies": {
"@types/pretty-hrtime": "^1.0.0",
"@types/terser-webpack-plugin": "^5.2.0",
"@types/webpack-dev-middleware": "^5.3.0",
"@types/webpack-hot-middleware": "^2.25.6",
"@types/webpack-virtual-modules": "^0.1.1",
"pretty-hrtime": "^1.0.3",
"typescript": "^4.9.3"
},
"peerDependencies": {

View File

@ -3,13 +3,24 @@ import webpack, { ProgressPlugin } from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import { logger } from '@storybook/node-logger';
import { useProgressReporting } from '@storybook/core-common';
import type { Builder, Options } from '@storybook/types';
import { checkWebpackVersion } from '@storybook/core-webpack';
import { join } from 'path';
import { dirname, join, parse } from 'path';
import express from 'express';
import fs from 'fs-extra';
import { PREVIEW_BUILDER_PROGRESS } from '@storybook/core-events';
// eslint-disable-next-line import/no-extraneous-dependencies
import prettyTime from 'pretty-hrtime';
export * from './types';
export const printDuration = (startTime: [number, number]) =>
prettyTime(process.hrtime(startTime))
.replace(' ms', ' milliseconds')
.replace(' s', ' seconds')
.replace(' m', ' minutes');
let compilation: ReturnType<typeof webpackDevMiddleware> | undefined;
let reject: (reason?: any) => void;
@ -98,6 +109,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({
startTime,
options,
router,
channel,
}) {
const webpackInstance = await executor.get(options);
yield;
@ -120,9 +132,40 @@ const starter: StarterFunction = async function* starterGeneratorFn({
};
}
const { handler, modulesCount } = await useProgressReporting(router, startTime, options);
yield;
new ProgressPlugin({ handler, modulesCount }).apply(compiler);
const modulesCount = (await options.cache?.get('modulesCount').catch(() => {})) || 1000;
let totalModules: number;
let value = 0;
new ProgressPlugin({
handler: (newValue, message, arg3) => {
value = Math.max(newValue, value); // never go backwards
const progress = { value, message: message.charAt(0).toUpperCase() + message.slice(1) };
if (message === 'building') {
// arg3 undefined in webpack5
const counts = (arg3 && arg3.match(/(\d+)\/(\d+)/)) || [];
const complete = parseInt(counts[1], 10);
const total = parseInt(counts[2], 10);
if (!Number.isNaN(complete) && !Number.isNaN(total)) {
(progress as any).modules = { complete, total };
totalModules = total;
}
}
if (value === 1) {
if (options.cache) {
options.cache.set('modulesCount', totalModules);
}
if (!progress.message) {
progress.message = `Completed in ${printDuration(startTime)}.`;
}
}
channel.emit(PREVIEW_BUILDER_PROGRESS, [progress]);
},
modulesCount,
}).apply(compiler);
const middlewareOptions: Parameters<typeof webpackDevMiddleware>[1] = {
publicPath: config.output?.publicPath as string,
@ -131,6 +174,11 @@ const starter: StarterFunction = async function* starterGeneratorFn({
compilation = webpackDevMiddleware(compiler, middlewareOptions);
const previewResolvedDir = dirname(require.resolve('@storybook/preview/package.json'));
const previewDirOrigin = join(previewResolvedDir, 'dist');
router.use(`/sb-preview`, express.static(previewDirOrigin, { immutable: true, maxAge: '5m' }));
router.use(compilation);
router.use(webpackHotMiddleware(compiler as any));
@ -181,7 +229,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime,
} as any as Stats;
}
return new Promise<Stats>((succeed, fail) => {
const webpackCompilation = new Promise<Stats>((succeed, fail) => {
compiler.run((error, stats) => {
if (error || !stats || stats.hasErrors()) {
logger.error('=> Failed to build the preview');
@ -238,6 +286,24 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime,
});
});
});
const previewResolvedDir = dirname(require.resolve('@storybook/preview/package.json'));
const previewDirOrigin = join(previewResolvedDir, 'dist');
const previewDirTarget = join(options.outputDir || '', `sb-preview`);
const previewFiles = fs.copy(previewDirOrigin, previewDirTarget, {
filter: (src) => {
const { ext } = parse(src);
if (ext) {
return ext === '.mjs';
}
return true;
},
});
const [webpackCompilationOutput] = await Promise.all([webpackCompilation, previewFiles]);
return webpackCompilationOutput;
};
export const start = async (options: BuilderStartOptions) => {

View File

@ -30,3 +30,6 @@ export const babel = async (config: any, options: any) => ({
});
export const babelLoaderRef = () => require.resolve('babel-loader');
export const previewMainTemplate = () =>
require.resolve('@storybook/builder-webpack5/templates/preview.ejs');

View File

@ -1,4 +1,4 @@
import path from 'path';
import { dirname, join, resolve } from 'path';
import { DefinePlugin, HotModuleReplacementPlugin, ProgressPlugin, ProvidePlugin } from 'webpack';
import type { Configuration } from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
@ -24,27 +24,17 @@ import type { BuilderOptions, TypescriptOptions } from '../types';
import { createBabelLoader } from './babel-loader-preview';
const storybookPaths: Record<string, string> = {
global: path.dirname(require.resolve(`global/package.json`)),
global: dirname(require.resolve(`global/package.json`)),
...[
'addons',
// these packages are not pre-bundled because of react dependencies
'api',
'store',
'channels',
'channel-postmessage',
'channel-websocket',
'components',
'core-events',
'router',
'theming',
'preview-web',
'client-api',
'client-logger',
].reduce(
(acc, sbPackage) => ({
...acc,
[`@storybook/${sbPackage}`]: path.dirname(
require.resolve(`@storybook/${sbPackage}/package.json`)
),
[`@storybook/${sbPackage}`]: dirname(require.resolve(`@storybook/${sbPackage}/package.json`)),
}),
{}
),
@ -54,7 +44,7 @@ export default async (
options: Options & Record<string, any> & { typescriptOptions: TypescriptOptions }
): Promise<Configuration> => {
const {
outputDir = path.join('.', 'public'),
outputDir = join('.', 'public'),
quiet,
packageJson,
configType,
@ -66,21 +56,42 @@ export default async (
serverChannelUrl,
} = options;
const frameworkOptions = await presets.apply('frameworkOptions');
const isProd = configType === 'PRODUCTION';
const envs = await presets.apply<Record<string, string>>('env');
const logLevel = await presets.apply('logLevel', undefined);
const workingDir = process.cwd();
const [
coreOptions,
frameworkOptions,
envs,
logLevel,
headHtmlSnippet,
bodyHtmlSnippet,
template,
docsOptions,
entries,
nonNormalizedStories,
] = await Promise.all([
presets.apply<CoreConfig>('core'),
presets.apply('frameworkOptions'),
presets.apply<Record<string, string>>('env'),
presets.apply('logLevel', undefined),
presets.apply('previewHead'),
presets.apply('previewBody'),
presets.apply<string>('previewMainTemplate'),
presets.apply<DocsOptions>('docs'),
presets.apply<string[]>('entries', [], options),
presets.apply('stories', [], options),
]);
const stories = normalizeStories(nonNormalizedStories, {
configDir: options.configDir,
workingDir,
});
const headHtmlSnippet = await presets.apply('previewHead');
const bodyHtmlSnippet = await presets.apply('previewBody');
const template = await presets.apply<string>('previewMainTemplate');
const coreOptions = await presets.apply<CoreConfig>('core');
const builderOptions: BuilderOptions =
typeof coreOptions.builder === 'string'
? {}
: coreOptions.builder?.options || ({} as BuilderOptions);
const docsOptions = await presets.apply<DocsOptions>('docs');
const previewAnnotations = [
...(await presets.apply<PreviewAnnotation[]>('previewAnnotations', [], options)).map(
@ -97,21 +108,15 @@ export default async (
),
loadPreviewOrConfigFile(options),
].filter(Boolean);
const entries = (await presets.apply('entries', [], options)) as string[];
const workingDir = process.cwd();
const stories = normalizeStories(await presets.apply('stories', [], options), {
configDir: options.configDir,
workingDir,
});
const virtualModuleMapping: Record<string, string> = {};
if (features?.storyStoreV7) {
const storiesFilename = 'storybook-stories.js';
const storiesPath = path.resolve(path.join(workingDir, storiesFilename));
const storiesPath = resolve(join(workingDir, storiesFilename));
const needPipelinedImport = !!builderOptions.lazyCompilation && !isProd;
virtualModuleMapping[storiesPath] = toImportFn(stories, { needPipelinedImport });
const configEntryPath = path.resolve(path.join(workingDir, 'storybook-config-entry.js'));
const configEntryPath = resolve(join(workingDir, 'storybook-config-entry.js'));
virtualModuleMapping[configEntryPath] = handlebars(
await readTemplate(
require.resolve(
@ -128,14 +133,12 @@ export default async (
} else {
const rendererName = await getRendererName(options);
const rendererInitEntry = path.resolve(
path.join(workingDir, 'storybook-init-renderer-entry.js')
);
const rendererInitEntry = resolve(join(workingDir, 'storybook-init-renderer-entry.js'));
virtualModuleMapping[rendererInitEntry] = `import '${rendererName}';`;
entries.push(rendererInitEntry);
const entryTemplate = await readTemplate(
path.join(__dirname, '..', '..', 'templates', 'virtualModuleEntry.template.js')
join(__dirname, '..', '..', 'templates', 'virtualModuleEntry.template.js')
);
previewAnnotations.forEach((previewAnnotationFilename: string | undefined) => {
@ -159,12 +162,12 @@ export default async (
});
if (stories.length > 0) {
const storyTemplate = await readTemplate(
path.join(__dirname, '..', '..', 'templates', 'virtualModuleStory.template.js')
join(__dirname, '..', '..', 'templates', 'virtualModuleStory.template.js')
);
// NOTE: this file has a `.cjs` extension as it is a CJS file (from `dist/cjs`) and runs
// in the user's webpack mode, which may be strict about the use of require/import.
// See https://github.com/storybookjs/storybook/issues/14877
const storiesFilename = path.resolve(path.join(workingDir, `generated-stories-entry.cjs`));
const storiesFilename = resolve(join(workingDir, `generated-stories-entry.cjs`));
virtualModuleMapping[storiesFilename] = interpolate(storyTemplate, {
rendererName,
})
@ -192,7 +195,7 @@ export default async (
devtool: 'cheap-module-source-map',
entry: entries,
output: {
path: path.resolve(process.cwd(), outputDir),
path: resolve(process.cwd(), outputDir),
filename: isProd ? '[name].[contenthash:8].iframe.bundle.js' : '[name].iframe.bundle.js',
publicPath: '',
},
@ -203,6 +206,29 @@ export default async (
watchOptions: {
ignored: /node_modules/,
},
externals: {
...[
// these packages are pre-bundled, so they are mapped to global shims
'channels',
'channel-postmessage',
'channel-websocket',
'core-events',
'client-logger',
'addons',
'store',
'preview-web',
'client-api',
'core-client',
].reduce(
(acc, sbPackage) => ({
...acc,
[`@storybook/${sbPackage}`]: `__STORYBOOK_MODULE_${sbPackage
.toUpperCase()
.replaceAll('-', '_')}__`,
}),
{}
),
},
ignoreWarnings: [
{
message: /export '\S+' was not found in 'global'/,

View File

@ -36,8 +36,13 @@
<% } %>
<% } %>
</script>
<% } %> <% htmlWebpackPlugin.files.js.forEach(file => { %>
<script src="<%= file %>"></script>
<% }); %>
<% } %>
<script type="module">
import './sb-preview/runtime.mjs';
<% htmlWebpackPlugin.files.js.forEach(file => { %>
import './<%= file %>';
<% }); %>
</script>
</body>
</html>

View File

@ -51,7 +51,7 @@
"@storybook/types": "7.0.0-alpha.52",
"@types/babel__core": "^7.1.20",
"@types/express": "^4.7.0",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"@types/pretty-hrtime": "^1.0.0",
"chalk": "^4.1.0",
"esbuild": "^0.14.48",

View File

@ -23,7 +23,6 @@ export * from './utils/load-preview-or-config-file';
export * from './utils/log-config';
export * from './utils/normalize-stories';
export * from './utils/paths';
export * from './utils/progress-reporting';
export * from './utils/readTemplate';
export * from './utils/resolve-path-in-sb-cache';
export * from './utils/symlinks';

View File

@ -244,10 +244,6 @@ async function loadPresets(
return [];
}
if (!level) {
logger.info('=> Loading presets');
}
return (
await Promise.all(presets.map(async (preset) => loadPreset(preset, level, storybookOptions)))
).reduce((acc, loaded) => {

View File

@ -1,67 +0,0 @@
import type { Router, Request, Response } from 'express';
import { printDuration } from './print-duration';
export const useProgressReporting = async (
router: Router,
startTime: [number, number],
options: any
): Promise<{ handler: any; modulesCount: number }> => {
let value = 0;
let totalModules: number;
let reportProgress: (progress?: {
value?: number;
message: string;
modules?: any;
}) => void = () => {};
router.get('/progress', (request: Request, response: Response) => {
let closed = false;
const close = () => {
closed = true;
response.end();
};
response.on('close', close);
if (closed || response.writableEnded) return;
response.setHeader('Cache-Control', 'no-cache');
response.setHeader('Content-Type', 'text/event-stream');
response.setHeader('Connection', 'keep-alive');
response.flushHeaders();
reportProgress = (progress: any) => {
if (closed || response.writableEnded) return;
response.write(`data: ${JSON.stringify(progress)}\n\n`);
response.flush();
if (progress.value === 1) close();
};
});
const handler = (newValue: number, message: string, arg3: any) => {
value = Math.max(newValue, value); // never go backwards
const progress = { value, message: message.charAt(0).toUpperCase() + message.slice(1) };
if (message === 'building') {
// arg3 undefined in webpack5
const counts = (arg3 && arg3.match(/(\d+)\/(\d+)/)) || [];
const complete = parseInt(counts[1], 10);
const total = parseInt(counts[2], 10);
if (!Number.isNaN(complete) && !Number.isNaN(total)) {
(progress as any).modules = { complete, total };
totalModules = total;
}
}
if (value === 1) {
if (options.cache) {
options.cache.set('modulesCount', totalModules);
}
if (!progress.message) {
progress.message = `Completed in ${printDuration(startTime)}.`;
}
}
reportProgress(progress);
};
const modulesCount = (await options.cache?.get('modulesCount').catch(() => {})) || 1000;
return { handler, modulesCount };
};

View File

@ -36,7 +36,3 @@ export function getPreviewHeadTemplate(
return interpolate(result, interpolations);
}
export function getPreviewMainTemplate() {
return `${sync(__dirname)}/templates/preview.ejs`;
}

View File

@ -51,6 +51,8 @@ enum events {
REGISTER_SUBSCRIPTION = 'registerSubscription',
// Tell the manager that the user pressed a key in the preview
PREVIEW_KEYDOWN = 'previewKeydown',
// Tell the preview that the builder is in progress
PREVIEW_BUILDER_PROGRESS = 'preview_builder_progress',
// Used in the manager to change the story selection
SELECT_STORY = 'selectStory',
STORIES_COLLAPSE_ALL = 'storiesCollapseAll',
@ -70,41 +72,42 @@ export default events;
export const {
CHANNEL_CREATED,
CONFIG_ERROR,
STORY_INDEX_INVALIDATED,
STORY_SPECIFIED,
SET_STORIES,
SET_INDEX,
SET_CONFIG,
SET_CURRENT_STORY,
CURRENT_STORY_WAS_SET,
DOCS_RENDERED,
FORCE_RE_RENDER,
FORCE_REMOUNT,
STORY_PREPARED,
STORY_CHANGED,
STORY_UNCHANGED,
PRELOAD_ENTRIES,
STORY_RENDERED,
STORY_MISSING,
STORY_ERRORED,
STORY_THREW_EXCEPTION,
STORY_RENDER_PHASE_CHANGED,
PLAY_FUNCTION_THREW_EXCEPTION,
UPDATE_STORY_ARGS,
STORY_ARGS_UPDATED,
RESET_STORY_ARGS,
SET_GLOBALS,
UPDATE_GLOBALS,
GLOBALS_UPDATED,
REGISTER_SUBSCRIPTION,
NAVIGATE_URL,
PLAY_FUNCTION_THREW_EXCEPTION,
PRELOAD_ENTRIES,
PREVIEW_BUILDER_PROGRESS,
PREVIEW_KEYDOWN,
REGISTER_SUBSCRIPTION,
RESET_STORY_ARGS,
SELECT_STORY,
STORIES_COLLAPSE_ALL,
STORIES_EXPAND_ALL,
DOCS_RENDERED,
SET_CONFIG,
SET_CURRENT_STORY,
SET_GLOBALS,
SET_INDEX,
SET_STORIES,
SHARED_STATE_CHANGED,
SHARED_STATE_SET,
NAVIGATE_URL,
STORIES_COLLAPSE_ALL,
STORIES_EXPAND_ALL,
STORY_ARGS_UPDATED,
STORY_CHANGED,
STORY_ERRORED,
STORY_INDEX_INVALIDATED,
STORY_MISSING,
STORY_PREPARED,
STORY_RENDER_PHASE_CHANGED,
STORY_RENDERED,
STORY_SPECIFIED,
STORY_THREW_EXCEPTION,
STORY_UNCHANGED,
UPDATE_GLOBALS,
UPDATE_QUERY_PARAMS,
UPDATE_STORY_ARGS,
} = events;
// Used to break out of the current render without showing a redbox

View File

@ -46,7 +46,7 @@
"@storybook/store": "7.0.0-alpha.52",
"@storybook/telemetry": "7.0.0-alpha.52",
"@storybook/types": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"@types/node-fetch": "^2.5.7",
"@types/pretty-hrtime": "^1.0.0",
"@types/semver": "^7.3.4",

View File

@ -25,7 +25,7 @@ import { outputStats } from './utils/output-stats';
import { outputStartupInformation } from './utils/output-startup-information';
import { updateCheck } from './utils/update-check';
import { getServerPort, getServerChannelUrl } from './utils/server-address';
import { getBuilders } from './utils/get-builders';
import { getManagerBuilder, getPreviewBuilder } from './utils/get-builders';
export async function buildDevStandalone(options: CLIOptions & LoadOptions & BuilderOptions) {
const { packageJson, versionUpdates, releaseNotes } = options;
@ -76,16 +76,23 @@ export async function buildDevStandalone(options: CLIOptions & LoadOptions & Bui
logger.warn(`you have not specified a framework in your ${options.configDir}/main.js`);
}
logger.info('=> Loading presets');
// Load first pass: We need to determine the builder
// We need to do this because builders might introduce 'overridePresets' which we need to take into account
// We hope to remove this in SB8
let presets = await loadAllPresets({
corePresets,
overridePresets: [],
...options,
});
const [previewBuilder, managerBuilder] = await getBuilders({ ...options, presets });
const { renderer } = await presets.apply<CoreConfig>('core', undefined);
const { renderer, builder } = await presets.apply<CoreConfig>('core', undefined);
const builderName = typeof builder === 'string' ? builder : builder?.name;
const [previewBuilder, managerBuilder] = await Promise.all([
getPreviewBuilder(builderName, options.configDir),
getManagerBuilder(),
]);
// Load second pass: all presets are applied in order
presets = await loadAllPresets({
corePresets: [
require.resolve('./presets/common-preset'),

View File

@ -1,312 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import 'jest-specific-snapshot';
import path from 'path';
import { mkdtemp as mkdtempCb } from 'fs';
import os from 'os';
import { promisify } from 'util';
import type { Configuration } from 'webpack';
import {
resolvePathInStorybookCache,
createFileSystemCache,
getProjectRoot,
} from '@storybook/core-common';
import { executor as previewExecutor } from '@storybook/builder-webpack5';
import { executor as managerExecutor } from '@storybook/builder-manager';
import { sync as readUpSync } from 'read-pkg-up';
import { buildDevStandalone } from './build-dev';
import { buildStaticStandalone } from './build-static';
import { outputStats } from './utils/output-stats';
// @ts-expect-error (not strict)
const { SNAPSHOT_OS } = global;
const mkdtemp = promisify(mkdtempCb);
const { packageJson } = readUpSync({ cwd: __dirname });
// this only applies to this file
jest.setTimeout(10000);
// FIXME: this doesn't work
jest.mock('webpack', () => {
const value = jest.fn(() => false);
const actual = jest.requireActual('webpack');
Object.keys(actual).forEach((key) => {
// @ts-expect-error (not strict)
value[key] = actual[key];
});
return value;
});
jest.mock('@storybook/telemetry', () => ({
getStorybookMetadata: jest.fn(() => ({})),
telemetry: jest.fn(() => ({})),
}));
jest.mock('fs-extra', () => ({
copy: jest.fn(() => undefined),
emptyDir: jest.fn(() => undefined),
ensureDir: jest.fn(() => true),
ensureFile: jest.fn(() => undefined),
pathExists: jest.fn(() => true),
readFile: jest.fn((f) => ''),
readJSON: jest.fn(() => ({})),
remove: jest.fn(() => undefined),
writeFile: jest.fn(() => undefined),
writeJSON: jest.fn(() => undefined),
}));
jest.mock('./utils/StoryIndexGenerator', () => {
const { StoryIndexGenerator } = jest.requireActual('./utils/StoryIndexGenerator');
return {
StoryIndexGenerator: class extends StoryIndexGenerator {
initialize() {
return Promise.resolve(undefined);
}
getIndex() {
return { stories: {}, v: 3 };
}
},
};
});
jest.mock('./utils/stories-json', () => ({
extractStoriesJson: () => Promise.resolve(),
useStoriesJson: () => {},
}));
// we're not in the right directory for auto-title to work, so just
// stub it out
jest.mock('@storybook/store', () => {
const actualStore = jest.requireActual('@storybook/store');
return {
...actualStore,
autoTitle: () => 'auto-title',
autoTitleFromSpecifier: () => 'auto-title-from-specifier',
};
});
jest.mock('http', () => ({
...jest.requireActual('http'),
// @ts-expect-error (not strict)
createServer: () => ({ listen: (_options, cb) => cb(), on: jest.fn() }),
}));
jest.mock('ws');
jest.mock('@storybook/node-logger', () => ({
logger: {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
line: jest.fn(),
trace: jest.fn(),
},
}));
jest.mock('./utils/output-startup-information', () => ({
outputStartupInformation: jest.fn(),
}));
jest.mock('./utils/output-stats');
jest.mock('./utils/open-in-browser', () => ({
openInBrowser: jest.fn(),
}));
const cache = createFileSystemCache({
basePath: resolvePathInStorybookCache('dev-server'),
ns: 'storybook-test', // Optional. A grouping namespace for items.
});
const managerOnly = false;
const baseOptions = {
ignorePreview: managerOnly,
// FIXME: this should just be ignorePreview everywhere
managerOnly, // production
docsMode: false,
cache,
configDir: path.resolve(`${__dirname}/__for-testing__/`),
ci: true,
managerCache: false,
};
const ROOT = getProjectRoot();
const CWD = process.cwd();
const NODE_MODULES = /.*node_modules/g;
const cleanRoots = (obj: any): any => {
if (!obj) return obj;
if (typeof obj === 'string')
return obj.replace(CWD, 'CWD').replace(ROOT, 'ROOT').replace(NODE_MODULES, 'NODE_MODULES');
if (Array.isArray(obj)) return obj.map(cleanRoots);
if (obj instanceof RegExp) return cleanRoots(obj.toString());
if (typeof obj === 'object') {
return Object.fromEntries(
Object.entries(obj).map(([key, val]) => {
if (key === 'version' && typeof val === 'string') {
return [key, '*'];
}
return [key, cleanRoots(val)];
})
);
}
return obj;
};
const getConfig = (fn: any, name: string): Configuration | null => {
const call = fn.mock.calls.find((c: any) => c[0].name === name);
if (!call) {
return null;
}
return call[0];
};
const prepareSnap = (
get: any,
name: string
): Pick<Configuration, 'module' | 'entry' | 'plugins'> => {
const config = getConfig(get(), name);
if (!config) {
return null;
}
const keys = Object.keys(config);
const { module, entry, plugins } = config;
return cleanRoots({ keys, module, entry, plugins: plugins.map((p) => p.constructor.name) });
};
const snap = (name: string) => `__snapshots__/${name}`;
// FIXME: we no longer have test cases
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('FIXME', () => {
// @ts-expect-error (not strict)
describe.each([[]])('%s', (example: string) => {
describe.each([
['manager', managerExecutor],
['preview', previewExecutor],
])('%s', (component, executor) => {
beforeEach(async () => {
jest.clearAllMocks();
await cache.clear();
});
it.each([
['prod', buildStaticStandalone],
['dev', buildDevStandalone],
])('%s', async (mode, builder) => {
const options = {
...baseOptions,
configDir: path.resolve(`${__dirname}/../../../examples/${example}/.storybook`),
// Only add an outputDir in production mode.
outputDir:
mode === 'prod'
? await mkdtemp(path.join(os.tmpdir(), 'storybook-static-'))
: undefined,
ignorePreview: component === 'manager',
managerCache: component === 'preview',
packageJson,
};
await builder(options);
const config = prepareSnap(executor.get, component);
expect(config).toMatchSpecificSnapshot(
snap(`${example}_${component}-${mode}-${SNAPSHOT_OS}`)
);
});
});
});
});
const progressPlugin = (config: any) =>
config.plugins.find((p: any) => p.constructor.name === 'ProgressPlugin');
describe('dev cli flags', () => {
beforeEach(async () => {
jest.clearAllMocks();
await cache.clear();
});
const cliOptions = { ...baseOptions, packageJson };
// eslint-disable-next-line jest/no-disabled-tests
it.skip('baseline', async () => {
await buildDevStandalone(cliOptions);
const config = getConfig(previewExecutor.get, 'preview');
expect(progressPlugin(config)).toBeTruthy();
});
// eslint-disable-next-line jest/no-disabled-tests
it.skip('--quiet', async () => {
const options = { ...cliOptions, quiet: true };
await buildDevStandalone(options);
const config = getConfig(previewExecutor.get, 'preview');
expect(progressPlugin(config)).toBeFalsy();
});
it('--webpack-stats-json calls output-stats', async () => {
await buildDevStandalone(cliOptions);
expect(outputStats).not.toHaveBeenCalled();
await buildDevStandalone({ ...cliOptions, webpackStatsJson: '/tmp/dir' });
expect(outputStats).toHaveBeenCalledWith(
'/tmp/dir',
expect.objectContaining({ toJson: expect.any(Function) })
);
});
describe.each([
['root directory /', '/', "Won't remove directory '/'. Check your outputDir!"],
['empty string ""', '', "Won't remove current directory. Check your outputDir!"],
])('Invalid outputDir must throw: %s', (_, outputDir, expectedErrorMessage) => {
const optionsWithInvalidDir = {
...cliOptions,
outputDir,
};
it('production mode', async () => {
expect.assertions(1);
await expect(buildStaticStandalone(optionsWithInvalidDir)).rejects.toThrow(
expectedErrorMessage
);
});
});
describe('Invalid staticDir must throw: root directory /', () => {
const optionsWithInvalidStaticDir = {
...cliOptions,
staticDir: ['/'],
};
it('production mode', async () => {
expect.assertions(1);
// @ts-expect-error (not strict)
await expect(buildStaticStandalone(optionsWithInvalidStaticDir)).rejects.toThrow(
"Won't copy root directory. Check your staticDirs!"
);
});
});
});
describe('build cli flags', () => {
beforeEach(async () => {
jest.clearAllMocks();
await cache.clear();
});
const cliOptions = {
...baseOptions,
outputDir: `${__dirname}/storybook-static`,
packageJson,
};
it('does not call output-stats', async () => {
await buildStaticStandalone(cliOptions);
expect(outputStats).not.toHaveBeenCalled();
});
it('--webpack-stats-json calls output-stats', async () => {
await buildStaticStandalone({ ...cliOptions, webpackStatsJson: '/tmp/dir' });
expect(outputStats).toHaveBeenCalledWith(
'/tmp/dir',
expect.objectContaining({ toJson: expect.any(Function) })
);
});
});

View File

@ -18,10 +18,11 @@ import { getServer } from './utils/server-init';
import { useStatics } from './utils/server-statics';
import { useStoriesJson } from './utils/stories-json';
import { useStorybookMetadata } from './utils/metadata';
import type { ServerChannel } from './utils/get-server-channel';
import { getServerChannel } from './utils/get-server-channel';
import { openInBrowser } from './utils/open-in-browser';
import { getBuilders } from './utils/get-builders';
import { getManagerBuilder, getPreviewBuilder } from './utils/get-builders';
import { StoryIndexGenerator } from './utils/StoryIndexGenerator';
import { summarizeIndex } from './utils/summarizeIndex';
@ -37,62 +38,24 @@ const versionStatus = (versionCheck: VersionCheck) => {
};
export async function storybookDevServer(options: Options) {
const startTime = process.hrtime();
const app = express();
const server = await getServer(app, options);
const [server, features, core] = await Promise.all([
getServer(app, options),
options.presets.apply<StorybookConfig['features']>('features'),
options.presets.apply<CoreConfig>('core'),
]);
const serverChannel = getServerChannel(server);
const features = await options.presets.apply<StorybookConfig['features']>('features');
const core = await options.presets.apply<CoreConfig>('core');
// try get index generator, if failed, send telemetry without storyCount, then rethrow the error
let initializedStoryIndexGenerator: Promise<StoryIndexGenerator> = Promise.resolve(undefined);
if (features?.buildStoriesJson || features?.storyStoreV7) {
const workingDir = process.cwd();
const directories = {
configDir: options.configDir,
workingDir,
};
const normalizedStories = normalizeStories(await options.presets.apply('stories'), directories);
const storyIndexers = await options.presets.apply('storyIndexers', []);
const docsOptions = await options.presets.apply<DocsOptions>('docs', {});
const initializedStoryIndexGenerator: Promise<StoryIndexGenerator> = getStoryIndexGenerator(
features,
options,
serverChannel
);
const generator = new StoryIndexGenerator(normalizedStories, {
...directories,
storyIndexers,
docs: docsOptions,
workingDir,
storiesV2Compatibility: !features?.breakingChangesV7 && !features?.storyStoreV7,
storyStoreV7: features?.storyStoreV7,
});
initializedStoryIndexGenerator = generator.initialize().then(() => generator);
useStoriesJson({
router,
initializedStoryIndexGenerator,
normalizedStories,
serverChannel,
workingDir,
});
}
if (!core?.disableTelemetry) {
initializedStoryIndexGenerator.then(async (generator) => {
const storyIndex = await generator?.getIndex();
const { versionCheck, versionUpdates } = options;
const payload = storyIndex
? {
versionStatus: versionUpdates ? versionStatus(versionCheck) : 'disabled',
storyIndex: summarizeIndex(storyIndex),
}
: undefined;
telemetry('dev', payload, { configDir: options.configDir });
});
}
if (!core?.disableProjectJson) {
useStorybookMetadata(router, options.configDir);
}
doTelemetry(core, initializedStoryIndexGenerator, options);
app.use(compression({ level: 1 }));
@ -119,8 +82,7 @@ export async function storybookDevServer(options: Options) {
}
// User's own static files
await useStatics(router, options);
const usingStatics = useStatics(router, options);
getMiddleware(options.configDir)(router);
app.use(router);
@ -129,49 +91,131 @@ export async function storybookDevServer(options: Options) {
const proto = options.https ? 'https' : 'http';
const { address, networkAddress } = getServerAddresses(port, host, proto);
await new Promise<void>((resolve, reject) => {
// FIXME: Following line doesn't match TypeScript signature at all 🤔
// @ts-expect-error (Converted from ts-ignore)
const listening = new Promise<void>((resolve, reject) => {
// @ts-expect-error (Following line doesn't match TypeScript signature at all 🤔)
server.listen({ port, host }, (error: Error) => (error ? reject(error) : resolve()));
});
const [previewBuilder, managerBuilder] = await getBuilders(options);
const builderName = typeof core?.builder === 'string' ? core.builder : core?.builder?.name;
const [previewBuilder, managerBuilder] = await Promise.all([
getPreviewBuilder(builderName, options.configDir),
getManagerBuilder(),
]);
if (options.debugWebpack) {
logConfig('Preview webpack config', await previewBuilder.getConfig(options));
}
Promise.all([initializedStoryIndexGenerator, listening, usingStatics]).then(async () => {
if (!options.ci && !options.smokeTest && options.open) {
openInBrowser(host ? networkAddress : address);
}
});
const managerResult = await managerBuilder.start({
startTime,
startTime: process.hrtime(),
options,
router,
server,
channel: serverChannel,
});
let previewResult;
if (!options.ignorePreview) {
try {
previewResult = await previewBuilder.start({
startTime,
previewResult = await previewBuilder
.start({
startTime: process.hrtime(),
options,
router,
server,
});
} catch (error) {
await managerBuilder?.bail();
// For some reason, even when Webpack fails e.g. wrong main.js config,
// the preview may continue to print to stdout, which can affect output
// when we catch this error and process those errors (e.g. telemetry)
// gets overwritten by preview progress output. Therefore, we should bail the preview too.
await previewBuilder?.bail().catch();
throw error;
}
}
channel: serverChannel,
})
.catch(async (e: any) => {
await managerBuilder?.bail().catch();
// For some reason, even when Webpack fails e.g. wrong main.js config,
// the preview may continue to print to stdout, which can affect output
// when we catch this error and process those errors (e.g. telemetry)
// gets overwritten by preview progress output. Therefore, we should bail the preview too.
await previewBuilder?.bail().catch();
// TODO #13083 Move this to before starting the previewBuilder - when compiling the preview is so fast that it will be done before the browser is done opening
if (!options.ci && !options.smokeTest && options.open) {
openInBrowser(host ? networkAddress : address);
// re-throw the error
throw e;
});
}
return { previewResult, managerResult, address, networkAddress };
}
async function doTelemetry(
core: CoreConfig,
initializedStoryIndexGenerator: Promise<StoryIndexGenerator>,
options: Options
) {
if (!core?.disableTelemetry) {
initializedStoryIndexGenerator.then(async (generator) => {
const storyIndex = await generator?.getIndex();
const { versionCheck, versionUpdates } = options;
const payload = storyIndex
? {
versionStatus: versionUpdates ? versionStatus(versionCheck) : 'disabled',
storyIndex: summarizeIndex(storyIndex),
}
: undefined;
telemetry('dev', payload, { configDir: options.configDir });
});
}
if (!core?.disableProjectJson) {
useStorybookMetadata(router, options.configDir);
}
}
async function getStoryIndexGenerator(
features: {
postcss?: boolean;
buildStoriesJson?: boolean;
previewCsfV3?: boolean;
storyStoreV7?: boolean;
breakingChangesV7?: boolean;
interactionsDebugger?: boolean;
babelModeV7?: boolean;
argTypeTargetsV7?: boolean;
warnOnLegacyHierarchySeparator?: boolean;
},
options: Options,
serverChannel: ServerChannel
) {
let initializedStoryIndexGenerator: Promise<StoryIndexGenerator> = Promise.resolve(undefined);
if (features?.buildStoriesJson || features?.storyStoreV7) {
const workingDir = process.cwd();
const directories = {
configDir: options.configDir,
workingDir,
};
const stories = options.presets.apply('stories');
const storyIndexers = options.presets.apply('storyIndexers', []);
const docsOptions = options.presets.apply<DocsOptions>('docs', {});
const normalizedStories = normalizeStories(await stories, directories);
const generator = new StoryIndexGenerator(normalizedStories, {
...directories,
storyIndexers: await storyIndexers,
docs: await docsOptions,
workingDir,
storiesV2Compatibility: !features?.breakingChangesV7 && !features?.storyStoreV7,
storyStoreV7: features?.storyStoreV7,
});
initializedStoryIndexGenerator = generator.initialize().then(() => generator);
useStoriesJson({
router,
initializedStoryIndexGenerator,
normalizedStories,
serverChannel,
workingDir,
});
}
return initializedStoryIndexGenerator;
}

View File

@ -1,11 +1,7 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="./typings.d.ts" />
export {
getPreviewHeadTemplate,
getPreviewBodyTemplate,
getPreviewMainTemplate,
} from '@storybook/core-common';
export { getPreviewHeadTemplate, getPreviewBodyTemplate } from '@storybook/core-common';
export * from './build-static';
export * from './build-dev';

View File

@ -1,11 +1,6 @@
import fs from 'fs-extra';
import { deprecate } from '@storybook/node-logger';
import {
getPreviewBodyTemplate,
getPreviewHeadTemplate,
getPreviewMainTemplate,
loadEnvs,
} from '@storybook/core-common';
import { getPreviewBodyTemplate, getPreviewHeadTemplate, loadEnvs } from '@storybook/core-common';
import type {
CLIOptions,
CoreCommon_IndexerOptions,
@ -41,8 +36,6 @@ export const previewBody = async (base: any, { configDir, presets }: Options) =>
return getPreviewBodyTemplate(configDir, interpolations);
};
export const previewMainTemplate = () => getPreviewMainTemplate();
export const typescript = () => ({
check: false,
// 'react-docgen' faster but produces lower quality typescript results

View File

@ -268,6 +268,7 @@ export class StoryIndexGenerator {
const importPath = slash(normalizedPath);
const content = await fs.readFile(absolutePath, 'utf8');
const result: {
title?: ComponentTitle;
of?: Path;

View File

@ -1,10 +1,13 @@
import type { Options, CoreConfig, Builder } from '@storybook/types';
async function getManagerBuilder() {
export async function getManagerBuilder(): Promise<Builder<unknown>> {
return import('@storybook/builder-manager');
}
async function getPreviewBuilder(builderName: string, configDir: string) {
export async function getPreviewBuilder(
builderName: string,
configDir: string
): Promise<Builder<unknown>> {
let builderPackage: string;
if (builderName) {
builderPackage = require.resolve(

View File

@ -18,7 +18,7 @@ export class ServerChannel {
});
}
emit(type: string, args: any[] = []) {
emit(type: string, args: any = []) {
const event = { type, args };
const data = stringify(event, { maxDepth: 15, allowFunction: true });
Array.from(this.webSocketServer.clients)

View File

@ -7,7 +7,7 @@ import { dedent } from 'ts-dedent';
export function openInBrowser(address: string) {
getDefaultBrowser(async (err: any, res: any) => {
try {
if (res && (res.isChrome || res.isChromium)) {
if (res && (res.isChrome || res.isChromium || res.identity === 'com.brave.browser')) {
// We use betterOpn for Chrome because it is better at handling which chrome tab
// or window the preview loads in.
betterOpn(address);

View File

@ -899,7 +899,7 @@ describe('useStoriesJson', () => {
});
it('debounces invalidation events', async () => {
(debounce as jest.Mock).mockImplementation(jest.requireActual('lodash/debounce'));
(debounce as jest.Mock).mockImplementation(jest.requireActual('lodash/debounce') as any);
const mockServerChannel = { emit: jest.fn() } as any as ServerChannel;
useStoriesJson({

View File

@ -45,7 +45,7 @@
"@storybook/core-common": "7.0.0-alpha.52",
"@storybook/node-logger": "7.0.0-alpha.52",
"@storybook/types": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"ts-dedent": "^2.0.0"
},
"devDependencies": {

View File

@ -0,0 +1,94 @@
# Preview (Web)
This is the main API for the (web) version of the Storybook Preview.
The preview's job is:
1. Read and update the URL (via the URL Store)
2. Listen to instructions on the channel and emit events as things occur.
3. Render the current selection to the web view in either story or docs mode.
## V7 Store vs Legacy (V6)
The story store is designed to load stories 'on demand', and will operate in this fashion if the `storyStoreV7` feature is enabled.
However, for back-compat reasons, in v6 mode, we need to load all stories, synchronously on bootup, emitting the `SET_STORIES` event.
In V7 mode we do not emit that event, instead preferring the `STORY_PREPARED` event, with the data for the single story being rendered.
## Initialization
The preview is `initialized` in two ways.
### V7 Mode:
- `importFn` - is an async `import()` function
- `getProjectAnnotations` - is a simple function that evaluations `preview.js` and addon config files and combines them. If it errors, the Preview will show the error.
- No `getStoryIndex` function is passed, instead the preview creates a `StoryIndexClient` that pulls `stories.json` from node and watches the event stream for invalidation events.
### V6 Mode
- `importFn` - is a simulated `import()` function, that is synchronous, see `client-api` for details.
- `getProjectAnnotations` - also evaluates `preview.js` et al, but watches for calls to `setStories`, and passes them to the `ClientApi`
- `getStoryIndex` is a local function (that must be called _after_ `getProjectAnnotations`) that gets the list of stories added.
See `client-api` for more details on this process.
## Story Rendering and interruptions
The Preview is split into three parts responsible for state management:
- `PreviewWeb` - which story is rendered, receives events and (maybe) changes/re-renders stories
- `StoryRender` - (imports +) prepares the story, renders it through the various phases
- `DocsRender` - if a story renders in docs mode, it is "transformed" into a `DocsRender` once we know.
A rendering story goes through these phases:
- `preparing` - (maybe async) import the story file and prepare the story function.
- `loading` - async loaders are running
- `rendering` - the `renderToDOM` function for the framework is running
- `playing` - the `play` function is running
- `completed` - the story is done.
It also has two error states:
- `aborted` - the story was stopped midway (see below)
- `errored` - there was an error thrown somewhere along the way.
### Re-rendering and aborting
A story may re-render due to various events, which can have implications if the story is not in the `completed` phase:
- `UPDATE_STORY_ARGS` / `UPDATE_GLOBALS` -- change of inputs
- `FORCE_RE_RENDER` - re-render unchanged
If these events happen during a render:
- if the story is `preparing` or `loading`, leave thing unchanged and let the new `args`/`globals` be picked up by the render phase
- otherwise, use the result of the previous `loaders` run, and simply re-render over the top
- `FORCE_REMOUNT` - remount (or equivalent) the component and re-render.
If this happens during a render, treat `loading` similarly, but:
- if the story is `rendering`, start a new render and abort the previous render immediately afterwards
- if the story is `playing`, attempt to abort the previous play function, and start a new render.
### Changing story
Also the `SET_CURRENT_STORY` event may change the current story. We need to check:
- If the `storyId` changed
- If the `viewMode` changed
- If the story implementation changed (i.e if HMR occurred).
If the _previous_ story is still `preparing`, we cannot know if the implementation changed, so we
abort the preparing immediately, and let the new story take over.
Otherwise, if all of the above are the same, we do nothing.
If they are different, and the old story is not `completed`, we try to abort it immediately. If that fails (e.g. the `play` function doesn't respond to the `abort` event), then we reload the window.

View File

@ -0,0 +1,105 @@
{
"name": "@storybook/preview",
"version": "7.0.0-alpha.52",
"description": "",
"keywords": [
"storybook"
],
"homepage": "https://github.com/storybookjs/storybook/tree/main/code/lib/preview",
"bugs": {
"url": "https://github.com/storybookjs/storybook/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/storybook.git",
"directory": "code/lib/preview"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"license": "MIT",
"sideEffects": false,
"exports": {
".": {
"import": "./dist/runtime.mjs",
"require": "./dist/runtime.js",
"types": "./dist/runtime.d.ts"
},
"./globals": {
"import": "./dist/globals.mjs",
"require": "./dist/globals.js",
"types": "./dist/globals.d.ts"
},
"./package.json": "./package.json"
},
"main": "dist/runtime.js",
"module": "dist/runtime.mjs",
"types": "dist/runtime.d.ts",
"typesVersions": {
"*": {
"*": [
"dist/runtime.d.ts"
],
"globals": [
"dist/globals.d.ts"
]
}
},
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts"
],
"scripts": {
"check": "../../../scripts/node_modules/.bin/tsc --noEmit",
"prep": "../../../scripts/prepare/bundle.ts"
},
"dependencies": {
"@types/shelljs": "^0.8.7",
"fs-extra": "^9.0.1",
"shelljs": "^0.8.5"
},
"devDependencies": {
"@storybook/api": "7.0.0-alpha.52",
"@storybook/channel-postmessage": "7.0.0-alpha.52",
"@storybook/channel-websocket": "7.0.0-alpha.52",
"@storybook/channels": "7.0.0-alpha.52",
"@storybook/client-logger": "7.0.0-alpha.52",
"@storybook/core-client": "7.0.0-alpha.52",
"@storybook/core-common": "7.0.0-alpha.52",
"@storybook/core-events": "7.0.0-alpha.52",
"@storybook/csf": "next",
"@storybook/preview-web": "7.0.0-alpha.52",
"@storybook/router": "7.0.0-alpha.52",
"@storybook/theming": "7.0.0-alpha.52",
"@storybook/types": "7.0.0-alpha.52",
"@types/qs": "^6.9.5",
"@types/webpack-env": "^1.16.4",
"ansi-to-html": "^0.6.11",
"dequal": "^2.0.2",
"global": "^4.4.0",
"lodash": "^4.17.21",
"memoizerific": "^1.11.3",
"qs": "^6.10.0",
"react": "16.14.0",
"slash": "^3.0.0",
"synchronous-promise": "^2.0.15",
"ts-dedent": "^2.0.0",
"ts-jest": "^28.0.8",
"typescript": "~4.6.3",
"util-deprecate": "^1.0.2"
},
"publishConfig": {
"access": "public"
},
"bundler": {
"pre": "./scripts/generate-exports-file.ts",
"entries": [
"./src/runtime.ts",
"./src/globals.ts"
]
},
"gitHead": "d2494e3f51ce0f55bcb1ef693a6477c669fbe666"
}

View File

@ -0,0 +1,40 @@
/* eslint-disable import/no-extraneous-dependencies, no-console */
import fs from 'fs-extra';
import path from 'path';
import shelljs from 'shelljs';
import { dedent } from 'ts-dedent';
const removeDefault = (input: string) => input !== 'default';
const location = path.join(__dirname, '..', 'src', 'globals', 'exports.ts');
const run = async () => {
const { values } = await import('../src/globals/runtime');
const data = Object.entries(values).reduce<Record<string, string[]>>((acc, [key, value]) => {
acc[key] = Object.keys(value).filter(removeDefault);
return acc;
}, {});
console.log('Generating...');
await fs.ensureFile(location);
await fs.writeFile(
location,
dedent`
// this file is generated by generate-exports-file.ts
// this is done to prevent runtime dependencies from making it's way into the build/start script of the manager
// the manager builder needs to know which dependencies are 'globalized' in the ui
export default ${JSON.stringify(data, null, 2)} as const;`
);
console.log('Linting...');
shelljs.exec(`yarn lint:js:cmd --fix ${location}`, {
cwd: path.join(__dirname, '..', '..', '..'),
});
console.log('Done!');
};
run().catch((e) => {
console.error(e);
process.exitCode = 1;
});

View File

@ -0,0 +1 @@
export * from './globals/definitions';

View File

@ -0,0 +1,35 @@
import type { ModuleInfo } from '@fal-works/esbuild-plugin-global-externals';
import Exports from './exports';
import { Keys } from './types';
import type { Definitions } from './types';
/*
* We create a map of a module's name to a ModuleInfo.
* Which is a config object for a esbuild-plugin, to swap a import of a module to a reference of a global variable.
* To get this plugin to do the best job it can, it needs to know all the exports in the ModuleInfo config object.
* We generate this information via a script into `exports.ts`.
*
* It's really important that there are no actual to the runtime of the modules, hence the cumbersome generation.
* But we also want to ensure we don't miss any exports, or globals.
*
* So in order to add additional modules to be swapped for globals, you need to add them to:
* - `Keys` in `types.ts`
* - `values` in `runtime.ts`.
*
* If you forget to do either, TypeScript will complain.
*
* This `definitions.ts` file is consumed by the `builder-*` package,
* The `runtime.ts` file is used inside the preview's browser code runtime.
*/
const createModuleInfo = (m: keyof typeof Keys): Required<ModuleInfo> => ({
type: 'esm',
varName: Keys[m],
namedExports: Exports[m],
defaultExport: true,
});
export const definitions = Object.keys(Keys).reduce<Definitions>((acc, key) => {
acc[key as keyof typeof Keys] = createModuleInfo(key as keyof typeof Keys);
return acc;
}, {} as Definitions);

View File

@ -0,0 +1,200 @@
// this file is generated by generate-exports-file.ts
// this is done to prevent runtime dependencies from making it's way into the build/start script of the manager
// the manager builder needs to know which dependencies are 'globalized' in the ui
export default {
'@storybook/addons': [
'AddonStore',
'HooksContext',
'addons',
'applyHooks',
'isSupportedType',
'makeDecorator',
'mockChannel',
'types',
'useArgs',
'useCallback',
'useChannel',
'useEffect',
'useGlobals',
'useMemo',
'useParameter',
'useReducer',
'useRef',
'useState',
'useStoryContext',
],
'@storybook/channel-postmessage': ['KEY', 'PostmsgTransport', 'createChannel'],
'@storybook/channel-websocket': ['WebsocketTransport', 'createChannel'],
'@storybook/channels': ['Channel'],
'@storybook/client-api': [
'ClientApi',
'addArgTypes',
'addArgTypesEnhancer',
'addArgs',
'addArgsEnhancer',
'addDecorator',
'addLoader',
'addParameters',
'addStepRunner',
'getQueryParam',
'getQueryParams',
'setGlobalRender',
'DEEPLY_EQUAL',
'HooksContext',
'NO_TARGET_NAME',
'StoryStore',
'applyHooks',
'combineArgs',
'combineParameters',
'composeConfigs',
'composeStepRunners',
'composeStories',
'composeStory',
'decorateStory',
'deepDiff',
'defaultDecorateStory',
'filterArgTypes',
'getArrayField',
'getField',
'getObjectField',
'getSingletonField',
'getValuesFromArgTypes',
'groupArgsByTarget',
'inferControls',
'mapArgsToTypes',
'noTargetArgs',
'normalizeComponentAnnotations',
'normalizeInputType',
'normalizeInputTypes',
'normalizeProjectAnnotations',
'normalizeStory',
'prepareStory',
'processCSFFile',
'sanitizeStoryContextUpdate',
'setProjectAnnotations',
'sortStoriesV6',
'sortStoriesV7',
'useAddonState',
'useArgs',
'useCallback',
'useChannel',
'useEffect',
'useGlobals',
'useMemo',
'useParameter',
'useReducer',
'useRef',
'useSharedState',
'useState',
'useStoryContext',
'userOrAutoTitle',
'userOrAutoTitleFromSpecifier',
'validateOptions',
],
'@storybook/client-logger': ['deprecate', 'logger', 'once', 'pretty'],
'@storybook/core-client': ['ClientApi', 'StoryStore', 'start'],
'@storybook/core-events': [
'CHANNEL_CREATED',
'CONFIG_ERROR',
'CURRENT_STORY_WAS_SET',
'DOCS_RENDERED',
'FORCE_REMOUNT',
'FORCE_RE_RENDER',
'GLOBALS_UPDATED',
'IGNORED_EXCEPTION',
'NAVIGATE_URL',
'PLAY_FUNCTION_THREW_EXCEPTION',
'PRELOAD_ENTRIES',
'PREVIEW_BUILDER_PROGRESS',
'PREVIEW_KEYDOWN',
'REGISTER_SUBSCRIPTION',
'RESET_STORY_ARGS',
'SELECT_STORY',
'SET_CONFIG',
'SET_CURRENT_STORY',
'SET_GLOBALS',
'SET_INDEX',
'SET_STORIES',
'SHARED_STATE_CHANGED',
'SHARED_STATE_SET',
'STORIES_COLLAPSE_ALL',
'STORIES_EXPAND_ALL',
'STORY_ARGS_UPDATED',
'STORY_CHANGED',
'STORY_ERRORED',
'STORY_INDEX_INVALIDATED',
'STORY_MISSING',
'STORY_PREPARED',
'STORY_RENDERED',
'STORY_RENDER_PHASE_CHANGED',
'STORY_SPECIFIED',
'STORY_THREW_EXCEPTION',
'STORY_UNCHANGED',
'UPDATE_GLOBALS',
'UPDATE_QUERY_PARAMS',
'UPDATE_STORY_ARGS',
],
'@storybook/preview-web': [
'DocsContext',
'Preview',
'PreviewWeb',
'PreviewWithSelection',
'composeConfigs',
'simulateDOMContentLoaded',
'simulatePageLoad',
],
'@storybook/store': [
'DEEPLY_EQUAL',
'HooksContext',
'NO_TARGET_NAME',
'StoryStore',
'applyHooks',
'combineArgs',
'combineParameters',
'composeConfigs',
'composeStepRunners',
'composeStories',
'composeStory',
'decorateStory',
'deepDiff',
'defaultDecorateStory',
'filterArgTypes',
'getArrayField',
'getField',
'getObjectField',
'getSingletonField',
'getValuesFromArgTypes',
'groupArgsByTarget',
'inferControls',
'mapArgsToTypes',
'noTargetArgs',
'normalizeComponentAnnotations',
'normalizeInputType',
'normalizeInputTypes',
'normalizeProjectAnnotations',
'normalizeStory',
'prepareStory',
'processCSFFile',
'sanitizeStoryContextUpdate',
'setProjectAnnotations',
'sortStoriesV6',
'sortStoriesV7',
'useAddonState',
'useArgs',
'useCallback',
'useChannel',
'useEffect',
'useGlobals',
'useMemo',
'useParameter',
'useReducer',
'useRef',
'useSharedState',
'useState',
'useStoryContext',
'userOrAutoTitle',
'userOrAutoTitleFromSpecifier',
'validateOptions',
],
} as const;

View File

@ -0,0 +1,26 @@
import * as ADDONS from '../modules/addons';
import * as CHANNEL_POSTMESSAGE from '../modules/channel-postmessage';
import * as CHANNEL_WEBSOCKET from '../modules/channel-websocket';
import * as CHANNELS from '../modules/channels';
import * as CLIENT_API from '../modules/client-api';
import * as CLIENT_LOGGER from '../modules/client-logger';
import * as CORE_CLIENT from '../modules/core-client';
import * as CORE_EVENTS from '../modules/core-events';
import * as PREVIEW_WEB from '../modules/preview-web';
import * as STORE from '../modules/store';
import type { Keys } from './types';
// Here we map the name of a module to their VALUE in the global scope.
export const values: Required<Record<keyof typeof Keys, any>> = {
'@storybook/addons': ADDONS,
'@storybook/channel-postmessage': CHANNEL_POSTMESSAGE,
'@storybook/channel-websocket': CHANNEL_WEBSOCKET,
'@storybook/channels': CHANNELS,
'@storybook/client-api': CLIENT_API,
'@storybook/client-logger': CLIENT_LOGGER,
'@storybook/core-client': CORE_CLIENT,
'@storybook/core-events': CORE_EVENTS,
'@storybook/preview-web': PREVIEW_WEB,
'@storybook/store': STORE,
};

View File

@ -0,0 +1,17 @@
import type { ModuleInfo } from '@fal-works/esbuild-plugin-global-externals';
// Here we map the name of a module to their NAME in the global scope.
export enum Keys {
'@storybook/addons' = '__STORYBOOK_MODULE_ADDONS__',
'@storybook/channel-postmessage' = '__STORYBOOK_MODULE_CHANNEL_POSTMESSAGE__',
'@storybook/channel-websocket' = '__STORYBOOK_MODULE_CHANNEL_WEBSOCKET__',
'@storybook/channels' = '__STORYBOOK_MODULE_CHANNELS__',
'@storybook/client-api' = '__STORYBOOK_MODULE_CLIENT_API__',
'@storybook/client-logger' = '__STORYBOOK_MODULE_CLIENT_LOGGER__',
'@storybook/core-client' = '__STORYBOOK_MODULE_CORE_CLIENT__',
'@storybook/core-events' = '__STORYBOOK_MODULE_CORE_EVENTS__',
'@storybook/preview-web' = '__STORYBOOK_MODULE_PREVIEW_WEB__',
'@storybook/store' = '__STORYBOOK_MODULE_STORE__',
}
export type Definitions = Required<Record<keyof typeof Keys, Required<ModuleInfo>>>;

View File

@ -0,0 +1,2 @@
/* eslint-disable import/no-extraneous-dependencies */
export * from '@storybook/addons';

View File

@ -0,0 +1,2 @@
/* eslint-disable import/no-extraneous-dependencies */
export * from '@storybook/channel-postmessage';

View File

@ -0,0 +1,2 @@
/* eslint-disable import/no-extraneous-dependencies */
export * from '@storybook/channel-websocket';

View File

@ -0,0 +1,2 @@
/* eslint-disable import/no-extraneous-dependencies */
export * from '@storybook/channels';

View File

@ -0,0 +1,2 @@
/* eslint-disable import/no-extraneous-dependencies */
export * from '@storybook/client-api';

View File

@ -0,0 +1,2 @@
/* eslint-disable import/no-extraneous-dependencies */
export * from '@storybook/client-logger';

View File

@ -0,0 +1,2 @@
/* eslint-disable import/no-extraneous-dependencies */
export * from '@storybook/core-client';

View File

@ -0,0 +1,2 @@
/* eslint-disable import/no-extraneous-dependencies */
export * from '@storybook/core-events';

View File

@ -0,0 +1,2 @@
/* eslint-disable import/no-extraneous-dependencies */
export * from '@storybook/preview-web';

View File

@ -0,0 +1,2 @@
/* eslint-disable import/no-extraneous-dependencies */
export * from '@storybook/store';

View File

@ -0,0 +1,9 @@
import { values } from './globals/runtime';
import { Keys } from './globals/types';
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;
// Apply all the globals
getKeys(Keys).forEach((key) => {
(globalThis as any)[Keys[key]] = values[key];
});

View File

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"strict": true,
"strictPropertyInitialization": false,
"useUnknownInCatchVariables": false,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": []
}

View File

@ -57,7 +57,7 @@
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@types/fs-extra": "^9.0.6",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"deep-object-diff": "^1.1.0",
"fs-extra": "^9.0.1",
"global": "^4.4.0",

View File

@ -48,7 +48,7 @@
},
"devDependencies": {
"@storybook/csf": "next",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"synchronous-promise": "^2.0.15",
"typescript": "^4.9.3"
},

View File

@ -15,6 +15,10 @@ import type { Parameters, Tag } from './csf';
export type BuilderName = 'webpack5' | '@storybook/builder-webpack5' | string;
export type RendererName = string;
interface ServerChannel {
emit(type: string, args?: any): void;
}
export interface CoreConfig {
builder?:
| BuilderName
@ -188,6 +192,7 @@ export interface Builder<Config, BuilderStats extends Stats = Stats> {
startTime: ReturnType<typeof process.hrtime>;
router: Router;
server: Server;
channel: ServerChannel;
}) => Promise<void | {
stats?: BuilderStats;
totalTime: ReturnType<typeof process.hrtime>;

View File

@ -244,7 +244,7 @@
"@types/fs-extra": "^9.0.6",
"@types/js-yaml": "^3.12.6",
"@types/lodash": "^4.14.167",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"@types/node-cleanup": "^2.1.1",
"@types/prompts": "2.0.11",
"@types/react": "^16.14.34",

View File

@ -51,7 +51,7 @@
},
"dependencies": {
"@storybook/core-webpack": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"html-loader": "^3.1.0",
"react": "16.14.0",
"react-dom": "16.14.0",

View File

@ -52,7 +52,7 @@
"dependencies": {
"@babel/plugin-transform-react-jsx": "^7.19.0",
"@storybook/core-webpack": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"react": "16.14.0",
"react-dom": "16.14.0"
},

View File

@ -79,7 +79,7 @@
"@storybook/node-logger": "7.0.0-alpha.52",
"@storybook/react": "7.0.0-alpha.52",
"@storybook/react-docgen-typescript-plugin": "1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"@types/semver": "^7.3.4",
"babel-plugin-add-react-displayname": "^0.0.5",
"babel-plugin-react-docgen": "^4.2.1",

View File

@ -58,7 +58,7 @@
"@storybook/core-server": "7.0.0-alpha.52",
"@storybook/core-webpack": "7.0.0-alpha.52",
"@storybook/server": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"global": "^4.4.0",
"react": "16.14.0",
"react-dom": "16.14.0",

View File

@ -62,7 +62,7 @@
"dependencies": {
"@storybook/core-webpack": "7.0.0-alpha.52",
"@storybook/docs-tools": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"react": "16.14.0",
"react-dom": "16.14.0",
"ts-loader": "^9.2.8",

View File

@ -62,7 +62,7 @@
"dependencies": {
"@storybook/core-webpack": "7.0.0-alpha.52",
"@storybook/docs-tools": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"react": "16.14.0",
"react-dom": "16.14.0",
"ts-loader": "^9.2.8",

View File

@ -56,7 +56,7 @@
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/preset-env": "^7.20.2",
"@storybook/core-webpack": "7.0.0-alpha.52",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"babel-loader": "^7.0.0 || ^8.0.0",
"babel-plugin-bundled-import-meta": "^0.3.1",
"react": "16.14.0",

View File

@ -59,7 +59,7 @@
"@storybook/store": "7.0.0-alpha.52",
"@storybook/types": "7.0.0-alpha.52",
"@types/estree": "^0.0.51",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"acorn": "^7.4.1",
"acorn-jsx": "^5.3.1",
"acorn-walk": "^7.2.0",

View File

@ -1,13 +1,10 @@
import global from 'global';
import { transparentize } from 'polished';
import type { ComponentProps, FC } from 'react';
import React, { useEffect, useState } from 'react';
import React from 'react';
import { styled, keyframes } from '@storybook/theming';
import { Icons } from '../icon/icon';
import { rotate360 } from '../shared/animation';
const { EventSource, CONFIG_TYPE } = global;
const LoaderWrapper = styled.div<{ size?: number }>(({ size = 32 }) => ({
borderRadius: '50%',
cursor: 'progress',
@ -107,7 +104,7 @@ interface LoaderProps {
size?: number;
}
export const PureLoader: FC<LoaderProps & ComponentProps<typeof ProgressWrapper>> = ({
export const Loader: FC<LoaderProps & ComponentProps<typeof ProgressWrapper>> = ({
progress,
error,
size,
@ -158,36 +155,3 @@ export const PureLoader: FC<LoaderProps & ComponentProps<typeof ProgressWrapper>
/>
);
};
export const Loader: FC<ComponentProps<typeof PureLoader>> = (props) => {
const [progress, setProgress] = useState(undefined);
const [error, setError] = useState(undefined);
useEffect(() => {
// Don't listen for progress updates in static builds
// Event source is not defined in IE 11
if (CONFIG_TYPE !== 'DEVELOPMENT' || !EventSource) return undefined;
const eventSource = new EventSource('/progress');
let lastProgress: Progress;
eventSource.onmessage = (event: any) => {
try {
lastProgress = JSON.parse(event.data);
setProgress(lastProgress);
} catch (e) {
setError(e);
eventSource.close();
}
};
eventSource.onerror = () => {
if (lastProgress && lastProgress.value !== 1) setError(new Error('Connection closed'));
eventSource.close();
};
return () => eventSource.close();
}, []);
return <PureLoader progress={progress} error={error} {...props} />;
};

View File

@ -72,7 +72,7 @@ export { StorybookLogo } from './brand/StorybookLogo';
export { StorybookIcon } from './brand/StorybookIcon';
// Loader
export { Loader } from './Loader/Loader';
export { Loader, PureLoader } from './Loader/Loader';
// Utils
export { getStoryHref } from './utils/getStoryHref';

View File

@ -4,14 +4,14 @@ import path from 'path';
import shelljs from 'shelljs';
import { dedent } from 'ts-dedent';
const remove = () => (input: string) => input !== 'default';
const removeDefault = (input: string) => input !== 'default';
const location = path.join(__dirname, '..', 'src', 'globals', 'exports.ts');
const run = async () => {
const { values } = await import('../src/globals/runtime');
const data = Object.entries(values).reduce<Record<string, string[]>>((acc, [key, value]) => {
acc[key] = Object.keys(value).filter(remove());
acc[key] = Object.keys(value).filter(removeDefault);
return acc;
}, {});

View File

@ -1,9 +1,9 @@
import type { ReactElement } from 'react';
import React, { Component, Fragment } from 'react';
import { shortcutToHumanString } from '@storybook/api/shortcut';
import { styled } from '@storybook/theming';
import { Tabs, Icons, IconButton } from '@storybook/components';
import type { State } from '@storybook/api';
import { shortcutToHumanString } from '@storybook/api';
const DesktopOnlyIconButton = styled(IconButton)({
// Hides full screen icon at mobile breakpoint defined in app.js

View File

@ -1,11 +1,12 @@
import React, { Fragment, useMemo, useEffect, useRef } from 'react';
import React, { Fragment, useMemo, useEffect, useRef, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import global from 'global';
import { type API, Consumer, type Combo, merge } from '@storybook/api';
import { SET_CURRENT_STORY } from '@storybook/core-events';
import { PREVIEW_BUILDER_PROGRESS, SET_CURRENT_STORY } from '@storybook/core-events';
import { addons, types, type Addon } from '@storybook/addons';
import { Loader } from '@storybook/components';
import { PureLoader } from '@storybook/components';
import { Location } from '@storybook/router';
import * as S from './utils/components';
@ -59,9 +60,23 @@ const createCanvas = (id: string, baseUrl = 'iframe.html', withLoader = true): A
[getElements, ...defaultWrappers]
);
const [progress, setProgress] = useState(undefined);
useEffect(() => {
if (global.CONFIG_TYPE === 'DEVELOPMENT') {
const channel = addons.getServerChannel();
channel.on(PREVIEW_BUILDER_PROGRESS, (options) => {
setProgress(options);
});
}
}, []);
const refLoading = !!refs[refId] && !refs[refId].ready;
const rootLoading = !refId && !(progress?.value === 1 || progress === undefined);
const isLoading = entry
? !!refs[refId] && !refs[refId].ready
: !storiesFailed && !storiesConfigured;
? refLoading || rootLoading
: (!storiesFailed && !storiesConfigured) || rootLoading;
return (
<ZoomConsumer>
@ -70,7 +85,7 @@ const createCanvas = (id: string, baseUrl = 'iframe.html', withLoader = true): A
<>
{withLoader && isLoading && (
<S.LoaderWrapper>
<Loader id="preview-loader" role="progressbar" />
<PureLoader id="preview-loader" role="progressbar" progress={progress} />
</S.LoaderWrapper>
)}
<ApplyWrappers

View File

@ -4,8 +4,15 @@ import React, { Fragment, useMemo } from 'react';
import { styled } from '@storybook/theming';
import { FlexBar, IconButton, Icons, Separator, TabButton, TabBar } from '@storybook/components';
import { Consumer, type Combo, type API, type State, merge, type LeafEntry } from '@storybook/api';
import { shortcutToHumanString } from '@storybook/api/shortcut';
import {
shortcutToHumanString,
Consumer,
type Combo,
type API,
type State,
merge,
type LeafEntry,
} from '@storybook/api';
import { addons, type Addon, types } from '@storybook/addons';
import { Location, type RenderData } from '@storybook/router';

View File

@ -5,7 +5,7 @@ import { Badge } from '@storybook/components';
import type { API } from '@storybook/api';
import { styled, useTheme } from '@storybook/theming';
import { shortcutToHumanString } from '@storybook/api/shortcut';
import { shortcutToHumanString } from '@storybook/api';
import { MenuItemIcon } from '../components/sidebar/Menu';
const focusableUIElements = {

View File

@ -78,6 +78,7 @@ export default {
'P',
'Placeholder',
'Pre',
'PureLoader',
'ResetWrapper',
'ScrollArea',
'Separator',
@ -123,6 +124,7 @@ export default {
'NAVIGATE_URL',
'PLAY_FUNCTION_THREW_EXCEPTION',
'PRELOAD_ENTRIES',
'PREVIEW_BUILDER_PROGRESS',
'PREVIEW_KEYDOWN',
'REGISTER_SUBSCRIPTION',
'RESET_STORY_ARGS',
@ -200,7 +202,10 @@ export default {
'ManagerContext',
'Provider',
'combineParameters',
'eventToShortcut',
'merge',
'shortcutMatchesShortcut',
'shortcutToHumanString',
'useAddonState',
'useArgTypes',
'useArgs',

View File

@ -25,13 +25,14 @@ class ReactProvider extends Provider {
constructor() {
super();
const channel = postMessage.createChannel({ page: 'manager' });
const postMessageChannel = postMessage.createChannel({ page: 'manager' });
addons.setChannel(channel);
channel.emit(CHANNEL_CREATED);
addons.setChannel(postMessageChannel);
postMessageChannel.emit(CHANNEL_CREATED);
this.addons = addons;
this.channel = channel;
this.channel = postMessageChannel;
if (FEATURES?.storyStoreV7 && SERVER_CHANNEL_URL) {
const serverChannel = webSocket.createChannel({ url: SERVER_CHANNEL_URL });

View File

@ -2,11 +2,7 @@ import type { ComponentProps, FC } from 'react';
import React, { Component } from 'react';
import { styled, keyframes } from '@storybook/theming';
import {
eventToShortcut,
shortcutToHumanString,
shortcutMatchesShortcut,
} from '@storybook/api/shortcut';
import { eventToShortcut, shortcutToHumanString, shortcutMatchesShortcut } from '@storybook/api';
import { Form, Icons } from '@storybook/components';
import SettingsFooter from './SettingsFooter';

File diff suppressed because it is too large Load Diff

View File

@ -81,7 +81,7 @@
"@types/express": "^4.17.11",
"@types/fs-extra": "^9.0.6",
"@types/js-yaml": "^3.12.6",
"@types/node": "^16.0.0 || ^18.0.0",
"@types/node": "^16.0.0",
"@types/node-cleanup": "^2.1.1",
"@types/node-fetch": "^2.5.7",
"@types/prompts": "2.0.11",

View File

@ -19,7 +19,7 @@ export const compile: Task = {
// To check if the code has been compiled as we need, we check the compiled output of
// `@storybook/store`. To check if it has been built for publishing (i.e. `--no-link`),
// we check if it built types or references source files directly.
const contents = await readFile(resolve(codeDir, './lib/store/dist/index.d.ts'), 'utf8');
const contents = await readFile(resolve(codeDir, './lib/preview/dist/runtime.d.ts'), 'utf8');
const isLinkedContents = contents.indexOf(linkedContents) !== -1;
if (link) return isLinkedContents;
return !isLinkedContents;

View File

@ -3659,7 +3659,7 @@ __metadata:
"@types/fs-extra": ^9.0.6
"@types/js-yaml": ^3.12.6
"@types/lodash": ^4
"@types/node": ^16.0.0 || ^18.0.0
"@types/node": ^16.0.0
"@types/node-cleanup": ^2.1.1
"@types/node-fetch": ^2.5.7
"@types/prompts": 2.0.11
@ -4257,10 +4257,10 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^16.0.0 || ^18.0.0":
version: 18.11.9
resolution: "@types/node@npm:18.11.9"
checksum: aeaa925406f841c41679b32def9391a9892171e977105e025050e9f66e2830b4c50d0d974a1af0077ead3337a1f3bdf49ee7e7f402ebf2e034a3f97d9d240dba
"@types/node@npm:^16.0.0":
version: 16.18.3
resolution: "@types/node@npm:16.18.3"
checksum: 058ddd61a3d39f517bc9c30b82b9d6257d903e84c42ba66aae63bd13203b6deb2acf7f7e14caefd5d7cebadbe8c90604c04f9851cd41cd6a1bc2fc4dcec85f01
languageName: node
linkType: hard