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

[heft-storybook] Support storybook static build #3843

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"plugin": "@rushstack/heft-storybook-plugin",
"options": {
"storykitPackageName": "heft-storybook-react-tutorial-storykit",
"startupModulePath": "@storybook/react/bin/index.js"
"startupModulePath": "@storybook/react/bin/index.js",
"staticBuildModulePath": "@storybook/react/bin/build.js"
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"build": "heft build --clean",
"start": "heft start",
"storybook": "heft start --storybook",
"build-storybook": "heft build --storybook",
"_phase:build": "heft build --clean",
"_phase:test": "heft test --no-build"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft-storybook-plugin",
"comment": "Add support for storybook static build",
"type": "minor"
}
],
"packageName": "@rushstack/heft-storybook-plugin"
}
3 changes: 2 additions & 1 deletion common/reviews/api/heft-storybook-plugin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export default _default;

// @public
export interface IStorybookPluginOptions {
startupModulePath: string;
startupModulePath?: string;
staticBuildModulePath?: string;
storykitPackageName: string;
}

Expand Down
72 changes: 53 additions & 19 deletions heft-plugins/heft-storybook-plugin/src/StorybookPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FileSystem,
Import,
IParsedPackageNameOrError,
JsonSchema,
PackageName
} from '@rushstack/node-core-library';
import type {
Expand All @@ -23,6 +24,7 @@ import { StorybookRunner } from './StorybookRunner';

const PLUGIN_NAME: string = 'StorybookPlugin';
const TASK_NAME: string = 'heft-storybook';
const PLUGIN_SCHEMA_PATH: string = path.resolve(__dirname, 'schemas', 'heft-storybook-plugin.schema.json');
RongqiZ marked this conversation as resolved.
Show resolved Hide resolved

/**
* Options for `StorybookPlugin`.
Expand Down Expand Up @@ -55,25 +57,37 @@ export interface IStorybookPluginOptions {
storykitPackageName: string;

/**
* The module entry point that Heft should use to launch the Storybook toolchain. Typically it
* is the path loaded the `start-storybook` shell script.
* The module entry point that Heft `start` command should use to launch the Storybook toolchain.
* Typically it is the path loaded the `start-storybook` shell script.
*
* @example
* If you are using `@storybook/react`, then the startup path would be:
*
* `"startupModulePath": "@storybook/react/bin/index.js"`
*/
startupModulePath: string;
startupModulePath?: string;

/**
* The module entry point that Heft `build` command should use to launch the Storybook toolchain.
* Typically it is the path loaded the `build-storybook` shell script.
*
* @example
* If you are using `@storybook/react`, then the static build path would be:
*
* `"staticBuildModulePath": "@storybook/react/bin/build.js"`
*/
staticBuildModulePath?: string;
}

