Skip to content

Commit

Permalink
Merge pull request #10 from D4N14L/user/danade/MultiPhaseHeftStorybook
Browse files Browse the repository at this point in the history
Update @rushstack/heft-storybook-plugin to be compatible with multi-phase Heft
  • Loading branch information
D4N14L authored Jul 20, 2022
2 parents 859fe75 + ff22ab7 commit a29d0e7
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 181 deletions.
1 change: 1 addition & 0 deletions heft-plugins/heft-storybook-plugin/.npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
!/lib-*/**
!/dist/**
!ThirdPartyNotice.txt
!heft-plugin.json

# Ignore certain patterns that should not get published.
/dist/*.stats.*
Expand Down
1 change: 1 addition & 0 deletions heft-plugins/heft-storybook-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ UI components.
- [CHANGELOG.md](
https://github.com/microsoft/rushstack/blob/main/heft-plugins/heft-storybook-plugin/CHANGELOG.md) - Find
out what's new in the latest version
- [@rushstack/heft](https://www.npmjs.com/package/@rushstack/heft) - Heft is a config-driven toolchain that invokes popular tools such as TypeScript, ESLint, Jest, Webpack, and API Extractor.

Heft is part of the [Rush Stack](https://rushstack.io/) family of projects.
16 changes: 0 additions & 16 deletions heft-plugins/heft-storybook-plugin/config/api-extractor.json

This file was deleted.

20 changes: 20 additions & 0 deletions heft-plugins/heft-storybook-plugin/heft-plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/heft-plugin.schema.json",

"taskPlugins": [
{
"pluginName": "StorybookPlugin",
"entryPoint": "./lib/StorybookPlugin",
"optionsSchema": "./lib/schemas/storybook.schema.json",

"parameterScope": "storybook",
"parameters": [
{
"longName": "--storybook",
"description": "(EXPERIMENTAL) Used by the \"@rushstack/heft-storybook-plugin\" package to launch Storybook.",
"parameterKind": "flag"
}
]
}
]
}
10 changes: 5 additions & 5 deletions heft-plugins/heft-storybook-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@
"directory": "heft-plugins/heft-storybook-plugin"
},
"homepage": "https://rushstack.io/pages/heft/overview/",
"main": "lib/index.js",
"types": "dist/heft-storybook-plugin.d.ts",
"license": "MIT",
"scripts": {
"build": "heft build --clean",
"start": "heft test --clean --watch",
"_phase:build": "heft build --clean",
"_phase:test": "heft test --no-build"
"_phase:build": "heft run --only build -- --clean",
"_phase:test": "heft run --only test -- --clean"
},
"peerDependencies": {
"@rushstack/heft": "^0.45.14"
Expand All @@ -27,6 +25,8 @@
"@rushstack/eslint-config": "workspace:*",
"@rushstack/heft": "workspace:*",
"@rushstack/heft-node-rig": "workspace:*",
"@rushstack/heft-webpack4-plugin": "workspace:*",
"@rushstack/heft-webpack5-plugin": "workspace:*",
"@types/node": "12.20.24"
}
}
}
187 changes: 81 additions & 106 deletions heft-plugins/heft-storybook-plugin/src/StorybookPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as path from 'path';
import {
AlreadyExistsBehavior,
FileSystem,
Expand All @@ -11,18 +10,23 @@ import {
} from '@rushstack/node-core-library';
import type {
HeftConfiguration,
HeftSession,
IBuildStageContext,
IBundleSubstage,
IHeftPlugin,
IHeftFlagParameter,
IPreCompileSubstage,
ScopedLogger
IHeftTaskSession,
IScopedLogger,
IHeftTaskPlugin,
CommandLineFlagParameter,
IHeftTaskRunHookOptions
} from '@rushstack/heft';
import { StorybookRunner } from './StorybookRunner';
import type {
PluginName as Webpack4PluginName,
IWebpackPluginAccessor as IWebpack4PluginAccessor
} from '@rushstack/heft-webpack4-plugin';
import type {
PluginName as Webpack5PluginName,
IWebpackPluginAccessor as IWebpack5PluginAccessor
} from '@rushstack/heft-webpack5-plugin';

const PLUGIN_NAME: string = 'StorybookPlugin';
const TASK_NAME: string = 'heft-storybook';
const WEBPACK_PLUGIN_NAME: typeof Webpack4PluginName & typeof Webpack5PluginName = 'WebpackPlugin';

/**
* Options for `StorybookPlugin`.
Expand Down Expand Up @@ -67,135 +71,118 @@ export interface IStorybookPluginOptions {
}

/** @public */
export class StorybookPlugin implements IHeftPlugin<IStorybookPluginOptions> {
public readonly pluginName: string = PLUGIN_NAME;

private _logger!: ScopedLogger;
private _storykitPackageName!: string;
private _startupModulePath!: string;
private _resolvedStartupModulePath!: string;
export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPluginOptions> {
private _logger!: IScopedLogger;

/**
* Generate typings for Sass files before TypeScript compilation.
*/
public apply(
heftSession: HeftSession,
taskSession: IHeftTaskSession,
heftConfiguration: HeftConfiguration,
options: IStorybookPluginOptions
): void {
this._logger = heftSession.requestScopedLogger(TASK_NAME);

if (!options.storykitPackageName) {
throw new Error(
`The ${TASK_NAME} task cannot start because the "storykitPackageName"` +
` plugin option was not specified`
);
}
this._logger = taskSession.logger;
const storybookParameter: CommandLineFlagParameter = taskSession.getFlagParameter('--storybook');

const parseResult: IParsedPackageNameOrError = PackageName.tryParse(options.storykitPackageName);
if (parseResult.error) {
throw new Error(
`The ${TASK_NAME} task cannot start because the "storykitPackageName"` +
`The ${taskSession.taskName} task cannot start because the "storykitPackageName"` +
` plugin option is not a valid package name: ` +
parseResult.error
);
}
this._storykitPackageName = options.storykitPackageName;

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

const storybookParameters: IHeftFlagParameter = heftSession.commandLine.registerFlagParameter({
associatedActionNames: ['start'],
parameterLongName: '--storybook',
description:
'(EXPERIMENTAL) Used by the "@rushstack/heft-storybook-plugin" package to launch Storybook.'
});

heftSession.hooks.build.tap(PLUGIN_NAME, (build: IBuildStageContext) => {
if (!storybookParameters.actionAssociated || !storybookParameters.value) {
this._logger.terminal.writeVerboseLine(
'The command line does not include "--storybook", so bundling will proceed without Storybook'
// Only tap if the --storybook flag is present.
if (storybookParameter.value) {
const configureWebpackTapOptions: { name: string, stage: number } = {
name: PLUGIN_NAME,
stage: Number.MAX_SAFE_INTEGER
};
const configureWebpackTap: () => Promise<null> = async () => {
// Discard Webpack's configuration to prevent Webpack from running
this._logger.terminal.writeLine(
'The command line includes "--storybook", redirecting Webpack to Storybook'
);
return;
}

this._logger.terminal.writeVerboseLine(
'The command line includes "--storybook", redirecting Webpack to Storybook'
return null;
};

taskSession.requestAccessToPluginByName(
'@rushstack/heft-webpack4-plugin',
WEBPACK_PLUGIN_NAME,
(accessor: IWebpack4PluginAccessor) =>
accessor.onConfigureWebpackHook.tapPromise(configureWebpackTapOptions, configureWebpackTap)
);

build.hooks.preCompile.tap(PLUGIN_NAME, (preCompile: IPreCompileSubstage) => {
preCompile.hooks.run.tapPromise(PLUGIN_NAME, () => {
return this._onPreCompileAsync(heftConfiguration);
});
});

build.hooks.bundle.tap(PLUGIN_NAME, (bundle: IBundleSubstage) => {
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;
}
);
taskSession.requestAccessToPluginByName(
'@rushstack/heft-webpack5-plugin',
WEBPACK_PLUGIN_NAME,
(accessor: IWebpack5PluginAccessor) =>
accessor.onConfigureWebpackHook.tapPromise(configureWebpackTapOptions, configureWebpackTap)
);

bundle.hooks.run.tapPromise(PLUGIN_NAME, async () => {
await this._onBundleRunAsync(heftSession, heftConfiguration);
});
taskSession.hooks.run.tapPromise(PLUGIN_NAME, async (runOptions: IHeftTaskRunHookOptions) => {
const resolvedStartupModulePath: string =
await this._prepareStorybookAsync(taskSession, heftConfiguration, options);
await this._runStorybookAsync(resolvedStartupModulePath);
});
});
}
}

private async _onPreCompileAsync(heftConfiguration: HeftConfiguration): Promise<void> {
this._logger.terminal.writeVerboseLine(`Probing for "${this._storykitPackageName}"`);
private async _prepareStorybookAsync(
taskSession: IHeftTaskSession,
heftConfiguration: HeftConfiguration,
options: IStorybookPluginOptions
): Promise<string> {
const { storykitPackageName, startupModulePath } = options;
this._logger.terminal.writeVerboseLine(`Probing for "${storykitPackageName}"`);

// Example: "/path/to/my-project/node_modules/my-storykit"
let storykitFolder: string;
try {
storykitFolder = Import.resolvePackage({
packageName: this._storykitPackageName,
packageName: storykitPackageName,
baseFolderPath: heftConfiguration.buildFolder
});
} catch (ex) {
throw new Error(`The ${TASK_NAME} task cannot start: ` + (ex as Error).message);
throw new Error(`The ${taskSession.taskName} task cannot start: ` + (ex as Error).message);
}

this._logger.terminal.writeVerboseLine(`Found "${this._storykitPackageName}" in ` + storykitFolder);
this._logger.terminal.writeVerboseLine(`Found "${storykitPackageName}" in ` + storykitFolder);

// Example: "/path/to/my-project/node_modules/my-storykit/node_modules"
const storykitModuleFolder: string = path.join(storykitFolder, 'node_modules');
if (!(await FileSystem.existsAsync(storykitModuleFolder))) {
const storykitModuleFolder: string = `${storykitFolder}/node_modules`;
const storykitModuleFolderExists: boolean = await FileSystem.existsAsync(storykitModuleFolder);
if (!storykitModuleFolderExists) {
throw new Error(
`The ${TASK_NAME} task cannot start because the storykit module folder does not exist:\n` +
`The ${taskSession.taskName} task cannot start because the storykit module folder does not exist:\n` +
storykitModuleFolder +
'\nDid you forget to install it?'
);
}

this._logger.terminal.writeVerboseLine(`Resolving startupModulePath "${this._startupModulePath}"`);
this._logger.terminal.writeVerboseLine(`Resolving startupModulePath "${startupModulePath}"`);
let resolvedStartupModulePath: string | undefined;
try {
this._resolvedStartupModulePath = Import.resolveModule({
modulePath: this._startupModulePath,
resolvedStartupModulePath = Import.resolveModule({
modulePath: startupModulePath,
baseFolderPath: storykitModuleFolder
});
} catch (ex) {
throw new Error(`The ${TASK_NAME} task cannot start: ` + (ex as Error).message);
throw new Error(`The ${taskSession.taskName} task cannot start: ` + (ex as Error).message);
}

this._logger.terminal.writeVerboseLine(
`Resolved startupModulePath is "${this._resolvedStartupModulePath}"`
`Resolved startupModulePath is "${resolvedStartupModulePath}"`
);

// Example: "/path/to/my-project/.storybook"
const dotStorybookFolder: string = path.join(heftConfiguration.buildFolder, '.storybook');
const dotStorybookFolder: string = `${heftConfiguration.buildFolder}/.storybook`;
await FileSystem.ensureFolderAsync(dotStorybookFolder);

// Example: "/path/to/my-project/.storybook/node_modules"
const dotStorybookModuleFolder: string = path.join(dotStorybookFolder, 'node_modules');
const dotStorybookModuleFolder: string = `${dotStorybookFolder}/node_modules`;

// Example:
// LINK FROM: "/path/to/my-project/.storybook/node_modules"
Expand All @@ -208,28 +195,16 @@ export class StorybookPlugin implements IHeftPlugin<IStorybookPluginOptions> {
linkTargetPath: storykitModuleFolder,
alreadyExistsBehavior: AlreadyExistsBehavior.Overwrite
});

return resolvedStartupModulePath;
}

private async _onBundleRunAsync(
heftSession: HeftSession,
heftConfiguration: HeftConfiguration
): Promise<void> {
this._logger.terminal.writeLine('Starting Storybook runner...');

const storybookRunner: StorybookRunner = new StorybookRunner(
heftConfiguration.terminalProvider,
{
buildFolder: heftConfiguration.buildFolder,
resolvedStartupModulePath: this._resolvedStartupModulePath
},
// TODO: Extract SubprocessRunnerBase into a public API
// eslint-disable-next-line
heftSession as any
);
if (heftSession.debugMode) {
await storybookRunner.invokeAsync();
} else {
await storybookRunner.invokeAsSubprocessAsync();
}
private async _runStorybookAsync(resolvedStartupModulePath: string): Promise<void> {
this._logger.terminal.writeLine('Starting Storybook...');
this._logger.terminal.writeLine(`Launching "${resolvedStartupModulePath}"`);

require(resolvedStartupModulePath);

this._logger.terminal.writeVerboseLine('Completed synchronous portion of launching startupModulePath');
}
}
36 changes: 0 additions & 36 deletions heft-plugins/heft-storybook-plugin/src/StorybookRunner.ts

This file was deleted.

Loading

0 comments on commit a29d0e7

Please sign in to comment.