Skip to content

Commit 657ab7e

Browse files
feat: includes PR feedback and suggestion plugin option simplification
1 parent e945a64 commit 657ab7e

File tree

9 files changed

+154
-71
lines changed

9 files changed

+154
-71
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# heft-storybook-react-tutorial-app
22

33
This is project builds the storybook exports from the
4-
[heft-storybook-react-tutorial](https://github.com/microsoft/rushstack-samples/tree/main/heft/heft-storybook-react-tutorial) and is a regression test for the heft-storybook-plugin `storybookPackageNameTarget` option.
4+
[heft-storybook-react-tutorial](https://github.com/microsoft/rushstack-samples/tree/main/heft/heft-storybook-react-tutorial) and is a regression test for the heft-storybook-plugin `cwdPackageName` option.

build-tests-samples/heft-storybook-react-tutorial-app/config/heft.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
"pluginPackage": "@rushstack/heft-storybook-plugin",
1313
"options": {
1414
"storykitPackageName": "heft-storybook-react-tutorial-storykit",
15-
"staticBuildModulePath": "@storybook/react/bin/build.js",
15+
"cliPackageName": "@storybook/react",
16+
"cliCallingConvention": "storybook6",
1617
"staticBuildOutputFolder": "dist",
17-
"storybookPackageNameTarget": "heft-storybook-react-tutorial"
18+
"cwdPackageName": "heft-storybook-react-tutorial"
1819
}
1920
}
2021
}

build-tests-samples/heft-storybook-react-tutorial/config/heft.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
"pluginPackage": "@rushstack/heft-storybook-plugin",
3434
"options": {
3535
"storykitPackageName": "heft-storybook-react-tutorial-storykit",
36-
"startupModulePath": "@storybook/react/bin/index.js",
37-
"staticBuildModulePath": "@storybook/react/bin/build.js",
36+
"cliPackageName": "@storybook/react",
37+
"cliCallingConvention": "storybook6",
3838
"staticBuildOutputFolder": "dist-storybook"
3939
}
4040
}

common/changes/@rushstack/heft-storybook-plugin/bartvandenende-wm-storybook-improvements_2024-01-03-11-53.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"changes": [
33
{
44
"packageName": "@rushstack/heft-storybook-plugin",
5-
"comment": "Add support for storybook 7, HMR, and a new 'storybookPackageNameTarget' plugin option. The new 'storybookPackageNameTarget' option provides the ability to set an alternative dependency name as (cwd) target for the storybook commands.",
5+
"comment": "Add support for storybook 7, HMR, and breaking chages in the plugin configuration option. The \"startupModulePath\" and \"staticBuildModulePath\" have been removed in favour of \"cliCallingConvention\" and \"cliPackageName\". A new 'cwdPackageName' option provides the ability to set an alternative dependency name as (cwd) target for the storybook commands." ,
66
"type": "minor"
77
}
88
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/node-core-library",
5+
"comment": "Improve 'bin' definition in `IPackageJson` type",
6+
"type": "patch"
7+
}
8+
],
9+
"packageName": "@rushstack/node-core-library"
10+
}