/** @public */
export class StorybookPlugin implements IHeftPlugin<IStorybookPluginOptions> {
public readonly pluginName: string = PLUGIN_NAME;
public readonly optionsSchema: JsonSchema = JsonSchema.fromFile(PLUGIN_SCHEMA_PATH);

private _logger!: ScopedLogger;
private _storykitPackageName!: string;
private _startupModulePath!: string;
private _resolvedStartupModulePath!: string;
private _modulePath!: string;
private _resolvedModulePath!: string;

/**
* Generate typings for Sass files before TypeScript compilation.
Expand Down Expand Up @@ -102,13 +116,12 @@ export class StorybookPlugin implements IHeftPlugin<IStorybookPluginOptions> {
}
this._storykitPackageName = options.storykitPackageName;

if (!options.startupModulePath) {
if (!options.startupModulePath && !options.staticBuildModulePath) {
throw new Error(
`The ${TASK_NAME} task cannot start because the "startupModulePath"` +
` plugin option was not specified`
`The ${TASK_NAME} task cannot start because the "startupModulePath" and the "staticBuildModulePath"` +
` plugin options were not specified`
);
}
this._startupModulePath = options.startupModulePath;

const storybookParameters: IHeftFlagParameter = heftSession.commandLine.registerFlagParameter({
associatedActionNames: ['start'],
Expand All @@ -117,14 +130,35 @@ export class StorybookPlugin implements IHeftPlugin<IStorybookPluginOptions> {
'(EXPERIMENTAL) Used by the "@rushstack/heft-storybook-plugin" package to launch Storybook.'
});

const storybookStaticBuildParameters: IHeftFlagParameter = heftSession.commandLine.registerFlagParameter({
iclanton marked this conversation as resolved.
Show resolved Hide resolved
iclanton marked this conversation as resolved.
Show resolved Hide resolved
associatedActionNames: ['build'],
parameterLongName: '--storybook',
description:
'(EXPERIMENTAL) Used by the "@rushstack/heft-storybook-plugin" package to start static build Storybook.'
});

heftSession.hooks.build.tap(PLUGIN_NAME, (build: IBuildStageContext) => {
if (!storybookParameters.actionAssociated || !storybookParameters.value) {
if (
(!storybookParameters.actionAssociated || !storybookParameters.value) &&
(!storybookStaticBuildParameters.actionAssociated || !storybookStaticBuildParameters.value)
) {
this._logger.terminal.writeVerboseLine(
'The command line does not include "--storybook", so bundling will proceed without Storybook'
);
return;
}

const modulePath: string | undefined = storybookStaticBuildParameters.actionAssociated
? options.staticBuildModulePath
: options.startupModulePath;
if (!modulePath) {
D4N14L marked this conversation as resolved.
Show resolved Hide resolved
this._logger.terminal.writeVerboseLine(
'No matching module path option specified in heft.json, so bundling will proceed without Storybook'
);
return;
}
this._modulePath = modulePath;

this._logger.terminal.writeVerboseLine(
'The command line includes "--storybook", redirecting Webpack to Storybook'
);
Expand All @@ -139,8 +173,10 @@ export class StorybookPlugin implements IHeftPlugin<IStorybookPluginOptions> {
bundle.hooks.configureWebpack.tap(
{ name: PLUGIN_NAME, stage: Number.MAX_SAFE_INTEGER },
(webpackConfiguration: unknown) => {
// Discard Webpack's configuration to prevent Webpack from running
return null;
// Discard Webpack's configuration to prevent Webpack from running only when starting a storybook server
return storybookParameters.actionAssociated && storybookParameters.value
? null
: webpackConfiguration;
}
);

Expand Down Expand Up @@ -177,18 +213,16 @@ export class StorybookPlugin implements IHeftPlugin<IStorybookPluginOptions> {
);
}

this._logger.terminal.writeVerboseLine(`Resolving startupModulePath "${this._startupModulePath}"`);
this._logger.terminal.writeVerboseLine(`Resolving modulePath "${this._modulePath}"`);
try {
this._resolvedStartupModulePath = Import.resolveModule({
modulePath: this._startupModulePath,
this._resolvedModulePath = Import.resolveModule({
modulePath: this._modulePath,
baseFolderPath: storykitModuleFolder
});
} catch (ex) {
throw new Error(`The ${TASK_NAME} task cannot start: ` + (ex as Error).message);
}
this._logger.terminal.writeVerboseLine(
`Resolved startupModulePath is "${this._resolvedStartupModulePath}"`
);
this._logger.terminal.writeVerboseLine(`Resolved modulePath is "${this._resolvedModulePath}"`);

// Example: "/path/to/my-project/.storybook"
const dotStorybookFolder: string = path.join(heftConfiguration.buildFolder, '.storybook');
Expand Down Expand Up @@ -220,7 +254,7 @@ export class StorybookPlugin implements IHeftPlugin<IStorybookPluginOptions> {
heftConfiguration.terminalProvider,
{
buildFolder: heftConfiguration.buildFolder,
resolvedStartupModulePath: this._resolvedStartupModulePath
resolvedStartupModulePath: this._resolvedModulePath
},
// TODO: Extract SubprocessRunnerBase into a public API
// eslint-disable-next-line
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Heft Storybook Plugin Options Configuration",
"description": "This schema describes the \"options\" field that can be specified in heft.json when loading \"@rushstack/heft-storybook-plugin\".",
"type": "object",
"additionalProperties": false,

"properties": {
"storykitPackageName": {
"title": "Specifies an NPM package that will provide the Storybook dependencies for the project.",
"description": "Storybook's conventional approach is for your app project to have direct dependencies\non NPM packages such as `@storybook/react` and `@storybook/addon-essentials`. These packages have\nheavyweight dependencies such as Babel, Webpack, and the associated loaders and plugins needed to\nbuild the Storybook app (which is bundled completely independently from Heft). Naively adding these\ndependencies to your app's package.json muddies the waters of two radically different toolchains,\nand is likely to lead to dependency conflicts, for example if Heft installs Webpack 5 but\n`@storybook/react` installs Webpack 4. To solve this problem, `heft-storybook-plugin` introduces the concept of a separate\n\"storykit package\". All of your Storybook NPM packages are moved to be dependencies of the\nstorykit. Storybook's browser API unfortunately isn't separated into dedicated NPM packages,\nbut instead is exported by the Node.js toolchain packages such as `@storybook/react`. For\nan even cleaner separation the storykit package can simply reexport such APIs.",
D4N14L marked this conversation as resolved.
Show resolved Hide resolved
"type": "string"
},
"startupModulePath": {
"title": "The module entry point that Heft `start` command should use to launch the Storybook toolchain.",
"description": "Typically it is the path loaded the `start-storybook` shell script. For example, If you are using `@storybook/react`, then the startup path would be: `\"startupModulePath\": \"@storybook/react/bin/index.js\"`",
"type": "string"
},
"staticBuildModulePath": {
"title": "The module entry point that Heft `build` command should use to launch the Storybook toolchain.",
"description": "Typically it is the path loaded the `build-storybook` shell script. For example, If you are using `@storybook/react`, then the static build path would be: `\"staticBuildModulePath\": \"@storybook/react/bin/build.js\"`",
"type": "string"
}
}
}