2
2
// See LICENSE in the project root for license information.
3
3
4
4
import * as child_process from 'child_process' ;
5
+ import * as path from 'path' ;
6
+
5
7
import {
6
8
AlreadyExistsBehavior ,
7
9
FileSystem ,
@@ -12,7 +14,10 @@ import {
12
14
TerminalWritable ,
13
15
type ITerminal ,
14
16
TerminalProviderSeverity ,
15
- InternalError
17
+ FileConstants ,
18
+ type IPackageJson ,
19
+ InternalError ,
20
+ JsonFile
16
21
} from '@rushstack/node-core-library' ;
17
22
import type {
18
23
HeftConfiguration ,
@@ -30,12 +35,41 @@ import type {
30
35
PluginName as Webpack5PluginName ,
31
36
IWebpackPluginAccessor as IWebpack5PluginAccessor
32
37
} from '@rushstack/heft-webpack5-plugin' ;
33
- import * as path from 'path' ;
34
38
35
39
const PLUGIN_NAME : 'storybook-plugin' = 'storybook-plugin' ;
36
40
const WEBPACK4_PLUGIN_NAME : typeof Webpack4PluginName = 'webpack4-plugin' ;
37
41
const WEBPACK5_PLUGIN_NAME : typeof Webpack5PluginName = 'webpack5-plugin' ;
38
42
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
+
39
73
/**
40
74
* Options for `StorybookPlugin`.
41
75
*
@@ -67,26 +101,31 @@ export interface IStorybookPluginOptions {
67
101
storykitPackageName : string ;
68
102
69
103
/**
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:
72
105
*
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.
75
115
*
76
- * `"startupModulePath": "@storybook/react/bin/index.js" `
116
+ * @defaultValue `storybook7 `
77
117
*/
78
- startupModulePath ?: string ;
118
+ cliCallingConvention ?: `${ StorybookCliVersion } ` ;
79
119
80
120
/**
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.
86
123
*
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`
88
127
*/
89
- staticBuildModulePath ?: string ;
128
+ cliPackageName ?: string ;
90
129
91
130
/**
92
131
* The customized output dir for storybook static build.
@@ -108,18 +147,37 @@ export interface IStorybookPluginOptions {
108
147
* If you create an 'my-storybook-ui-app' project for distribution purposes and the library holding
109
148
* the (storybook) sources is `my-storybook-ui-library`, then the storybook package name would be:
110
149
*
111
- * `"storybookPackageNameTarget ": "my-storybook-ui-library"`
150
+ * `"cwdPackageName ": "my-storybook-ui-library"`
112
151
*/
113
- storybookPackageNameTarget ?: string ;
152
+ cwdPackageName ?: string ;
114
153
}
115
154
116
155
interface IRunStorybookOptions {
117
156
workingDirectory : string ;
118
157
resolvedModulePath : string ;
119
158
outputFolder : string | undefined ;
159
+ moduleDefaultArgs : string [ ] ;
120
160
verbose : boolean ;
121
161
}
122
162
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
+
123
181
/** @public */
124
182
export default class StorybookPlugin implements IHeftTaskPlugin < IStorybookPluginOptions > {
125
183
private _logger ! : IScopedLogger ;
@@ -146,13 +204,6 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
146
204
) ;
147
205
}
148
206
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
-
156
207
// Only tap if the --storybook flag is present.
157
208
if ( storybookParameter . value ) {
158
209
const configureWebpackTap : ( ) => Promise < false > = async ( ) => {
@@ -203,10 +254,16 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
203
254
heftConfiguration : HeftConfiguration ,
204
255
options : IStorybookPluginOptions
205
256
) : 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 ;
209
265
266
+ this . _logger . terminal . writeVerboseLine ( `Probing for "${ storykitPackageName } "` ) ;
210
267
// Example: "/path/to/my-project/node_modules/my-storykit"
211
268
let storykitFolderPath : string ;
212
269
try {
@@ -220,6 +277,33 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
220
277
221
278
this . _logger . terminal . writeVerboseLine ( `Found "${ storykitPackageName } " in ` + storykitFolderPath ) ;
222
279
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
+
223
307
// Example: "/path/to/my-project/node_modules/my-storykit/node_modules"
224
308
const storykitModuleFolderPath : string = `${ storykitFolderPath } /node_modules` ;
225
309
const storykitModuleFolderExists : boolean = await FileSystem . existsAsync ( storykitModuleFolderPath ) ;
@@ -232,8 +316,9 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
232
316
}
233
317
234
318
// 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
+
237
322
if ( ! modulePath ) {
238
323
this . _logger . terminal . writeVerboseLine (
239
324
'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
244
329
let resolvedModulePath : string ;
245
330
try {
246
331
resolvedModulePath = Import . resolveModule ( {
247
- modulePath : modulePath ! ,
248
- baseFolderPath : storykitModuleFolderPath
332
+ modulePath : modulePath ,
333
+ baseFolderPath : storyBookCliPackage
249
334
} ) ;
250
335
} catch ( ex ) {
251
336
throw new Error ( `The ${ taskSession . taskName } task cannot start: ` + ( ex as Error ) . message ) ;
@@ -273,8 +358,9 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
273
358
274
359
return {
275
360
workingDirectory : heftConfiguration . buildFolderPath ,
276
- resolvedModulePath : resolvedModulePath ,
277
- outputFolder : outputFolder ,
361
+ resolvedModulePath,
362
+ moduleDefaultArgs,
363
+ outputFolder,
278
364
verbose : taskSession . parameters . verbose
279
365
} ;
280
366
}
@@ -289,38 +375,25 @@ export default class StorybookPlugin implements IHeftTaskPlugin<IStorybookPlugin
289
375
this . _logger . terminal . writeVerboseLine ( `Loading Storybook module "${ resolvedModulePath } "` ) ;
290
376
291
377
/**
292
- * Support \'storybookPackageNameTarget \' option
378
+ * Support \'cwdPackageName \' option
293
379
* by changing the working directory of the storybook command
294
380
*/
295
- if ( options . storybookPackageNameTarget ) {
381
+ if ( options . cwdPackageName ) {
296
382
// Map outputFolder to local context.
297
383
if ( outputFolder ) {
298
384
outputFolder = path . resolve ( workingDirectory , outputFolder ) ;
299
385
}
300
386
301
387
// Update workingDirectory to target context.
302
388
workingDirectory = await Import . resolvePackageAsync ( {
303
- packageName : options . storybookPackageNameTarget ,
389
+ packageName : options . cwdPackageName ,
304
390
baseFolderPath : workingDirectory
305
391
} ) ;
306
392
307
393
this . _logger . terminal . writeVerboseLine ( `Changing Storybook working directory to "${ workingDirectory } "` ) ;
308
394
}
309
395
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 ?? [ ] ;
324
397
325
398
if ( outputFolder ) {
326
399
storybookArgs . push ( '--output-dir' , outputFolder ) ;
0 commit comments