common/reviews/api/node-core-library.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ export class Import {
550550

551551
// @public
552552
export interface INodePackageJson {
553-
bin?: string;
553+
bin?: string | Record<string, string>;
554554
dependencies?: IPackageJsonDependencyTable;
555555
description?: string;
556556
devDependencies?: IPackageJsonDependencyTable;

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

Lines changed: 123 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// See LICENSE in the project root for license information.
33

44
import * as child_process from 'child_process';
5+
import * as path from 'path';
6+
57
import {
68
AlreadyExistsBehavior,
79
FileSystem,
@@ -12,7 +14,10 @@ import {
1214
TerminalWritable,
1315
type ITerminal,
1416
TerminalProviderSeverity,
15-
InternalError
17+
FileConstants,
18+
type IPackageJson,
19+
InternalError,
20+
JsonFile
1621
} from '@rushstack/node-core-library';
1722
import type {
1823
HeftConfiguration,
@@ -30,12 +35,41 @@ import type {
3035
PluginName as Webpack5PluginName,
3136
IWebpackPluginAccessor as IWebpack5PluginAccessor
3237
} from '@rushstack/heft-webpack5-plugin';
33-
import * as path from 'path';
3438

3539
const PLUGIN_NAME: 'storybook-plugin' = 'storybook-plugin';
3640
const WEBPACK4_PLUGIN_NAME: typeof Webpack4PluginName = 'webpack4-plugin';
3741
const WEBPACK5_PLUGIN_NAME: typeof Webpack5PluginName = 'webpack5-plugin';
3842

43+
/**
44+
* Storybook CLI build type targets
45+
*/
46+
enum StorybookBuildMode {
47+
/**
48+
* Invoke storybook in watch mode
49+
*/
50+
WATCH = 'watch',
51+
/**
52+
* Invoke storybook in build mode
53+
*/
54+
BUILD = 'build'
55+
}
56+
57+
/**
58+
* Storybook CLI versions
59+
*/
60+
enum StorybookCliVersion {
61+
STORYBOOK7 = 'storybook7',
62+
STORYBOOK6 = 'storybook6'
63+
}
64+
65+
/**
66+
* Configuration object holding default storybook cli package and command
67+
*/
68+
interface IStorybookCliCallingConfig {
69+
command: Record<StorybookBuildMode, string[]>;
70+
packageName: string;
71+
}
72+
3973
/**
4074
* Options for `StorybookPlugin`.
4175
*
@@ -67,26 +101,31 @@ export interface IStorybookPluginOptions {
67101
storykitPackageName: string;
68102

69103
/**
70-
* The module entry point that Heft serve mode should use to launch the Storybook toolchain.
71-
* Typically it is the path loaded the `start-storybook` shell script.
104+
* Specify how the Storybook CLI should be invoked. Possible values:
72105
*
73-
* @example
74-
* If you are using `@storybook/react`, then the startup path would be:
106+
* - "storybook6": For a static build, Heft will expect the cliPackageName package
107+
* to define a binary command named "build-storybook". For the dev server mode,
108+
* Heft will expect to find a binary command named "start-storybook". These commands
109+
* must be declared in the "bin" section of package.json since Heft invokes the script directly.
110+
* The output folder will be specified using the "--output-dir" CLI parameter.
111+
*
112+
* - "storybook7": Heft looks for a single binary command named "sb". It will be invoked as
113+
* "sb build" for static builds, or "sb dev" for dev server mode.
114+
* The output folder will be specified using the "--output-dir" CLI parameter.
75115
*
76-
* `"startupModulePath": "@storybook/react/bin/index.js"`
116+
* @defaultValue `storybook7`
77117
*/
78-
startupModulePath?: string;
118+
cliCallingConvention?: `${StorybookCliVersion}`;
79119

80120
/**
81-
* The module entry point that Heft non-serve mode should use to launch the Storybook toolchain.
82-
* Typically it is the path loaded the `build-storybook` shell script.
83-
*
84-
* @example
85-
* If you are using `@storybook/react`, then the static build path would be:
121+
* Specify the NPM package that provides the CLI binary to run.
122+
* It will be resolved from the folder of your storykit package.
86123
*
87-
* `"staticBuildModulePath": "@storybook/react/bin/build.js"`
124+
* @defaultValue
125+
* The default is `@storybook/cli` when `cliCallingConvention` is `storybook7`
126+
* and `@storybook/react` when `cliCallingConvention` is `storybook6`
88127
*/
89-
staticBuildModulePath?: string;
128+
cliPackageName?: string;
90129

91130
/**
92131
* The customized output dir for storybook static build.
@@ -108,18 +147,37 @@ export interface IStorybookPluginOptions {
108147
* If you create an 'my-storybook-ui-app' project for distribution purposes and the library holding
109148
* the (storybook) sources is `my-storybook-ui-library`, then the storybook package name would be:
110149
*
111-
* `"storybookPackageNameTarget": "my-storybook-ui-library"`
150+
* `"cwdPackageName": "my-storybook-ui-library"`
112151
*/
113-
storybookPackageNameTarget?: string;
152+
cwdPackageName?: string;
114153
}
115154

116155
interface IRunStorybookOptions {
117156
workingDirectory: string;
118157
resolvedModulePath: string;
119158
outputFolder: string | undefined;
159+
moduleDefaultArgs: string[];
120160
verbose: boolean;
121161
}
122162

163+
const DEFAULT_STORYBOOK_VERSION: StorybookCliVersion = StorybookCliVersion.STORYBOOK7;
164+
const DEFAULT_STORYBOOK_CLI_CONFIG: Record<StorybookCliVersion, IStorybookCliCallingConfig> = {
165+
[StorybookCliVersion.STORYBOOK6]: {
166+
packageName: '@storybook/react',
167+
command: {
168+
watch: ['start-storybook'],
169+
build: ['build-storybook']
170+
}
171+
},
172+
[StorybookCliVersion.STORYBOOK7]: {
173+
packageName: '@storybook/cli',
174+
command: {
175+
watch: ['sb', 'dev'],
176+
build: ['sb', 'build']
177+
}
178+
}
179+
};
180+
123181
/** @public */
124182
export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPluginOptions> {
125183
private _logger!: IScopedLogger;
@@ -146,13 +204,6 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
146204
);
147205
}
148206

149-
if (!options.startupModulePath && !options.staticBuildModulePath) {
150-
throw new Error(
151-
`The ${taskSession.taskName} task cannot start because the "startupModulePath" and the "staticBuildModulePath"` +
152-
` plugin options were not specified`
153-
);
154-
}
155-
156207
// Only tap if the --storybook flag is present.
157208
if (storybookParameter.value) {
158209
const configureWebpackTap: () => Promise<false> = async () => {
@@ -203,10 +254,16 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
203254
heftConfiguration: HeftConfiguration,
204255
options: IStorybookPluginOptions
205256
): Promise<IRunStorybookOptions> {
206-
const { storykitPackageName, startupModulePath, staticBuildModulePath, staticBuildOutputFolder } =
207-
options;
208-
this._logger.terminal.writeVerboseLine(`Probing for "${storykitPackageName}"`);
257+
const { storykitPackageName, staticBuildOutputFolder } = options;
258+
const storybookCliVersion: `${StorybookCliVersion}` =
259+
options.cliCallingConvention ?? DEFAULT_STORYBOOK_VERSION;
260+
const storyBookCliConfig: IStorybookCliCallingConfig = DEFAULT_STORYBOOK_CLI_CONFIG[storybookCliVersion];
261+
const cliPackageName: string = options.cliPackageName ?? storyBookCliConfig.packageName;
262+
const buildMode: StorybookBuildMode = taskSession.parameters.watch
263+
? StorybookBuildMode.WATCH
264+
: StorybookBuildMode.BUILD;
209265

266+
this._logger.terminal.writeVerboseLine(`Probing for "${storykitPackageName}"`);
210267
// Example: "/path/to/my-project/node_modules/my-storykit"
211268
let storykitFolderPath: string;
212269
try {
@@ -220,6 +277,33 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
220277

221278
this._logger.terminal.writeVerboseLine(`Found "${storykitPackageName}" in ` + storykitFolderPath);
222279

280+
this._logger.terminal.writeVerboseLine(`Probing for "${cliPackageName}" in "${storykitPackageName}"`);
281+
// Example: "/path/to/my-project/node_modules/my-storykit/node_modules/@storybook/cli"
282+
let storyBookCliPackage: string;
283+
try {
284+
storyBookCliPackage = Import.resolvePackage({
285+
packageName: cliPackageName,
286+
baseFolderPath: storykitFolderPath
287+
});
288+
} catch (ex) {
289+
throw new Error(`The ${taskSession.taskName} task cannot start: ` + (ex as Error).message);
290+
}
291+
292+
this._logger.terminal.writeVerboseLine(`Found "${cliPackageName}" in ` + storyBookCliPackage);
293+
294+
const storyBookPackagePackageJsonFile: string = path.join(storyBookCliPackage, FileConstants.PackageJson);
295+
const packageJson: IPackageJson = await JsonFile.loadAsync(storyBookPackagePackageJsonFile);
296+
if (!packageJson.bin || typeof packageJson.bin === 'string') {
297+
throw new Error(
298+
`The cli package "${cliPackageName}" does not provide a 'bin' executables in the 'package.json'`
299+
);
300+
}
301+
const [moduleExecutableName, ...moduleDefaultArgs] = storyBookCliConfig.command[buildMode];
302+
const modulePath: string | undefined = packageJson.bin[moduleExecutableName];
303+
this._logger.terminal.writeVerboseLine(
304+
`Found storybook "${modulePath}" for "${buildMode}" mode in "${cliPackageName}"`
305+
);
306+
223307
// Example: "/path/to/my-project/node_modules/my-storykit/node_modules"
224308
const storykitModuleFolderPath: string = `${storykitFolderPath}/node_modules`;
225309
const storykitModuleFolderExists: boolean = await FileSystem.existsAsync(storykitModuleFolderPath);
@@ -232,8 +316,9 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
232316
}
233317

234318
// We only want to specify a different output dir when operating in build mode
235-
const outputFolder: string | undefined = this._isServeMode ? undefined : staticBuildOutputFolder;
236-
const modulePath: string | undefined = this._isServeMode ? startupModulePath : staticBuildModulePath;
319+
const outputFolder: string | undefined =
320+
buildMode === StorybookBuildMode.WATCH ? undefined : staticBuildOutputFolder;
321+
237322
if (!modulePath) {
238323
this._logger.terminal.writeVerboseLine(
239324
'No matching module path option specified in heft.json, so bundling will proceed without Storybook'
@@ -244,8 +329,8 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
244329
let resolvedModulePath: string;
245330
try {
246331
resolvedModulePath = Import.resolveModule({
247-
modulePath: modulePath!,
248-
baseFolderPath: storykitModuleFolderPath
332+
modulePath: modulePath,
333+
baseFolderPath: storyBookCliPackage
249334
});
250335
} catch (ex) {
251336
throw new Error(`The ${taskSession.taskName} task cannot start: ` + (ex as Error).message);
@@ -273,8 +358,9 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
273358

274359
return {
275360
workingDirectory: heftConfiguration.buildFolderPath,
276-
resolvedModulePath: resolvedModulePath,
277-
outputFolder: outputFolder,
361+
resolvedModulePath,
362+
moduleDefaultArgs,
363+
outputFolder,
278364
verbose: taskSession.parameters.verbose
279365
};
280366
}
@@ -289,38 +375,25 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
289375
this._logger.terminal.writeVerboseLine(`Loading Storybook module "${resolvedModulePath}"`);
290376

291377
/**
292-
* Support \'storybookPackageNameTarget\' option
378+
* Support \'cwdPackageName\' option
293379
* by changing the working directory of the storybook command
294380
*/
295-
if (options.storybookPackageNameTarget) {
381+
if (options.cwdPackageName) {
296382
// Map outputFolder to local context.
297383
if (outputFolder) {
298384
outputFolder = path.resolve(workingDirectory, outputFolder);
299385
}
300386

301387
// Update workingDirectory to target context.
302388
workingDirectory = await Import.resolvePackageAsync({
303-
packageName: options.storybookPackageNameTarget,
389+
packageName: options.cwdPackageName,
304390
baseFolderPath: workingDirectory
305391
});
306392

307393
this._logger.terminal.writeVerboseLine(`Changing Storybook working directory to "${workingDirectory}"`);
308394
}
309395

310-
const storybookArgs: string[] = [];
311-
312-
/**
313-
* Storybook 7 is using the new '\@storybook/cli' module
314-
* combining storybook-build and storybook-start commands
315-
* into a single script by using 'dev' and 'build' arguments
316-
*/
317-
if (resolvedModulePath.includes('@storybook/cli')) {
318-
if (this._isServeMode) {
319-
storybookArgs.push('dev');
320-
} else {
321-
storybookArgs.push('build');
322-
}
323-
}
396+
const storybookArgs: string[] = runStorybookOptions.moduleDefaultArgs ?? [];
324397

325398
if (outputFolder) {
326399
storybookArgs.push('--output-dir', outputFolder);

0 commit comments

Comments
 (0)