Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next.js: Enable SWC for new Webpack5-based projects #24871

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions code/builders/builder-webpack5/src/preview/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,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],
};
Expand Down
20 changes: 12 additions & 8 deletions code/e2e-tests/addon-controls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 6 additions & 15 deletions code/frameworks/nextjs/src/swc/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import type { NextConfig } from 'next';
import path from 'path';
import type { RuleSetRule } from 'webpack';
import semver from 'semver';
import { dedent } from 'ts-dedent';
import { logger } from '@storybook/node-logger';
import { NextjsSWCNotSupportedError } from 'lib/core-events/src/errors/server-errors';
import { getNextjsVersion } from '../utils';

export const configureSWCLoader = async (
Expand All @@ -19,13 +18,7 @@ export const configureSWCLoader = async (
const version = getNextjsVersion();

if (semver.lt(version, '14.0.0')) {
logger.warn(
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.
Skipping SWC and using Babel instead.
`
);
return;
throw new NextjsSWCNotSupportedError();
}

const dir = getProjectRoot();
Expand All @@ -43,14 +36,14 @@ export const configureSWCLoader = async (

baseConfig.module.rules = [
// TODO: Remove filtering in Storybook 8.0
...baseConfig.module.rules.filter(
(r: RuleSetRule) =>
!(typeof r.use === 'object' && 'loader' in r.use && r.use.loader?.includes('swc-loader'))
),
...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
Expand All @@ -61,14 +54,12 @@ export const configureSWCLoader = async (
pagesDir: `${dir}/pages`,
appDir: `${dir}/apps`,
hasReactRefresh: isDevelopment,
hasServerComponents: true,
nextConfig,
supportedBrowsers: require('next/dist/build/utils').getSupportedBrowsers(
dir,
isDevelopment
),
swcCacheDir: path.join(dir, nextConfig?.distDir ?? '.next', 'cache', 'swc'),
isServerLayer: false,
bundleTarget: 'default',
},
},
Expand Down
11 changes: 10 additions & 1 deletion code/frameworks/nextjs/src/swc/next-swc-loader-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,20 @@ export function pitch(this: any) {
}, 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));
}
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved

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, inputSourceMap))
.traceAsyncFn(() =>
loaderTransform.call(this, loaderSpan, inputSource, sanitizeSourceMap(inputSourceMap))
)
.then(
([transformedSource, outputSourceMap]: any) => {
callback(null, transformedSource, outputSourceMap || inputSourceMap);
Expand Down
8 changes: 2 additions & 6 deletions code/lib/cli/src/generators/ANGULAR/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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');
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
const updatedOptions = isWebpack5 ? { ...options, builder: CoreBuilder.Webpack5 } : options;

const angularJSON = new AngularJSON();

if (
Expand Down Expand Up @@ -62,7 +57,8 @@ const generator: Generator<{ projectName: string }> = async (
packageManager,
npmOptions,
{
...updatedOptions,
...options,
builder: CoreBuilder.Webpack5,
...(useCompodoc && {
frameworkPreviewParts: {
prefix: compoDocPreviewPrefix,
Expand Down
64 changes: 47 additions & 17 deletions code/lib/cli/src/generators/baseGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { dedent } from 'ts-dedent';
import ora from 'ora';
import type { NpmOptions } from '../NpmOptions';
import type { SupportedRenderers, SupportedFrameworks, Builder } from '../project_types';
import { SupportedLanguage, externalFrameworks, CoreBuilder } from '../project_types';
import { SupportedLanguage, externalFrameworks, CoreBuilder, ProjectType } from '../project_types';
import { copyTemplateFiles } from '../helpers';
import { configureMain, configurePreview } from './configure';
import type { JsPackageManager } from '../js-package-manager';
Expand Down Expand Up @@ -171,6 +171,15 @@ const hasInteractiveStories = (rendererId: SupportedRenderers) =>
const hasFrameworkTemplates = (framework?: SupportedFrameworks) =>
['angular', 'nextjs'].includes(framework);

function shouldUseSWCCompiler(builder: Builder, projectType: ProjectType) {
return (
builder === CoreBuilder.Webpack5 &&
projectType !== ProjectType.ANGULAR &&
// TODO: Remove in Storybook 8.0
projectType !== ProjectType.NEXTJS
);
}
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved

export async function baseGenerator(
packageManager: JsPackageManager,
npmOptions: NpmOptions,
Expand All @@ -194,14 +203,30 @@ export async function baseGenerator(
builder = await detectBuilder(packageManager, projectType);
}

const useSWC = shouldUseSWCCompiler(builder, projectType);

const {
packages: frameworkPackages,
type,
rendererId,
framework: frameworkInclude,
builder: builderInclude,
} = getFrameworkDetails(
renderer,
builder,
pnp,
language,
framework,
shouldApplyRequireWrapperOnPackageNames
);

const {
extraAddons: extraAddonPackages,
extraPackages,
staticDir,
addScripts,
addMainFile,
addComponents,
skipBabel,
extraMain,
extensions,
storybookConfigFolder,
Expand All @@ -211,20 +236,14 @@ export async function baseGenerator(
...options,
};

const {
packages: frameworkPackages,
type,
rendererId,
framework: frameworkInclude,
builder: builderInclude,
} = getFrameworkDetails(
renderer,
builder,
pnp,
language,
framework,
shouldApplyRequireWrapperOnPackageNames
);
let { skipBabel } = {
...defaultOptions,
...options,
};

if (useSWC) {
skipBabel = true;
}

const extraAddonsToInstall =
typeof extraAddonPackages === 'function'
Expand Down Expand Up @@ -401,7 +420,18 @@ export async function baseGenerator(
: [];

await configureMain({
framework: { name: frameworkInclude, options: options.framework || {} },
framework: {
name: frameworkInclude,
options: useSWC
? {
...(options.framework ?? {}),
builder: {
...(options.framework?.builder ?? {}),
useSWC: true,
},
}
: options.framework || {},
},
prefixes,
storybookConfigFolder,
docs: { autodocs: 'tag' },
Expand Down
2 changes: 1 addition & 1 deletion code/lib/cli/src/initiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
17 changes: 17 additions & 0 deletions code/lib/core-events/src/errors/server-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
`;
}
}