Merge branch 'next' into docs_updates_contributions

This commit is contained in:
jonniebigodes 2023-11-20 15:19:02 +00:00 committed by GitHub
commit 6a94ba89e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 619 additions and 216 deletions

View File

@ -1,4 +1,4 @@
import { dirname, isAbsolute, join, resolve } 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';
@ -7,25 +7,20 @@ import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
import TerserWebpackPlugin from 'terser-webpack-plugin';
import VirtualModulePlugin from 'webpack-virtual-modules';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import slash from 'slash';
import type { TransformOptions as EsbuildOptions } from 'esbuild';
import type { JsMinifyOptions as SwcOptions } from '@swc/core';
import type { Options, CoreConfig, DocsOptions, PreviewAnnotation } from '@storybook/types';
import type { Options, CoreConfig, DocsOptions } from '@storybook/types';
import { globalsNameReferenceMap } from '@storybook/preview/globals';
import {
getBuilderOptions,
getRendererName,
stringifyProcessEnvs,
handlebars,
interpolate,
normalizeStories,
readTemplate,
loadPreviewOrConfigFile,
isPreservingSymlinks,
} from '@storybook/core-common';
import { toRequireContextString, toImportFn } from '@storybook/core-webpack';
import type { BuilderOptions } from '@storybook/core-webpack';
import { getVirtualModuleMapping } from '@storybook/core-webpack';
import { dedent } from 'ts-dedent';
import type { BuilderOptions, TypescriptOptions } from '../types';
import type { TypescriptOptions } from '../types';
import { createBabelLoader, createSWCLoader } from './loaders';
const getAbsolutePath = <I extends string>(input: I): I =>
@ -114,92 +109,6 @@ export default async (
const builderOptions = await getBuilderOptions<BuilderOptions>(options);
const previewAnnotations = [
...(await presets.apply<PreviewAnnotation[]>('previewAnnotations', [], options)).map(
(entry) => {
// If entry is an object, use the absolute import specifier.
// This is to maintain back-compat with community addons that bundle other addons
// and package managers that "hide" sub dependencies (e.g. pnpm / yarn pnp)
// The vite builder uses the bare import specifier.
if (typeof entry === 'object') {
return entry.absolute;
}
// TODO: Remove as soon as we drop support for disabled StoryStoreV7
if (isAbsolute(entry)) {
return entry;
}
return slash(entry);
}
),
loadPreviewOrConfigFile(options),
].filter(Boolean);
const virtualModuleMapping: Record<string, string> = {};
if (features?.storyStoreV7) {
const storiesFilename = 'storybook-stories.js';
const storiesPath = resolve(join(workingDir, storiesFilename));
const needPipelinedImport = !!builderOptions.lazyCompilation && !isProd;
virtualModuleMapping[storiesPath] = toImportFn(stories, { needPipelinedImport });
const configEntryPath = resolve(join(workingDir, 'storybook-config-entry.js'));
virtualModuleMapping[configEntryPath] = handlebars(
await readTemplate(
require.resolve(
'@storybook/builder-webpack5/templates/virtualModuleModernEntry.js.handlebars'
)
),
{
storiesFilename,
previewAnnotations,
}
// We need to double escape `\` for webpack. We may have some in windows paths
).replace(/\\/g, '\\\\');
entries.push(configEntryPath);
} else {
const rendererName = await getRendererName(options);
const rendererInitEntry = resolve(join(workingDir, 'storybook-init-renderer-entry.js'));
virtualModuleMapping[rendererInitEntry] = `import '${slash(rendererName)}';`;
entries.push(rendererInitEntry);
const entryTemplate = await readTemplate(
join(__dirname, '..', '..', 'templates', 'virtualModuleEntry.template.js')
);
previewAnnotations.forEach((previewAnnotationFilename: string | undefined) => {
if (!previewAnnotationFilename) return;
// Ensure that relative paths end up mapped to a filename in the cwd, so a later import
// of the `previewAnnotationFilename` in the template works.
const entryFilename = previewAnnotationFilename.startsWith('.')
? `${previewAnnotationFilename.replace(/(\w)(\/|\\)/g, '$1-')}-generated-config-entry.js`
: `${previewAnnotationFilename}-generated-config-entry.js`;
// NOTE: although this file is also from the `dist/cjs` directory, it is actually a ESM
// file, see https://github.com/storybookjs/storybook/pull/16727#issuecomment-986485173
virtualModuleMapping[entryFilename] = interpolate(entryTemplate, {
previewAnnotationFilename,
});
entries.push(entryFilename);
});
if (stories.length > 0) {
const storyTemplate = await readTemplate(
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 = resolve(join(workingDir, `generated-stories-entry.cjs`));
virtualModuleMapping[storiesFilename] = interpolate(storyTemplate, {
rendererName,
})
// Make sure we also replace quotes for this one
.replace("'{{stories}}'", stories.map(toRequireContextString).join(','));
entries.push(storiesFilename);
}
}
const shouldCheckTs =
typescriptOptions.check && !typescriptOptions.skipBabel && !typescriptOptions.skipCompiler;
const tsCheckOptions = typescriptOptions.checkOptions || {};
@ -226,6 +135,12 @@ export default async (
externals['@storybook/blocks'] = '__STORYBOOK_BLOCKS_EMPTY_MODULE__';
}
const virtualModuleMapping = await getVirtualModuleMapping(options);
Object.keys(virtualModuleMapping).forEach((key) => {
entries.push(key);
});
return {
name: 'preview',
mode: isProd ? 'production' : 'development',

View File

@ -10,6 +10,7 @@ export const createBabelLoader = (
typescriptOptions: TypescriptOptions,
excludes: string[] = []
) => {
logger.info(dedent`Using Babel compiler`);
return {
test: typescriptOptions.skipBabel ? /\.(mjs|jsx?)$/ : /\.(mjs|tsx?|jsx?)$/,
use: [
@ -24,9 +25,7 @@ export const createBabelLoader = (
};
export const createSWCLoader = async (excludes: string[] = [], options: Options) => {
logger.warn(dedent`
The SWC loader is an experimental feature and may change or even be removed at any time.
`);
logger.info(dedent`Using SWC compiler`);
const swc = await options.presets.apply('swc', {}, options);
const typescriptOptions = await options.presets.apply<{ skipCompiler?: boolean }>(
@ -49,12 +48,8 @@ export const createSWCLoader = async (excludes: string[] = [], options: Options)
};
return {
test: typescriptOptions.skipCompiler ? /\.(mjs|cjs|jsx?)$/ : /\.(mjs|cjs|tsx?|jsx?)$/,
use: [
{
loader: require.resolve('swc-loader'),
options: config,
},
],
loader: require.resolve('swc-loader'),
options: config,
include: [getProjectRoot()],
exclude: [/node_modules/, ...excludes],
};

View File

@ -31,18 +31,22 @@ test.describe('addon-controls', () => {
);
const toggle = sbPage.panelContent().locator('input[name=primary]');
await toggle.click();
await expect(sbPage.previewRoot().locator('button')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)'
);
await expect(async () => {
await expect(sbPage.previewRoot().locator('button')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)'
);
}).toPass();
// Color picker: Background color
const color = sbPage.panelContent().locator('input[placeholder="Choose color..."]');
await color.fill('red');
await expect(sbPage.previewRoot().locator('button')).toHaveCSS(
'background-color',
'rgb(255, 0, 0)'
);
await expect(async () => {
await expect(sbPage.previewRoot().locator('button')).toHaveCSS(
'background-color',
'rgb(255, 0, 0)'
);
}).toPass();
// TODO: enable this once the controls for size are aligned in all CLI templates.
// Radio buttons: Size

View File

@ -122,7 +122,12 @@ export default {
framework: {
// name: '@storybook/react-webpack5', // Remove this
name: '@storybook/nextjs', // Add this
options: {},
options: {
builder: {
// Set useSWC to true if you want to try out the experimental SWC compiler in Next.js >= 14.0.0
useSWC: true,
},
},
},
};
```

View File

@ -36,6 +36,11 @@
"types": "./dist/preset.d.ts",
"require": "./dist/preset.js"
},
"./font/webpack/loader/storybook-nextjs-font-loader": {
"types": "./dist/font/webpack/loader/storybook-nextjs-font-loader.d.ts",
"require": "./dist/font/webpack/loader/storybook-nextjs-font-loader.js",
"import": "./dist/font/webpack/loader/storybook-nextjs-font-loader.mjs"
},
"./dist/preview.mjs": "./dist/preview.mjs",
"./next-image-loader-stub.js": {
"types": "./dist/next-image-loader-stub.d.ts",
@ -83,10 +88,12 @@
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.2",
"@babel/runtime": "^7.23.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@storybook/addon-actions": "workspace:*",
"@storybook/builder-webpack5": "workspace:*",
"@storybook/core-common": "workspace:*",
"@storybook/core-events": "workspace:*",
"@storybook/core-webpack": "workspace:*",
"@storybook/node-logger": "workspace:*",
"@storybook/preset-react-webpack": "workspace:*",
"@storybook/preview-api": "workspace:*",
@ -117,7 +124,7 @@
"@types/babel__plugin-transform-runtime": "^7",
"@types/babel__preset-env": "^7",
"@types/loader-utils": "^2.0.5",
"next": "^14.0.0",
"next": "^14.0.2",
"typescript": "^4.9.3",
"webpack": "^5.65.0"
},
@ -156,7 +163,8 @@
"./src/images/next-future-image.tsx",
"./src/images/next-legacy-image.tsx",
"./src/images/next-image.tsx",
"./src/font/webpack/loader/storybook-nextjs-font-loader.ts"
"./src/font/webpack/loader/storybook-nextjs-font-loader.ts",
"./src/swc/next-swc-loader-patch.ts"
],
"externals": [
"sb-original/next/image",

View File

@ -34,6 +34,9 @@ export const configureCss = (baseConfig: WebpackConfig, nextConfig: NextConfig):
},
require.resolve('postcss-loader'),
],
// We transform the "target.css" files from next.js into Javascript
// for Next.js to support fonts, so it should be ignored by the css-loader.
exclude: /next\/.*\/target.css$/,
};
}
});

View File

@ -1,14 +1,22 @@
import type { Configuration } from 'webpack';
export function configureNextFont(baseConfig: Configuration) {
baseConfig.plugins = [...(baseConfig.plugins || [])];
baseConfig.resolveLoader = {
...baseConfig.resolveLoader,
alias: {
...baseConfig.resolveLoader?.alias,
'storybook-nextjs-font-loader': require.resolve(
'./font/webpack/loader/storybook-nextjs-font-loader'
),
},
};
export function configureNextFont(baseConfig: Configuration, isSWC?: boolean) {
const fontLoaderPath = require.resolve(
'@storybook/nextjs/font/webpack/loader/storybook-nextjs-font-loader'
);
if (isSWC) {
baseConfig.module?.rules?.push({
test: /next\/.*\/target.css$/,
loader: fontLoaderPath,
});
} else {
baseConfig.resolveLoader = {
...baseConfig.resolveLoader,
alias: {
...baseConfig.resolveLoader?.alias,
'storybook-nextjs-font-loader': fontLoaderPath,
},
};
}
}

View File

@ -1,6 +1,7 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import loaderUtils from 'next/dist/compiled/loader-utils3';
import { getProjectRoot } from '@storybook/core-common';
import path from 'path';
import type { LoaderOptions } from '../types';
@ -11,7 +12,9 @@ export async function getFontFaceDeclarations(options: LoaderOptions, rootContex
const localFontSrc = options.props.src as LocalFontSrc;
// Parent folder relative to the root context
const parentFolder = path.dirname(options.filename).replace(rootContext, '');
const parentFolder = path
.dirname(path.join(getProjectRoot(), options.filename))
.replace(rootContext, '');
const { validateData } = require('../utils/local-font-utils');
const { weight, style, variable } = validateData('', options.props);

View File

@ -14,18 +14,34 @@ type FontFaceDeclaration = {
};
export default async function storybookNextjsFontLoader(this: any) {
const options = this.getOptions() as LoaderOptions;
const loaderOptions = this.getOptions() as LoaderOptions;
let options;
if (Object.keys(loaderOptions).length > 0) {
// handles Babel mode
options = loaderOptions;
} else {
// handles SWC mode
const importQuery = JSON.parse(this.resourceQuery.slice(1));
options = {
filename: importQuery.path,
fontFamily: importQuery.import,
props: importQuery.arguments[0],
source: this.context.replace(this.rootContext, ''),
};
}
// get execution context
const rootCtx = this.rootContext;
let fontFaceDeclaration: FontFaceDeclaration | undefined;
if (options.source === 'next/font/google' || options.source === '@next/font/google') {
if (options.source.endsWith('next/font/google') || options.source.endsWith('@next/font/google')) {
fontFaceDeclaration = await getGoogleFontFaceDeclarations(options);
}
if (options.source === 'next/font/local' || options.source === '@next/font/local') {
if (options.source.endsWith('next/font/local') || options.source.endsWith('@next/font/local')) {
fontFaceDeclaration = await getLocalFontFaceDeclarations(options, rootCtx);
}

View File

@ -17,6 +17,7 @@ import { configureNextFont } from './font/webpack/configureNextFont';
import nextBabelPreset from './babel/preset';
import { configureNodePolyfills } from './nodePolyfills/webpack';
import { configureAliasing } from './dependency-map';
import { configureSWCLoader } from './swc/loader';
export const addons: PresetProperty<'addons', StorybookConfig> = [
dirname(require.resolve(join('@storybook/preset-react-webpack', 'package.json'))),
@ -61,7 +62,9 @@ export const core: PresetProperty<'core', StorybookConfig> = async (config, opti
name: dirname(
require.resolve(join('@storybook/builder-webpack5', 'package.json'))
) as '@storybook/builder-webpack5',
options: typeof framework === 'string' ? {} : framework.options.builder || {},
options: {
...(typeof framework === 'string' ? {} : framework.options.builder || {}),
},
},
renderer: dirname(require.resolve(join('@storybook/react', 'package.json'))),
};
@ -135,7 +138,7 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig,
const frameworkOptions = await options.presets.apply<{ options: FrameworkOptions }>(
'frameworkOptions'
);
const { options: { nextConfigPath } = {} } = frameworkOptions;
const { options: { nextConfigPath, builder } = {} } = frameworkOptions;
const nextConfig = await configureConfig({
baseConfig,
nextConfigPath,
@ -143,7 +146,7 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig,
});
configureAliasing(baseConfig);
configureNextFont(baseConfig);
configureNextFont(baseConfig, builder?.useSWC);
configureNextImport(baseConfig);
configureRuntimeNextjsVersionResolution(baseConfig);
configureImports({ baseConfig, configDir: options.configDir });
@ -152,5 +155,10 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig,
configureStyledJsx(baseConfig);
configureNodePolyfills(baseConfig);
// TODO: In Storybook 8.0, we have to check whether the babel-compiler addon is used. Otherwise, swc should be used.
if (builder?.useSWC) {
await configureSWCLoader(baseConfig, options, nextConfig);
}
return baseConfig;
};

View File

@ -0,0 +1,68 @@
import { getProjectRoot } from '@storybook/core-common';
import { getVirtualModuleMapping } from '@storybook/core-webpack';
import type { Options } from '@storybook/types';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import type { NextConfig } from 'next';
import path from 'path';
import type { RuleSetRule } from 'webpack';
import semver from 'semver';
import { NextjsSWCNotSupportedError } from 'lib/core-events/src/errors/server-errors';
import { getNextjsVersion } from '../utils';
export const configureSWCLoader = async (
baseConfig: any,
options: Options,
nextConfig: NextConfig
) => {
const isDevelopment = options.configType !== 'PRODUCTION';
const version = getNextjsVersion();
if (semver.lt(version, '14.0.0')) {
throw new NextjsSWCNotSupportedError();
}
const dir = getProjectRoot();
baseConfig.plugins = [
...baseConfig.plugins,
new ReactRefreshWebpackPlugin({
overlay: {
sockIntegration: 'whm',
},
}),
];
const virtualModules = await getVirtualModuleMapping(options);
baseConfig.module.rules = [
// TODO: Remove filtering in Storybook 8.0
...baseConfig.module.rules.filter((r: RuleSetRule) => {
return !r.loader?.includes('swc-loader');
}),
{
test: /\.(m?(j|t)sx?)$/,
include: [getProjectRoot()],
exclude: [/(node_modules)/, ...Object.keys(virtualModules)],
enforce: 'post',
use: {
// we use our own patch because we need to remove tracing from the original code
// which is not possible otherwise
loader: require.resolve('./swc/next-swc-loader-patch.js'),
options: {
isServer: false,
rootDir: dir,
pagesDir: `${dir}/pages`,
appDir: `${dir}/apps`,
hasReactRefresh: isDevelopment,
nextConfig,
supportedBrowsers: require('next/dist/build/utils').getSupportedBrowsers(
dir,
isDevelopment
),
swcCacheDir: path.join(dir, nextConfig?.distDir ?? '.next', 'cache', 'swc'),
bundleTarget: 'default',
},
},
},
];
};

View File

@ -0,0 +1,193 @@
// THIS IS A PATCH over the original code from Next 14.0.0
// we use our own patch because we need to remove tracing from the original code
// which is not possible otherwise
/* eslint-disable no-restricted-syntax */
/*
Copyright (c) 2017 The swc Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
import type { NextConfig } from 'next';
import { isWasm, transform } from 'next/dist/build/swc';
import { getLoaderSWCOptions } from 'next/dist/build/swc/options';
import path, { isAbsolute } from 'path';
export interface SWCLoaderOptions {
rootDir: string;
isServer: boolean;
pagesDir?: string;
appDir?: string;
hasReactRefresh: boolean;
optimizeServerReact?: boolean;
nextConfig: NextConfig;
jsConfig: any;
supportedBrowsers: string[] | undefined;
swcCacheDir: string;
serverComponents?: boolean;
isReactServerLayer?: boolean;
}
const mockCurrentTraceSpan = {
traceChild: (name: string) => mockCurrentTraceSpan,
traceAsyncFn: async (fn: any) => fn(),
};
async function loaderTransform(this: any, parentTrace: any, source?: string, inputSourceMap?: any) {
// Make the loader async
const filename = this.resourcePath;
const loaderOptions: SWCLoaderOptions = this.getOptions() || {};
const {
isServer,
rootDir,
pagesDir,
appDir,
hasReactRefresh,
nextConfig,
jsConfig,
supportedBrowsers,
swcCacheDir,
serverComponents,
isReactServerLayer,
} = loaderOptions;
const isPageFile = filename.startsWith(pagesDir);
const relativeFilePathFromRoot = path.relative(rootDir, filename);
const swcOptions = getLoaderSWCOptions({
pagesDir,
appDir,
filename,
isServer,
isPageFile,
development: this.mode === 'development',
hasReactRefresh,
modularizeImports: nextConfig?.modularizeImports,
optimizePackageImports: nextConfig?.experimental?.optimizePackageImports,
swcPlugins: nextConfig?.experimental?.swcPlugins,
compilerOptions: nextConfig?.compiler,
optimizeServerReact: nextConfig?.experimental?.optimizeServerReact,
jsConfig,
supportedBrowsers,
swcCacheDir,
relativeFilePathFromRoot,
serverComponents,
isReactServerLayer,
});
const programmaticOptions = {
...swcOptions,
filename,
inputSourceMap: inputSourceMap ? JSON.stringify(inputSourceMap) : undefined,
// Set the default sourcemap behavior based on Webpack's mapping flag,
sourceMaps: this.sourceMap,
inlineSourcesContent: this.sourceMap,
// Ensure that Webpack will get a full absolute path in the sourcemap
// so that it can properly map the module back to its internal cached
// modules.
sourceFileName: filename,
};
if (!programmaticOptions.inputSourceMap) {
delete programmaticOptions.inputSourceMap;
}
// auto detect development mode
if (
this.mode &&
programmaticOptions.jsc &&
programmaticOptions.jsc.transform &&
programmaticOptions.jsc.transform.react &&
!Object.prototype.hasOwnProperty.call(programmaticOptions.jsc.transform.react, 'development')
) {
programmaticOptions.jsc.transform.react.development = this.mode === 'development';
}
const swcSpan = parentTrace.traceChild('next-swc-transform');
return swcSpan.traceAsyncFn(() =>
transform(source as any, programmaticOptions).then((output) => {
if (output.eliminatedPackages && this.eliminatedPackages) {
for (const pkg of JSON.parse(output.eliminatedPackages)) {
this.eliminatedPackages.add(pkg);
}
}
return [output.code, output.map ? JSON.parse(output.map) : undefined];
})
);
}
const EXCLUDED_PATHS = /[\\/](cache[\\/][^\\/]+\.zip[\\/]node_modules|__virtual__)[\\/]/g;
export function pitch(this: any) {
const callback = this.async();
(async () => {
if (
// TODO: investigate swc file reading in PnP mode?
!process.versions.pnp &&
!EXCLUDED_PATHS.test(this.resourcePath) &&
this.loaders.length - 1 === this.loaderIndex &&
isAbsolute(this.resourcePath) &&
!(await isWasm())
) {
const loaderSpan = mockCurrentTraceSpan.traceChild('next-swc-loader');
this.addDependency(this.resourcePath);
return loaderSpan.traceAsyncFn(() => loaderTransform.call(this, loaderSpan));
}
return null;
})().then((r) => {
if (r) return callback(null, ...r);
callback();
return null;
}, callback);
}
function sanitizeSourceMap(rawSourceMap: any): any {
const { sourcesContent, ...sourceMap } = rawSourceMap ?? {};
// JSON parse/stringify trick required for swc to accept the SourceMap
return JSON.parse(JSON.stringify(sourceMap));
}
export default function swcLoader(this: any, inputSource: string, inputSourceMap: any) {
const loaderSpan = mockCurrentTraceSpan.traceChild('next-swc-loader');
const callback = this.async();
loaderSpan
.traceAsyncFn(() =>
loaderTransform.call(this, loaderSpan, inputSource, sanitizeSourceMap(inputSourceMap))
)
.then(
([transformedSource, outputSourceMap]: any) => {
callback(null, transformedSource, outputSourceMap || inputSourceMap);
},
(err: Error) => {
callback(err);
}
);
}
// accept Buffers instead of strings
export const raw = true;

View File

@ -1,5 +1,4 @@
import { join } from 'path';
import semver from 'semver';
import { baseGenerator } from '../baseGenerator';
import type { Generator } from '../types';
import { CoreBuilder } from '../../project_types';
@ -13,10 +12,6 @@ const generator: Generator<{ projectName: string }> = async (
options,
commandOptions
) => {
const angularVersion = await packageManager.getPackageVersion('@angular/core');
const isWebpack5 = angularVersion && semver.gte(angularVersion, '12.0.0');
const updatedOptions = isWebpack5 ? { ...options, builder: CoreBuilder.Webpack5 } : options;
const angularJSON = new AngularJSON();
if (
@ -62,7 +57,8 @@ const generator: Generator<{ projectName: string }> = async (
packageManager,
npmOptions,
{
...updatedOptions,
...options,
builder: CoreBuilder.Webpack5,
...(useCompodoc && {
frameworkPreviewParts: {
prefix: compoDocPreviewPrefix,

View File

@ -1,8 +1,11 @@
import { CoreBuilder } from '../../project_types';
import { baseGenerator } from '../baseGenerator';
import type { Generator } from '../types';
const generator: Generator = async (packageManager, npmOptions, options) => {
await baseGenerator(packageManager, npmOptions, options, 'html');
await baseGenerator(packageManager, npmOptions, options, 'html', {
useSWC: ({ builder }) => builder === CoreBuilder.Webpack5,
});
};
export default generator;

View File

@ -1,8 +1,11 @@
import { CoreBuilder } from '../../project_types';
import { baseGenerator } from '../baseGenerator';
import type { Generator } from '../types';
const generator: Generator = async (packageManager, npmOptions, options) => {
await baseGenerator(packageManager, npmOptions, options, 'preact');
await baseGenerator(packageManager, npmOptions, options, 'preact', {
useSWC: ({ builder }) => builder === CoreBuilder.Webpack5,
});
};
export default generator;

View File

@ -1,5 +1,5 @@
import { detectLanguage } from '../../detect';
import { SupportedLanguage } from '../../project_types';
import { CoreBuilder, SupportedLanguage } from '../../project_types';
import { baseGenerator } from '../baseGenerator';
import type { Generator } from '../types';
@ -10,6 +10,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => {
await baseGenerator(packageManager, npmOptions, options, 'react', {
extraPackages,
useSWC: ({ builder }) => builder === CoreBuilder.Webpack5,
extraAddons: ['@storybook/addon-onboarding'],
});
};

View File

@ -59,6 +59,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => {
{ ...options, builder: CoreBuilder.Webpack5 },
'react',
{
useSWC: () => true,
extraAddons,
extraPackages,
staticDir: fs.existsSync(path.resolve('./public')) ? 'public' : undefined,

View File

@ -9,6 +9,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => {
{ ...options, builder: CoreBuilder.Webpack5 },
'server',
{
useSWC: () => true,
extensions: ['json', 'yaml', 'yml'],
}
);

View File

@ -1,8 +1,11 @@
import { CoreBuilder } from '../../project_types';
import { baseGenerator } from '../baseGenerator';
import type { Generator } from '../types';
const generator: Generator = async (packageManager, npmOptions, options) => {
await baseGenerator(packageManager, npmOptions, options, 'vue');
await baseGenerator(packageManager, npmOptions, options, 'vue', {
useSWC: ({ builder }) => builder === CoreBuilder.Webpack5,
});
};
export default generator;

View File

@ -7,6 +7,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => {
extraPackages: async ({ builder }) => {
return builder === CoreBuilder.Webpack5 ? ['vue-loader@^15.7.0'] : [];
},
useSWC: ({ builder }) => builder === CoreBuilder.Webpack5,
});
};

View File

@ -9,6 +9,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => {
? ['vue-loader@^17.0.0', '@vue/compiler-sfc@^3.2.0']
: [];
},
useSWC: ({ builder }) => builder === CoreBuilder.Webpack5,
});
};

View File

@ -1,9 +1,11 @@
import { CoreBuilder } from '../../project_types';
import { baseGenerator } from '../baseGenerator';
import type { Generator } from '../types';
const generator: Generator = async (packageManager, npmOptions, options) => {
return baseGenerator(packageManager, npmOptions, options, 'web-components', {
extraPackages: ['lit'],
useSWC: ({ builder }) => builder === CoreBuilder.Webpack5,
});
};

View File

@ -1,9 +1,11 @@
import { CoreBuilder } from '../../project_types';
import { baseGenerator } from '../baseGenerator';
import type { Generator } from '../types';
const generator: Generator = async (packageManager, npmOptions, options) => {
await baseGenerator(packageManager, npmOptions, options, 'react', {
extraAddons: ['@storybook/addon-onboarding'],
useSWC: ({ builder }) => builder === CoreBuilder.Webpack5,
});
};

View File

@ -29,6 +29,7 @@ const defaultOptions: FrameworkOptions = {
addMainFile: true,
addComponents: true,
skipBabel: false,
useSWC: () => false,
extraMain: undefined,
framework: undefined,
extensions: undefined,
@ -194,23 +195,6 @@ export async function baseGenerator(
builder = await detectBuilder(packageManager, projectType);
}
const {
extraAddons: extraAddonPackages,
extraPackages,
staticDir,
addScripts,
addMainFile,
addComponents,
skipBabel,
extraMain,
extensions,
storybookConfigFolder,
componentsDestinationPath,
} = {
...defaultOptions,
...options,
};
const {
packages: frameworkPackages,
type,
@ -226,6 +210,34 @@ export async function baseGenerator(
shouldApplyRequireWrapperOnPackageNames
);
const {
extraAddons: extraAddonPackages,
extraPackages,
staticDir,
addScripts,
addMainFile,
addComponents,
extraMain,
extensions,
storybookConfigFolder,
componentsDestinationPath,
useSWC,
} = {
...defaultOptions,
...options,
};
let { skipBabel } = {
...defaultOptions,
...options,
};
const swc = useSWC({ builder });
if (swc) {
skipBabel = true;
}
const extraAddonsToInstall =
typeof extraAddonPackages === 'function'
? await extraAddonPackages({
@ -401,7 +413,18 @@ export async function baseGenerator(
: [];
await configureMain({
framework: { name: frameworkInclude, options: options.framework || {} },
framework: {
name: frameworkInclude,
options: swc
? {
...(options.framework ?? {}),
builder: {
...(options.framework?.builder ?? {}),
useSWC: true,
},
}
: options.framework || {},
},
prefixes,
storybookConfigFolder,
docs: { autodocs: 'tag' },

View File

@ -24,6 +24,7 @@ export interface FrameworkOptions {
addMainFile?: boolean;
addComponents?: boolean;
skipBabel?: boolean;
useSWC?: ({ builder }: { builder: Builder }) => boolean;
extraMain?: any;
extensions?: string[];
framework?: Record<string, any>;

View File

@ -57,7 +57,7 @@ const installStorybook = async <Project extends ProjectType>(
linkable: !!options.linkable,
pnp: pnp || options.usePnp,
yes: options.yes,
projectType: options.type,
projectType,
};
const runGenerator: () => Promise<any> = async () => {

View File

@ -68,7 +68,7 @@
"recast": "^0.23.1"
},
"devDependencies": {
"@types/jscodeshift": "^0.11.6",
"@types/jscodeshift": "^0.11.10",
"ansi-regex": "^5.0.1",
"jest": "^29.7.0",
"jest-specific-snapshot": "^8.0.0",

View File

@ -368,3 +368,20 @@ export class GoogleFontsLoadingError extends StorybookError {
`;
}
}
export class NextjsSWCNotSupportedError extends StorybookError {
readonly category = Category.FRAMEWORK_NEXTJS;
readonly code = 3;
public readonly documentation =
'https://github.com/storybookjs/storybook/blob/next/code/frameworks/nextjs/README.md#manual-migration';
template() {
return dedent`
You have activated the SWC mode for Next.js, but you are not using Next.js 14.0.0 or higher.
SWC is only supported in Next.js 14.0.0 and higher. Please go to your .storybook/main.<js|ts> file
and remove the { framework: { options: { builder: { useSWC: true } } } } option or upgrade to Next.js v14 or later.
`;
}
}

View File

@ -51,6 +51,7 @@
"ts-dedent": "^2.0.0"
},
"devDependencies": {
"slash": "^5.1.0",
"typescript": "~4.9.3",
"webpack": "5"
},

View File

@ -4,3 +4,4 @@ export * from './check-webpack-version';
export * from './merge-webpack-config';
export * from './to-importFn';
export * from './to-require-context';
export * from './virtual-module-mapping';

View File

@ -22,6 +22,12 @@ export interface WebpackConfiguration {
devtool?: false | string;
}
export type BuilderOptions = {
fsCache?: boolean;
useSWC?: boolean;
lazyCompilation?: boolean;
};
export type StorybookConfig<TWebpackConfiguration = WebpackConfiguration> = StorybookConfigBase & {
/**
* Modify or return a custom Webpack config after the Storybook's default configuration

View File

@ -0,0 +1,111 @@
import type { Options, PreviewAnnotation } from '@storybook/types';
import { isAbsolute, join, resolve } from 'path';
import {
getBuilderOptions,
getRendererName,
handlebars,
interpolate,
loadPreviewOrConfigFile,
normalizeStories,
readTemplate,
} from '@storybook/core-common';
import slash from 'slash';
import type { BuilderOptions } from './types';
import { toImportFn } from './to-importFn';
import { toRequireContextString } from './to-require-context';
export const getVirtualModuleMapping = async (options: Options) => {
const virtualModuleMapping: Record<string, string> = {};
const builderOptions = await getBuilderOptions<BuilderOptions>(options);
const workingDir = process.cwd();
const isProd = options.configType === 'PRODUCTION';
const nonNormalizedStories = await options.presets.apply('stories', []);
const stories = normalizeStories(nonNormalizedStories, {
configDir: options.configDir,
workingDir,
});
const previewAnnotations = [
...(await options.presets.apply<PreviewAnnotation[]>('previewAnnotations', [], options)).map(
(entry) => {
// If entry is an object, use the absolute import specifier.
// This is to maintain back-compat with community addons that bundle other addons
// and package managers that "hide" sub dependencies (e.g. pnpm / yarn pnp)
// The vite builder uses the bare import specifier.
if (typeof entry === 'object') {
return entry.absolute;
}
// TODO: Remove as soon as we drop support for disabled StoryStoreV7
if (isAbsolute(entry)) {
return entry;
}
return slash(entry);
}
),
loadPreviewOrConfigFile(options),
].filter(Boolean);
if (options.features?.storyStoreV7) {
const storiesFilename = 'storybook-stories.js';
const storiesPath = resolve(join(workingDir, storiesFilename));
const needPipelinedImport = !!builderOptions.lazyCompilation && !isProd;
virtualModuleMapping[storiesPath] = toImportFn(stories, { needPipelinedImport });
const configEntryPath = resolve(join(workingDir, 'storybook-config-entry.js'));
virtualModuleMapping[configEntryPath] = handlebars(
await readTemplate(
require.resolve(
'@storybook/builder-webpack5/templates/virtualModuleModernEntry.js.handlebars'
)
),
{
storiesFilename,
previewAnnotations,
}
// We need to double escape `\` for webpack. We may have some in windows paths
).replace(/\\/g, '\\\\');
} else {
const rendererName = await getRendererName(options);
const rendererInitEntry = resolve(join(workingDir, 'storybook-init-renderer-entry.js'));
virtualModuleMapping[rendererInitEntry] = `import '${slash(rendererName)}';`;
const entryTemplate = await readTemplate(
join(__dirname, '..', 'templates', 'virtualModuleEntry.template.js')
);
previewAnnotations.forEach((previewAnnotationFilename: string | undefined) => {
if (!previewAnnotationFilename) return;
// Ensure that relative paths end up mapped to a filename in the cwd, so a later import
// of the `previewAnnotationFilename` in the template works.
const entryFilename = previewAnnotationFilename.startsWith('.')
? `${previewAnnotationFilename.replace(/(\w)(\/|\\)/g, '$1-')}-generated-config-entry.js`
: `${previewAnnotationFilename}-generated-config-entry.js`;
// NOTE: although this file is also from the `dist/cjs` directory, it is actually a ESM
// file, see https://github.com/storybookjs/storybook/pull/16727#issuecomment-986485173
virtualModuleMapping[entryFilename] = interpolate(entryTemplate, {
previewAnnotationFilename,
});
});
if (stories.length > 0) {
const storyTemplate = await readTemplate(
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 = resolve(join(workingDir, `generated-stories-entry.cjs`));
virtualModuleMapping[storiesFilename] = interpolate(storyTemplate, {
rendererName,
})
// Make sure we also replace quotes for this one
.replace("'{{stories}}'", stories.map(toRequireContextString).join(','));
}
}
return virtualModuleMapping;
};

View File

@ -4031,72 +4031,72 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:14.0.0":
version: 14.0.0
resolution: "@next/env@npm:14.0.0"
checksum: c43e81dbd162a29a4b380342e416209d69d731e8ced7688d09668ec8196f543e358ed65adad81a26e943c63a293d7a018552f8389b6b1ac95cd0f63f4ef257c0
"@next/env@npm:14.0.2":
version: 14.0.2
resolution: "@next/env@npm:14.0.2"
checksum: 9fad703ce13b7b7fecf898d3c239f8976f2ec7f3c7c461c06da70898a0221775c48e1a2e2c76740216c4093c2db9bd7adaacd196586cd4283e09eb89de4c1db6
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:14.0.0":
version: 14.0.0
resolution: "@next/swc-darwin-arm64@npm:14.0.0"
"@next/swc-darwin-arm64@npm:14.0.2":
version: 14.0.2
resolution: "@next/swc-darwin-arm64@npm:14.0.2"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:14.0.0":
version: 14.0.0
resolution: "@next/swc-darwin-x64@npm:14.0.0"
"@next/swc-darwin-x64@npm:14.0.2":
version: 14.0.2
resolution: "@next/swc-darwin-x64@npm:14.0.2"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:14.0.0":
version: 14.0.0
resolution: "@next/swc-linux-arm64-gnu@npm:14.0.0"
"@next/swc-linux-arm64-gnu@npm:14.0.2":
version: 14.0.2
resolution: "@next/swc-linux-arm64-gnu@npm:14.0.2"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:14.0.0":
version: 14.0.0
resolution: "@next/swc-linux-arm64-musl@npm:14.0.0"
"@next/swc-linux-arm64-musl@npm:14.0.2":
version: 14.0.2
resolution: "@next/swc-linux-arm64-musl@npm:14.0.2"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:14.0.0":
version: 14.0.0
resolution: "@next/swc-linux-x64-gnu@npm:14.0.0"
"@next/swc-linux-x64-gnu@npm:14.0.2":
version: 14.0.2
resolution: "@next/swc-linux-x64-gnu@npm:14.0.2"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:14.0.0":
version: 14.0.0
resolution: "@next/swc-linux-x64-musl@npm:14.0.0"
"@next/swc-linux-x64-musl@npm:14.0.2":
version: 14.0.2
resolution: "@next/swc-linux-x64-musl@npm:14.0.2"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:14.0.0":
version: 14.0.0
resolution: "@next/swc-win32-arm64-msvc@npm:14.0.0"
"@next/swc-win32-arm64-msvc@npm:14.0.2":
version: 14.0.2
resolution: "@next/swc-win32-arm64-msvc@npm:14.0.2"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:14.0.0":
version: 14.0.0
resolution: "@next/swc-win32-ia32-msvc@npm:14.0.0"
"@next/swc-win32-ia32-msvc@npm:14.0.2":
version: 14.0.2
resolution: "@next/swc-win32-ia32-msvc@npm:14.0.2"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:14.0.0":
version: 14.0.0
resolution: "@next/swc-win32-x64-msvc@npm:14.0.0"
"@next/swc-win32-x64-msvc@npm:14.0.2":
version: 14.0.2
resolution: "@next/swc-win32-x64-msvc@npm:14.0.2"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@ -6311,7 +6311,7 @@ __metadata:
"@storybook/node-logger": "workspace:*"
"@storybook/types": "workspace:*"
"@types/cross-spawn": "npm:^6.0.2"
"@types/jscodeshift": "npm:^0.11.6"
"@types/jscodeshift": "npm:^0.11.10"
ansi-regex: "npm:^5.0.1"
cross-spawn: "npm:^7.0.3"
globby: "npm:^11.0.2"
@ -6490,6 +6490,7 @@ __metadata:
"@storybook/node-logger": "workspace:*"
"@storybook/types": "workspace:*"
"@types/node": "npm:^18.0.0"
slash: "npm:^5.1.0"
ts-dedent: "npm:^2.0.0"
typescript: "npm:~4.9.3"
webpack: "npm:5"
@ -6853,10 +6854,12 @@ __metadata:
"@babel/preset-typescript": "npm:^7.23.2"
"@babel/runtime": "npm:^7.23.2"
"@babel/types": "npm:^7.23.0"
"@pmmmwh/react-refresh-webpack-plugin": "npm:^0.5.11"
"@storybook/addon-actions": "workspace:*"
"@storybook/builder-webpack5": "workspace:*"
"@storybook/core-common": "workspace:*"
"@storybook/core-events": "workspace:*"
"@storybook/core-webpack": "workspace:*"
"@storybook/node-logger": "workspace:*"
"@storybook/preset-react-webpack": "workspace:*"
"@storybook/preview-api": "workspace:*"
@ -6871,7 +6874,7 @@ __metadata:
fs-extra: "npm:^11.1.0"
image-size: "npm:^1.0.0"
loader-utils: "npm:^3.2.1"
next: "npm:^14.0.0"
next: "npm:^14.0.2"
node-polyfill-webpack-plugin: "npm:^2.0.1"
pnp-webpack-plugin: "npm:^1.7.0"
postcss: "npm:^8.4.21"
@ -8936,13 +8939,13 @@ __metadata:
languageName: node
linkType: hard
"@types/jscodeshift@npm:^0.11.6":
version: 0.11.7
resolution: "@types/jscodeshift@npm:0.11.7"
"@types/jscodeshift@npm:^0.11.10":
version: 0.11.10
resolution: "@types/jscodeshift@npm:0.11.10"
dependencies:
ast-types: "npm:^0.14.1"
recast: "npm:^0.20.3"
checksum: a2c26f8e64950296bae6176c52e832e1f5c5eb3672adad3c1cdc63e23b8bd3de47890ac8eaae7eb0788feea7628ce540513ff5189379f79e882ddcfa1c855cfc
checksum: 1d477ea1addd62a5949f028ef16bac3226341d65052e4f51d61e51789c6c7aa17e953dac34eb6d1e5a2b761fc4c7920df875e20e85cdf4122fc08836e7da547a
languageName: node
linkType: hard
@ -22737,20 +22740,20 @@ __metadata:
languageName: node
linkType: hard
"next@npm:^14.0.0":
version: 14.0.0
resolution: "next@npm:14.0.0"
"next@npm:^14.0.2":
version: 14.0.2
resolution: "next@npm:14.0.2"
dependencies:
"@next/env": "npm:14.0.0"
"@next/swc-darwin-arm64": "npm:14.0.0"
"@next/swc-darwin-x64": "npm:14.0.0"
"@next/swc-linux-arm64-gnu": "npm:14.0.0"
"@next/swc-linux-arm64-musl": "npm:14.0.0"
"@next/swc-linux-x64-gnu": "npm:14.0.0"
"@next/swc-linux-x64-musl": "npm:14.0.0"
"@next/swc-win32-arm64-msvc": "npm:14.0.0"
"@next/swc-win32-ia32-msvc": "npm:14.0.0"
"@next/swc-win32-x64-msvc": "npm:14.0.0"
"@next/env": "npm:14.0.2"
"@next/swc-darwin-arm64": "npm:14.0.2"
"@next/swc-darwin-x64": "npm:14.0.2"
"@next/swc-linux-arm64-gnu": "npm:14.0.2"
"@next/swc-linux-arm64-musl": "npm:14.0.2"
"@next/swc-linux-x64-gnu": "npm:14.0.2"
"@next/swc-linux-x64-musl": "npm:14.0.2"
"@next/swc-win32-arm64-msvc": "npm:14.0.2"
"@next/swc-win32-ia32-msvc": "npm:14.0.2"
"@next/swc-win32-x64-msvc": "npm:14.0.2"
"@swc/helpers": "npm:0.5.2"
busboy: "npm:1.6.0"
caniuse-lite: "npm:^1.0.30001406"
@ -22788,7 +22791,7 @@ __metadata:
optional: true
bin:
next: dist/bin/next
checksum: cfb18a72d6e1d875efb1bb3806f9a06551f482c5cb87231e77e179a71d26f3d43700290988ad27e739302bfa7ff8ac8081aafd5456c39a2819fdd315617e5acf
checksum: 65ae7a09f1643bc3deafdbdae9ce0c02326346c4a60a7c739f8f6b154b2226b8fcc5efb984cdcb4ef100116910d4c1013089135800d30c7a50cf98c9d22e5a26
languageName: node
linkType: hard
@ -27467,7 +27470,7 @@ __metadata:
languageName: node
linkType: hard
"slash@npm:^5.0.0":
"slash@npm:^5.0.0, slash@npm:^5.1.0":
version: 5.1.0
resolution: "slash@npm:5.1.0"
checksum: eb48b815caf0bdc390d0519d41b9e0556a14380f6799c72ba35caf03544d501d18befdeeef074bc9c052acf69654bc9e0d79d7f1de0866284137a40805299eb3