Skip to content

Commit 89bf70d

Browse files
committed
Add support for the --docs parameter to the Heft storybook plugin.
1 parent b6522b8 commit 89bf70d

File tree

3 files changed

+96
-50
lines changed

3 files changed

+96
-50
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/heft-storybook-plugin",
5+
"comment": "Add support for the `--docs` parameter.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/heft-storybook-plugin"
10+
}

heft-plugins/heft-storybook-plugin/heft-plugin.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
"longName": "--storybook-test",
1919
"description": "Executes a stripped down build-storybook for testing purposes.",
2020
"parameterKind": "flag"
21+
},
22+
{
23+
"longName": "--docs",
24+
"description": "Execute storybook in docs mode.",
25+
"parameterKind": "flag"
2126
}
2227
]
2328
}

heft-plugins/heft-storybook-plugin/src/StorybookPlugin.ts

Lines changed: 81 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,25 @@ export interface IStorybookPluginOptions {
157157
captureWebpackStats?: boolean;
158158
}
159159

160-
interface IRunStorybookOptions {
160+
interface IRunStorybookOptions extends IPrepareStorybookOptions {
161+
logger: IScopedLogger;
162+
isServeMode: boolean;
161163
workingDirectory: string;
162164
resolvedModulePath: string;
163165
outputFolder: string | undefined;
164166
moduleDefaultArgs: string[];
165167
verbose: boolean;
166168
}
167169

170+
interface IPrepareStorybookOptions extends IStorybookPluginOptions {
171+
logger: IScopedLogger;
172+
taskSession: IHeftTaskSession;
173+
heftConfiguration: HeftConfiguration;
174+
isServeMode: boolean;
175+
isTestMode: boolean;
176+
isDocsMode: boolean;
177+
}
178+
168179
const DEFAULT_STORYBOOK_VERSION: StorybookCliVersion = StorybookCliVersion.STORYBOOK7;
169180
const DEFAULT_STORYBOOK_CLI_CONFIG: Record<StorybookCliVersion, IStorybookCliCallingConfig> = {
170181
[StorybookCliVersion.STORYBOOK6]: {
@@ -190,14 +201,12 @@ const DEFAULT_STORYBOOK_CLI_CONFIG: Record<StorybookCliVersion, IStorybookCliCal
190201
}
191202
};
192203

204+
const STORYBOOK_FLAG_NAME: '--storybook' = '--storybook';
193205
const STORYBOOK_TEST_FLAG_NAME: '--storybook-test' = '--storybook-test';
206+
const DOCS_FLAG_NAME: '--docs' = '--docs';
194207

195208
/** @public */
196209
export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPluginOptions> {
197-
private _logger!: IScopedLogger;
198-
private _isServeMode: boolean = false;
199-
private _isTestMode: boolean = false;
200-
201210
/**
202211
* Generate typings for Sass files before TypeScript compilation.
203212
*/
@@ -206,11 +215,12 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
206215
heftConfiguration: HeftConfiguration,
207216
options: IStorybookPluginOptions
208217
): void {
209-
this._logger = taskSession.logger;
218+
const logger: IScopedLogger = taskSession.logger;
210219
const storybookParameter: CommandLineFlagParameter =
211-
taskSession.parameters.getFlagParameter('--storybook');
220+
taskSession.parameters.getFlagParameter(STORYBOOK_FLAG_NAME);
212221
const storybookTestParameter: CommandLineFlagParameter =
213222
taskSession.parameters.getFlagParameter(STORYBOOK_TEST_FLAG_NAME);
223+
const docsParameter: CommandLineFlagParameter = taskSession.parameters.getFlagParameter(DOCS_FLAG_NAME);
214224

215225
const parseResult: IParsedPackageNameOrError = PackageName.tryParse(options.storykitPackageName);
216226
if (parseResult.error) {
@@ -221,27 +231,23 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
221231
);
222232
}
223233

224-
if (storybookTestParameter.value) {
225-
this._isTestMode = true;
226-
}
227-
228234
// Only tap if the --storybook flag is present.
229235
if (storybookParameter.value) {
230236
const configureWebpackTap: () => Promise<false> = async () => {
231237
// Discard Webpack's configuration to prevent Webpack from running
232-
this._logger.terminal.writeLine(
238+
logger.terminal.writeLine(
233239
'The command line includes "--storybook", redirecting Webpack to Storybook'
234240
);
235241
return false;
236242
};
237243

244+
let isServeMode: boolean = false;
238245
taskSession.requestAccessToPluginByName(
239246
'@rushstack/heft-webpack4-plugin',
240247
WEBPACK4_PLUGIN_NAME,
241248
(accessor: IWebpack4PluginAccessor) => {
242-
if (accessor.parameters.isServeMode) {
243-
this._isServeMode = true;
244-
}
249+
isServeMode = accessor.parameters.isServeMode;
250+
245251
// Discard Webpack's configuration to prevent Webpack from running only when performing Storybook build
246252
accessor.hooks.onLoadConfiguration.tapPromise(PLUGIN_NAME, configureWebpackTap);
247253
}
@@ -251,43 +257,49 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
251257
'@rushstack/heft-webpack5-plugin',
252258
WEBPACK5_PLUGIN_NAME,
253259
(accessor: IWebpack5PluginAccessor) => {
254-
if (accessor.parameters.isServeMode) {
255-
this._isServeMode = true;
256-
}
260+
isServeMode = accessor.parameters.isServeMode;
261+
257262
// Discard Webpack's configuration to prevent Webpack from running only when performing Storybook build
258263
accessor.hooks.onLoadConfiguration.tapPromise(PLUGIN_NAME, configureWebpackTap);
259264
}
260265
);
261266

262267
taskSession.hooks.run.tapPromise(PLUGIN_NAME, async (runOptions: IHeftTaskRunHookOptions) => {
263-
const runStorybookOptions: IRunStorybookOptions = await this._prepareStorybookAsync(
268+
const runStorybookOptions: IRunStorybookOptions = await this._prepareStorybookAsync({
269+
logger,
264270
taskSession,
265271
heftConfiguration,
266-
options
267-
);
272+
isServeMode,
273+
isTestMode: storybookTestParameter.value,
274+
isDocsMode: docsParameter.value,
275+
...options
276+
});
268277
await this._runStorybookAsync(runStorybookOptions, options);
269278
});
270279
}
271280
}
272281

273-
private async _prepareStorybookAsync(
274-
taskSession: IHeftTaskSession,
275-
heftConfiguration: HeftConfiguration,
276-
options: IStorybookPluginOptions
277-
): Promise<IRunStorybookOptions> {
278-
const { storykitPackageName, staticBuildOutputFolder } = options;
282+
private async _prepareStorybookAsync(options: IPrepareStorybookOptions): Promise<IRunStorybookOptions> {
283+
const {
284+
logger,
285+
taskSession,
286+
heftConfiguration,
287+
storykitPackageName,
288+
staticBuildOutputFolder,
289+
isTestMode
290+
} = options;
279291
const storybookCliVersion: `${StorybookCliVersion}` = this._getStorybookVersion(options);
280292
const storyBookCliConfig: IStorybookCliCallingConfig = DEFAULT_STORYBOOK_CLI_CONFIG[storybookCliVersion];
281293
const cliPackageName: string = options.cliPackageName ?? storyBookCliConfig.packageName;
282294
const buildMode: StorybookBuildMode = taskSession.parameters.watch
283295
? StorybookBuildMode.WATCH
284296
: StorybookBuildMode.BUILD;
285297

286-
if (buildMode === StorybookBuildMode.WATCH && this._isTestMode) {
298+
if (buildMode === StorybookBuildMode.WATCH && isTestMode) {
287299
throw new Error(`The ${STORYBOOK_TEST_FLAG_NAME} flag is not supported in watch mode`);
288300
}
289301
if (
290-
this._isTestMode &&
302+
isTestMode &&
291303
(storybookCliVersion === StorybookCliVersion.STORYBOOK6 ||
292304
storybookCliVersion === StorybookCliVersion.STORYBOOK7)
293305
) {
@@ -296,7 +308,7 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
296308
);
297309
}
298310

299-
this._logger.terminal.writeVerboseLine(`Probing for "${storykitPackageName}"`);
311+
logger.terminal.writeVerboseLine(`Probing for "${storykitPackageName}"`);
300312
// Example: "/path/to/my-project/node_modules/my-storykit"
301313
let storykitFolderPath: string;
302314
try {
@@ -308,9 +320,9 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
308320
throw new Error(`The ${taskSession.taskName} task cannot start: ` + (ex as Error).message);
309321
}
310322

311-
this._logger.terminal.writeVerboseLine(`Found "${storykitPackageName}" in ` + storykitFolderPath);
323+
logger.terminal.writeVerboseLine(`Found "${storykitPackageName}" in ` + storykitFolderPath);
312324

313-
this._logger.terminal.writeVerboseLine(`Probing for "${cliPackageName}" in "${storykitPackageName}"`);
325+
logger.terminal.writeVerboseLine(`Probing for "${cliPackageName}" in "${storykitPackageName}"`);
314326
// Example: "/path/to/my-project/node_modules/my-storykit/node_modules/@storybook/cli"
315327
let storyBookCliPackage: string;
316328
try {
@@ -322,7 +334,7 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
322334
throw new Error(`The ${taskSession.taskName} task cannot start: ` + (ex as Error).message);
323335
}
324336

325-
this._logger.terminal.writeVerboseLine(`Found "${cliPackageName}" in ` + storyBookCliPackage);
337+
logger.terminal.writeVerboseLine(`Found "${cliPackageName}" in ` + storyBookCliPackage);
326338

327339
const storyBookPackagePackageJsonFile: string = path.join(storyBookCliPackage, FileConstants.PackageJson);
328340
const packageJson: IPackageJson = await JsonFile.loadAsync(storyBookPackagePackageJsonFile);
@@ -333,7 +345,7 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
333345
}
334346
const [moduleExecutableName, ...moduleDefaultArgs] = storyBookCliConfig.command[buildMode];
335347
const modulePath: string | undefined = packageJson.bin[moduleExecutableName];
336-
this._logger.terminal.writeVerboseLine(
348+
logger.terminal.writeVerboseLine(
337349
`Found storybook "${modulePath}" for "${buildMode}" mode in "${cliPackageName}"`
338350
);
339351

@@ -353,12 +365,12 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
353365
buildMode === StorybookBuildMode.WATCH ? undefined : staticBuildOutputFolder;
354366

355367
if (!modulePath) {
356-
this._logger.terminal.writeVerboseLine(
368+
logger.terminal.writeVerboseLine(
357369
'No matching module path option specified in heft.json, so bundling will proceed without Storybook'
358370
);
359371
}
360372

361-
this._logger.terminal.writeVerboseLine(`Resolving modulePath "${modulePath}"`);
373+
logger.terminal.writeVerboseLine(`Resolving modulePath "${modulePath}"`);
362374
let resolvedModulePath: string;
363375
try {
364376
resolvedModulePath = Import.resolveModule({
@@ -368,7 +380,7 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
368380
} catch (ex) {
369381
throw new Error(`The ${taskSession.taskName} task cannot start: ` + (ex as Error).message);
370382
}
371-
this._logger.terminal.writeVerboseLine(`Resolved modulePath is "${resolvedModulePath}"`);
383+
logger.terminal.writeVerboseLine(`Resolved modulePath is "${resolvedModulePath}"`);
372384

373385
// Example: "/path/to/my-project/.storybook"
374386
const dotStorybookFolderPath: string = `${heftConfiguration.buildFolderPath}/.storybook`;
@@ -390,6 +402,7 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
390402
});
391403

392404
return {
405+
...options,
393406
workingDirectory: heftConfiguration.buildFolderPath,
394407
resolvedModulePath,
395408
moduleDefaultArgs,
@@ -402,10 +415,10 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
402415
runStorybookOptions: IRunStorybookOptions,
403416
options: IStorybookPluginOptions
404417
): Promise<void> {
405-
const { resolvedModulePath, verbose } = runStorybookOptions;
418+
const { logger, resolvedModulePath, verbose, isServeMode, isTestMode, isDocsMode } = runStorybookOptions;
406419
let { workingDirectory, outputFolder } = runStorybookOptions;
407-
this._logger.terminal.writeLine('Running Storybook compilation');
408-
this._logger.terminal.writeVerboseLine(`Loading Storybook module "${resolvedModulePath}"`);
420+
logger.terminal.writeLine('Running Storybook compilation');
421+
logger.terminal.writeVerboseLine(`Loading Storybook module "${resolvedModulePath}"`);
409422
const storybookCliVersion: `${StorybookCliVersion}` = this._getStorybookVersion(options);
410423

411424
/**
@@ -424,34 +437,42 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
424437
baseFolderPath: workingDirectory
425438
});
426439

427-
this._logger.terminal.writeVerboseLine(`Changing Storybook working directory to "${workingDirectory}"`);
440+
logger.terminal.writeVerboseLine(`Changing Storybook working directory to "${workingDirectory}"`);
428441
}
429442

430443
const storybookArgs: string[] = runStorybookOptions.moduleDefaultArgs ?? [];
431444

432445
if (outputFolder) {
433446
storybookArgs.push('--output-dir', outputFolder);
434447
}
448+
435449
if (options.captureWebpackStats) {
436450
storybookArgs.push('--webpack-stats-json');
437451
}
452+
438453
if (!verbose) {
439454
storybookArgs.push('--quiet');
440455
}
441-
if (this._isTestMode) {
456+
457+
if (isTestMode) {
442458
storybookArgs.push('--test');
443459
}
444460

445-
if (this._isServeMode) {
461+
if (isDocsMode) {
462+
storybookArgs.push('--docs');
463+
}
464+
465+
if (isServeMode) {
446466
// Instantiate storybook runner synchronously for incremental builds
447467
// this ensure that the process is not killed when heft watcher detects file changes
448468
this._invokeSync(
469+
logger,
449470
resolvedModulePath,
450471
storybookArgs,
451472
storybookCliVersion === StorybookCliVersion.STORYBOOK8
452473
);
453474
} else {
454-
await this._invokeAsSubprocessAsync(resolvedModulePath, storybookArgs, workingDirectory);
475+
await this._invokeAsSubprocessAsync(logger, resolvedModulePath, storybookArgs, workingDirectory);
455476
}
456477
}
457478

@@ -462,7 +483,12 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
462483
* @param cwd - working directory
463484
* @returns
464485
*/
465-
private async _invokeAsSubprocessAsync(command: string, args: string[], cwd: string): Promise<void> {
486+
private async _invokeAsSubprocessAsync(
487+
logger: IScopedLogger,
488+
command: string,
489+
args: string[],
490+
cwd: string
491+
): Promise<void> {
466492
return await new Promise<void>((resolve, reject) => {
467493
const storybookEnv: NodeJS.ProcessEnv = { ...process.env };
468494
const forkedProcess: child_process.ChildProcess = child_process.fork(command, args, {
@@ -479,12 +505,12 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
479505
if (childPid === undefined) {
480506
throw new InternalError(`Failed to spawn child process`);
481507
}
482-
this._logger.terminal.writeVerboseLine(`Started storybook process #${childPid}`);
508+
logger.terminal.writeVerboseLine(`Started storybook process #${childPid}`);
483509

484510
// Apply the pipe here instead of doing it in the forked process args due to a bug in Node
485511
// We will output stderr to the normal stdout stream since all output is piped through
486512
// stdout. We have to rely on the exit code to determine if there was an error.
487-
const terminal: ITerminal = this._logger.terminal;
513+
const terminal: ITerminal = logger.terminal;
488514
const terminalOutStream: TerminalStreamWritable = new TerminalStreamWritable({
489515
terminal,
490516
severity: TerminalProviderSeverity.log
@@ -520,8 +546,13 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
520546
* @param args - storybook args
521547
* @param cwd - working directory
522548
*/
523-
private _invokeSync(command: string, args: string[], patchNpmConfigUserAgent: boolean): void {
524-
this._logger.terminal.writeLine('Launching ' + command);
549+
private _invokeSync(
550+
logger: IScopedLogger,
551+
command: string,
552+
args: string[],
553+
patchNpmConfigUserAgent: boolean
554+
): void {
555+
logger.terminal.writeLine('Launching ' + command);
525556

526557
// simulate storybook cli command
527558
const originalArgv: string[] = process.argv;
@@ -547,7 +578,7 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
547578
// restore original heft process argv
548579
process.argv = originalArgv;
549580

550-
this._logger.terminal.writeVerboseLine('Completed synchronous portion of launching startupModulePath');
581+
logger.terminal.writeVerboseLine('Completed synchronous portion of launching startupModulePath');
551582
}
552583

553584
private _getStorybookVersion(options: IStorybookPluginOptions): `${StorybookCliVersion}` {

0 commit comments

Comments
 (0)