storybook-addon-dependency-previews 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/cli/sb-deps.cjs +204 -52
- package/dist/cli/sb-deps.cjs.map +1 -1
- package/dist/cli/sb-deps.mjs +205 -53
- package/dist/cli/sb-deps.mjs.map +1 -1
- package/dist/cli/setup/index.cjs +41 -3
- package/dist/cli/setup/index.cjs.map +1 -1
- package/dist/cli/setup/index.mjs +43 -5
- package/dist/cli/setup/index.mjs.map +1 -1
- package/dist/cli/setup/patchers/main.cjs +66 -0
- package/dist/cli/setup/patchers/main.cjs.map +1 -1
- package/dist/cli/setup/patchers/main.d.cts +23 -1
- package/dist/cli/setup/patchers/main.d.mts +23 -1
- package/dist/cli/setup/patchers/main.mjs +66 -1
- package/dist/cli/setup/patchers/main.mjs.map +1 -1
- package/dist/cli/setup/patchers/preview.cjs +1 -1
- package/dist/cli/setup/patchers/preview.cjs.map +1 -1
- package/dist/cli/setup/patchers/preview.mjs +1 -1
- package/dist/cli/setup/patchers/preview.mjs.map +1 -1
- package/dist/cli/setup/patchers/sbDepsConfig.cjs +31 -12
- package/dist/cli/setup/patchers/sbDepsConfig.cjs.map +1 -1
- package/dist/cli/setup/patchers/sbDepsConfig.mjs +31 -12
- package/dist/cli/setup/patchers/sbDepsConfig.mjs.map +1 -1
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +8 -0
- package/dist/config.d.mts +8 -0
- package/dist/config.mjs.map +1 -1
- package/dist/hooks/useDynamicStory.cjs +1 -1
- package/dist/hooks/useDynamicStory.cjs.map +1 -1
- package/dist/hooks/useDynamicStory.mjs +1 -1
- package/dist/hooks/useDynamicStory.mjs.map +1 -1
- package/dist/types.d.cts +1 -1
- package/dist/types.d.mts +1 -1
- package/package.json +122 -122
package/README.md
CHANGED
|
@@ -97,6 +97,17 @@ The wizard supports React (`@storybook/react-vite`), Svelte (`@storybook/sveltek
|
|
|
97
97
|
- [Manual setup — Vite (React, Svelte, Vue 3)](https://github.com/Dan503/storybook-addon-dependency-previews/blob/main/storybook-addon-package/docs/manual-setup-vite.md)
|
|
98
98
|
- [Manual setup — webpack (`@storybook/angular`, `@storybook/nextjs`)](https://github.com/Dan503/storybook-addon-dependency-previews/blob/main/storybook-addon-package/docs/manual-setup-webpack.md)
|
|
99
99
|
|
|
100
|
+
## Auto-scaffolding new components and stories
|
|
101
|
+
|
|
102
|
+
While `sb-deps` is watching (`npm run sb`), creating an **empty** source file fills it in from a template — and creates its matching sibling too. It works from either side:
|
|
103
|
+
|
|
104
|
+
- **Create a component file** (`Button.tsx`, `Button.svelte`, `Button.vue`, `Button.component.ts`) → the component body is scaffolded **and** a matching story file is generated next to it.
|
|
105
|
+
- **Create a story file** (`Button.stories.tsx`, or the singular `Button.story.tsx`) → the story is scaffolded into that exact file, and if the sibling component doesn't exist yet it is created and scaffolded too.
|
|
106
|
+
|
|
107
|
+
Either way you end up with a working component + story pair. Only empty files are touched, so existing files are never overwritten. A `.stories.ts` with no component beside it is resolved to React, Vue, or Angular from your project's framework (Svelte stories use a `.svelte` file, so `.ts` isn't scaffolded for Svelte).
|
|
108
|
+
|
|
109
|
+
**Tip — if a brand-new story shows `importers[path] is not a function` in Storybook**, just reload the browser tab. This is an occasional Storybook dev-server timing quirk when a story file is added while the dev server is running (the preview's internal module map briefly lags behind); a refresh clears it and the scaffolded files themselves are correct. Creating the **component** first (and letting the story auto-generate) avoids the hiccup entirely.
|
|
110
|
+
|
|
100
111
|
## Configuration file (optional)
|
|
101
112
|
|
|
102
113
|
The `sb-deps` CLI can be customized via a `sb-deps.config.mjs` file in your project root (alongside `package.json`). Supported formats: `.mjs`, `.js`, or `.cjs`.
|
|
@@ -154,6 +165,26 @@ export default defineSbDepsConfig({
|
|
|
154
165
|
})
|
|
155
166
|
```
|
|
156
167
|
|
|
168
|
+
### `storybookFileExtension`
|
|
169
|
+
|
|
170
|
+
The extension `sb-deps` uses when it auto-scaffolds a story file for a new component — `'stories'` (Storybook's convention) or `'story'`. The setup wizard asks for this preference.
|
|
171
|
+
|
|
172
|
+
**Default:** `'stories'`
|
|
173
|
+
|
|
174
|
+
| Value | Generated story file |
|
|
175
|
+
| -------------------- | -------------------- |
|
|
176
|
+
| `'stories'` (default) | `ButtonAtom.stories.tsx` |
|
|
177
|
+
| `'story'` | `ButtonAtom.story.tsx` |
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
// sb-deps.config.mjs
|
|
181
|
+
import { defineSbDepsConfig } from 'storybook-addon-dependency-previews/config'
|
|
182
|
+
|
|
183
|
+
export default defineSbDepsConfig({
|
|
184
|
+
storybookFileExtension: 'story',
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
157
188
|
### `scaffold`
|
|
158
189
|
|
|
159
190
|
Override the templates used when `sb-deps` auto-scaffolds new component and story files. Each template function receives a context object with relevant variables and must return the full file content as a string.
|
package/dist/cli/sb-deps.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
3
|
+
const require_cli_setup_detect = require('./setup/detect.cjs');
|
|
3
4
|
const require_cli_setup_index = require('./setup/index.cjs');
|
|
4
5
|
let node_fs = require("node:fs");
|
|
5
6
|
node_fs = require_rolldown_runtime.__toESM(node_fs);
|
|
@@ -84,6 +85,12 @@ async function loadSbDepsConfig() {
|
|
|
84
85
|
let ANGULAR_SELECTOR_PREFIX = "app-";
|
|
85
86
|
let SCAFFOLD_CONFIG = {};
|
|
86
87
|
let SRC_DIR = "src";
|
|
88
|
+
let STORYBOOK_FILE_EXTENSION = "stories";
|
|
89
|
+
let cachedProjectFramework = null;
|
|
90
|
+
function getProjectFramework() {
|
|
91
|
+
if (cachedProjectFramework === null) cachedProjectFramework = require_cli_setup_detect.detectProject(projectRoot).framework;
|
|
92
|
+
return cachedProjectFramework;
|
|
93
|
+
}
|
|
87
94
|
const IS_WIN = process.platform === "win32";
|
|
88
95
|
/**
|
|
89
96
|
* Locate the `dependency-cruiser` CLI binary in the user's `node_modules/.bin`.
|
|
@@ -214,7 +221,11 @@ function srcSubpathRegex(suffixPattern) {
|
|
|
214
221
|
* `.js`, `.jsx`, `.mdx`, …). Used by the scaffolding-trigger helpers to make
|
|
215
222
|
* sure they don't try to auto-scaffold a story file for an existing story.
|
|
216
223
|
*/
|
|
217
|
-
const STORY_FILE_REGEX = /\.
|
|
224
|
+
const STORY_FILE_REGEX = /\.stor(?:y|ies)\.\w+$/i;
|
|
225
|
+
function isStoryFileUnderSrc(absPath) {
|
|
226
|
+
const norm = absPath.replace(/\\/g, "/");
|
|
227
|
+
return srcSubpathRegex(STORY_FILE_REGEX.source).test(norm);
|
|
228
|
+
}
|
|
218
229
|
function isComponentsTsx(absPath) {
|
|
219
230
|
const norm = absPath.replace(/\\/g, "/");
|
|
220
231
|
return srcSubpathRegex("\\.tsx$").test(norm) && !STORY_FILE_REGEX.test(norm);
|
|
@@ -278,17 +289,17 @@ function detectAtomicTag(absPath) {
|
|
|
278
289
|
function storyPathForComponent(absCompPath) {
|
|
279
290
|
const dir = (0, node_path.dirname)(absCompPath);
|
|
280
291
|
const base = componentBaseFromComponent(absCompPath);
|
|
281
|
-
return (0, node_path.join)(dir, `${base}.
|
|
292
|
+
return (0, node_path.join)(dir, `${base}.${STORYBOOK_FILE_EXTENSION}.tsx`);
|
|
282
293
|
}
|
|
283
294
|
function storyPathForSvelteComponent(absCompPath) {
|
|
284
295
|
const dir = (0, node_path.dirname)(absCompPath);
|
|
285
296
|
const base = componentBaseFromSvelteComponent(absCompPath);
|
|
286
|
-
return (0, node_path.join)(dir, `${base}.
|
|
297
|
+
return (0, node_path.join)(dir, `${base}.${STORYBOOK_FILE_EXTENSION}.svelte`);
|
|
287
298
|
}
|
|
288
299
|
function storyPathForVueComponent(absCompPath) {
|
|
289
300
|
const dir = (0, node_path.dirname)(absCompPath);
|
|
290
301
|
const base = componentBaseFromVueComponent(absCompPath);
|
|
291
|
-
return (0, node_path.join)(dir, `${base}.
|
|
302
|
+
return (0, node_path.join)(dir, `${base}.${STORYBOOK_FILE_EXTENSION}.ts`);
|
|
292
303
|
}
|
|
293
304
|
function makeTitleFromComponent(absCompPath) {
|
|
294
305
|
const srcRoot = (0, node_path.join)(projectRoot, SRC_DIR) + node_path.sep;
|
|
@@ -327,9 +338,9 @@ export function ${componentName}({ children }: ${propsName}) {
|
|
|
327
338
|
}
|
|
328
339
|
`;
|
|
329
340
|
(0, node_fs.writeFileSync)(absCompPath, tpl, "utf8");
|
|
330
|
-
info(`scaffolded component →
|
|
341
|
+
info(`scaffolded component → ${rel(absCompPath)}`);
|
|
331
342
|
}
|
|
332
|
-
function scaffoldStoryForComponent(absCompPath) {
|
|
343
|
+
function scaffoldStoryForComponent(absCompPath, targetStoryPath = storyPathForComponent(absCompPath)) {
|
|
333
344
|
const base = componentBaseFromComponent(absCompPath);
|
|
334
345
|
const componentName = toPascalCase(base);
|
|
335
346
|
const propsName = `PropsFor${componentName}`;
|
|
@@ -345,7 +356,7 @@ function scaffoldStoryForComponent(absCompPath) {
|
|
|
345
356
|
base
|
|
346
357
|
}) ?? `import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
347
358
|
import type { StoryParameters } from 'storybook-addon-dependency-previews'
|
|
348
|
-
import { ${componentName}, type ${propsName} } from './${
|
|
359
|
+
import { ${componentName}, type ${propsName} } from './${base}'
|
|
349
360
|
|
|
350
361
|
const meta: Meta<typeof ${componentName}> = {
|
|
351
362
|
title: '${title}',
|
|
@@ -364,15 +375,31 @@ export const Primary: Story = {
|
|
|
364
375
|
args: {} satisfies ${propsName},
|
|
365
376
|
}
|
|
366
377
|
`;
|
|
367
|
-
|
|
368
|
-
(
|
|
369
|
-
|
|
370
|
-
return storyPath;
|
|
378
|
+
(0, node_fs.writeFileSync)(targetStoryPath, storyTpl, "utf8");
|
|
379
|
+
info(`scaffolded story → ${rel(targetStoryPath)}`);
|
|
380
|
+
return targetStoryPath;
|
|
371
381
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Return the on-disk story file for `canonicalStoryPath` if one already exists —
|
|
384
|
+
* under either `.story.` / `.stories.` naming AND, for React, either extension
|
|
385
|
+
* (canonical `.tsx`, or a JSX-free `.ts`). The canonical path is built from
|
|
386
|
+
* `STORYBOOK_FILE_EXTENSION` and the component's own extension, so it may not
|
|
387
|
+
* match the exact file the user authored; checking every variant is what stops
|
|
388
|
+
* `ensureStoryFor` emitting a duplicate story when the sibling was created under
|
|
389
|
+
* a different naming or extension.
|
|
390
|
+
*/
|
|
391
|
+
function findExistingStory(canonicalStoryPath) {
|
|
392
|
+
const namingVariants = [canonicalStoryPath];
|
|
393
|
+
const alternateStoryPath = getAlternateStoryNaming(canonicalStoryPath);
|
|
394
|
+
if (alternateStoryPath) namingVariants.push(alternateStoryPath);
|
|
395
|
+
const allVariants = namingVariants.flatMap((path) => path.endsWith(".tsx") ? [path, path.replace(/\.tsx$/, ".ts")] : [path]);
|
|
396
|
+
return allVariants.find((path) => (0, node_fs.existsSync)(path)) ?? null;
|
|
397
|
+
}
|
|
398
|
+
/** Swap a story path between its `.story.<ext>` and `.stories.<ext>` naming. */
|
|
399
|
+
function getAlternateStoryNaming(storyPath) {
|
|
400
|
+
if (/\.story\.\w+$/i.test(storyPath)) return storyPath.replace(/\.story\.(\w+)$/i, ".stories.$1");
|
|
401
|
+
if (/\.stories\.\w+$/i.test(storyPath)) return storyPath.replace(/\.stories\.(\w+)$/i, ".story.$1");
|
|
402
|
+
return null;
|
|
376
403
|
}
|
|
377
404
|
function scaffoldSvelteComponent(absCompPath) {
|
|
378
405
|
const base = componentBaseFromSvelteComponent(absCompPath);
|
|
@@ -435,7 +462,7 @@ function makeTitleFromSvelteComponent(absCompPath) {
|
|
|
435
462
|
const dedupedStoryPath = [...new Set(fullStoryPath)];
|
|
436
463
|
return dedupedStoryPath.join(" / ");
|
|
437
464
|
}
|
|
438
|
-
function scaffoldStoryForSvelteComponent(absCompPath) {
|
|
465
|
+
function scaffoldStoryForSvelteComponent(absCompPath, targetStoryPath = storyPathForSvelteComponent(absCompPath)) {
|
|
439
466
|
const base = componentBaseFromSvelteComponent(absCompPath);
|
|
440
467
|
const componentName = toPascalCase(base);
|
|
441
468
|
const title = makeTitleFromSvelteComponent(absCompPath);
|
|
@@ -448,7 +475,7 @@ function scaffoldStoryForSvelteComponent(absCompPath) {
|
|
|
448
475
|
tags
|
|
449
476
|
}) ?? `<script lang="ts" module>
|
|
450
477
|
import type { StoryParameters } from 'storybook-addon-dependency-previews'
|
|
451
|
-
import ${componentName}, { type PropsFor${componentName} } from './${
|
|
478
|
+
import ${componentName}, { type PropsFor${componentName} } from './${base}.svelte'
|
|
452
479
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
453
480
|
|
|
454
481
|
const { Story } = defineMeta({
|
|
@@ -466,15 +493,9 @@ function scaffoldStoryForSvelteComponent(absCompPath) {
|
|
|
466
493
|
<p>${title}</p>
|
|
467
494
|
</Story>
|
|
468
495
|
`;
|
|
469
|
-
|
|
470
|
-
(
|
|
471
|
-
|
|
472
|
-
return storyPath;
|
|
473
|
-
}
|
|
474
|
-
function ensureStoryForSvelteComponent(absCompPath) {
|
|
475
|
-
const sPath = storyPathForSvelteComponent(absCompPath);
|
|
476
|
-
if ((0, node_fs.existsSync)(sPath)) return null;
|
|
477
|
-
return scaffoldStoryForSvelteComponent(absCompPath);
|
|
496
|
+
(0, node_fs.writeFileSync)(targetStoryPath, storyTpl, "utf8");
|
|
497
|
+
info(`scaffolded svelte story → ${rel(targetStoryPath)}`);
|
|
498
|
+
return targetStoryPath;
|
|
478
499
|
}
|
|
479
500
|
function scaffoldVueComponent(absCompPath) {
|
|
480
501
|
const base = componentBaseFromVueComponent(absCompPath);
|
|
@@ -509,7 +530,7 @@ function makeTitleFromVueComponent(absCompPath) {
|
|
|
509
530
|
const dedupedStoryPath = [...new Set(fullStoryPath)];
|
|
510
531
|
return dedupedStoryPath.join(" / ");
|
|
511
532
|
}
|
|
512
|
-
function scaffoldStoryForVueComponent(absCompPath) {
|
|
533
|
+
function scaffoldStoryForVueComponent(absCompPath, targetStoryPath = storyPathForVueComponent(absCompPath)) {
|
|
513
534
|
const base = componentBaseFromVueComponent(absCompPath);
|
|
514
535
|
const componentName = toPascalCase(base);
|
|
515
536
|
const title = makeTitleFromVueComponent(absCompPath);
|
|
@@ -522,7 +543,7 @@ function scaffoldStoryForVueComponent(absCompPath) {
|
|
|
522
543
|
tags
|
|
523
544
|
}) ?? `import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
524
545
|
import type { StoryParameters } from 'storybook-addon-dependency-previews'
|
|
525
|
-
import ${componentName}, { type PropsFor${componentName} } from './${
|
|
546
|
+
import ${componentName}, { type PropsFor${componentName} } from './${base}.vue'
|
|
526
547
|
|
|
527
548
|
const meta: Meta<typeof ${componentName}> = {
|
|
528
549
|
title: '${title}',
|
|
@@ -551,20 +572,14 @@ export const Primary: Story = {
|
|
|
551
572
|
}),
|
|
552
573
|
}
|
|
553
574
|
`;
|
|
554
|
-
|
|
555
|
-
(
|
|
556
|
-
|
|
557
|
-
return storyPath;
|
|
558
|
-
}
|
|
559
|
-
function ensureStoryForVueComponent(absCompPath) {
|
|
560
|
-
const sPath = storyPathForVueComponent(absCompPath);
|
|
561
|
-
if ((0, node_fs.existsSync)(sPath)) return null;
|
|
562
|
-
return scaffoldStoryForVueComponent(absCompPath);
|
|
575
|
+
(0, node_fs.writeFileSync)(targetStoryPath, storyTpl, "utf8");
|
|
576
|
+
info(`scaffolded vue story → ${rel(targetStoryPath)}`);
|
|
577
|
+
return targetStoryPath;
|
|
563
578
|
}
|
|
564
579
|
function storyPathForAngularComponent(absCompPath) {
|
|
565
580
|
const dir = (0, node_path.dirname)(absCompPath);
|
|
566
581
|
const base = componentBaseFromAngularComponent(absCompPath);
|
|
567
|
-
return (0, node_path.join)(dir, `${base}.
|
|
582
|
+
return (0, node_path.join)(dir, `${base}.${STORYBOOK_FILE_EXTENSION}.ts`);
|
|
568
583
|
}
|
|
569
584
|
function makeTitleFromAngularComponent(absCompPath) {
|
|
570
585
|
const srcRoot = (0, node_path.join)(projectRoot, SRC_DIR) + node_path.sep;
|
|
@@ -717,7 +732,7 @@ function scaffoldAngularHtmlFromTs(absHtmlPath, absTsPath) {
|
|
|
717
732
|
(0, node_fs.writeFileSync)(absHtmlPath, htmlContent, "utf8");
|
|
718
733
|
info(`scaffolded angular template → ${rel(absHtmlPath)}`);
|
|
719
734
|
}
|
|
720
|
-
function scaffoldStoryForAngularComponent(absCompPath) {
|
|
735
|
+
function scaffoldStoryForAngularComponent(absCompPath, targetStoryPath = storyPathForAngularComponent(absCompPath)) {
|
|
721
736
|
const base = componentBaseFromAngularComponent(absCompPath);
|
|
722
737
|
const componentName = toPascalCase(base);
|
|
723
738
|
const className = `${componentName}Component`;
|
|
@@ -752,15 +767,132 @@ export const Primary: Story = {
|
|
|
752
767
|
args: {},
|
|
753
768
|
}
|
|
754
769
|
`;
|
|
755
|
-
|
|
756
|
-
(
|
|
757
|
-
|
|
758
|
-
return storyPath;
|
|
770
|
+
(0, node_fs.writeFileSync)(targetStoryPath, storyTpl, "utf8");
|
|
771
|
+
info(`scaffolded angular story → ${rel(targetStoryPath)}`);
|
|
772
|
+
return targetStoryPath;
|
|
759
773
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
774
|
+
/**
|
|
775
|
+
* Per-framework scaffolders + story-path helper, keyed by `StoryFramework`. Lets
|
|
776
|
+
* `ensureStoryFor` and `scaffoldStoryFromCreatedStoryFile` pick the right trio
|
|
777
|
+
* without four near-identical branches per operation. Angular's component
|
|
778
|
+
* scaffolder takes a template-style arg, wrapped here to `'internal'` (inline
|
|
779
|
+
* template) to match the signature.
|
|
780
|
+
*/
|
|
781
|
+
const STORY_SCAFFOLDERS = {
|
|
782
|
+
react: {
|
|
783
|
+
component: scaffoldComponent,
|
|
784
|
+
story: scaffoldStoryForComponent,
|
|
785
|
+
storyPath: storyPathForComponent
|
|
786
|
+
},
|
|
787
|
+
svelte: {
|
|
788
|
+
component: scaffoldSvelteComponent,
|
|
789
|
+
story: scaffoldStoryForSvelteComponent,
|
|
790
|
+
storyPath: storyPathForSvelteComponent
|
|
791
|
+
},
|
|
792
|
+
vue: {
|
|
793
|
+
component: scaffoldVueComponent,
|
|
794
|
+
story: scaffoldStoryForVueComponent,
|
|
795
|
+
storyPath: storyPathForVueComponent
|
|
796
|
+
},
|
|
797
|
+
angular: {
|
|
798
|
+
component: (compPath) => scaffoldAngularComponent(compPath, "internal"),
|
|
799
|
+
story: scaffoldStoryForAngularComponent,
|
|
800
|
+
storyPath: storyPathForAngularComponent
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
/**
|
|
804
|
+
* Ensure the component at `absCompPath` has a story file, scaffolding one if it
|
|
805
|
+
* doesn't already exist under either naming variant. The component-side mirror
|
|
806
|
+
* of `scaffoldStoryFromCreatedStoryFile`; called when a component file is
|
|
807
|
+
* created. Returns the scaffolded story path, or `null` if one already existed.
|
|
808
|
+
*/
|
|
809
|
+
function ensureStoryFor(framework, absCompPath) {
|
|
810
|
+
const { storyPath, story } = STORY_SCAFFOLDERS[framework];
|
|
811
|
+
if (findExistingStory(storyPath(absCompPath))) return null;
|
|
812
|
+
return story(absCompPath);
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Work out the component a created story file belongs to, and which framework's
|
|
816
|
+
* scaffolders to use. `.tsx` → React, `.svelte` → Svelte, `.ts` → React, Vue,
|
|
817
|
+
* or Angular (disambiguated in `resolveTsStoryComponent`). Any other extension
|
|
818
|
+
* (`.js`, `.jsx`, `.mdx`) returns `null` — not something we scaffold.
|
|
819
|
+
*/
|
|
820
|
+
function resolveComponentForStory(absStoryPath) {
|
|
821
|
+
const storyBase = absStoryPath.replace(STORY_FILE_REGEX, "");
|
|
822
|
+
const ext = (0, node_path.extname)(absStoryPath).toLowerCase();
|
|
823
|
+
if (ext === ".tsx") return {
|
|
824
|
+
compPath: `${storyBase}.tsx`,
|
|
825
|
+
framework: "react"
|
|
826
|
+
};
|
|
827
|
+
if (ext === ".svelte") return {
|
|
828
|
+
compPath: `${storyBase}.svelte`,
|
|
829
|
+
framework: "svelte"
|
|
830
|
+
};
|
|
831
|
+
if (ext === ".ts") return resolveTsStoryComponent(storyBase);
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* A `.stories.ts` story can be React, Vue, or Angular — all three use `.ts`
|
|
836
|
+
* story files (React's scaffolded template is JSX-free, so it's valid as `.ts`
|
|
837
|
+
* even though React stories are `.tsx` by convention). Prefer an existing
|
|
838
|
+
* sibling component to decide (`<base>.tsx` → React, `<base>.vue` → Vue,
|
|
839
|
+
* `<base>.component.ts` → Angular); with none present, fall back to the
|
|
840
|
+
* project's detected framework. Svelte is intentionally excluded: its story
|
|
841
|
+
* template is `.svelte`-specific, so a `.ts` Svelte story can't be scaffolded
|
|
842
|
+
* from it.
|
|
843
|
+
*/
|
|
844
|
+
function resolveTsStoryComponent(storyBase) {
|
|
845
|
+
const reactPath = `${storyBase}.tsx`;
|
|
846
|
+
if ((0, node_fs.existsSync)(reactPath)) return {
|
|
847
|
+
compPath: reactPath,
|
|
848
|
+
framework: "react"
|
|
849
|
+
};
|
|
850
|
+
const vuePath = `${storyBase}.vue`;
|
|
851
|
+
if ((0, node_fs.existsSync)(vuePath)) return {
|
|
852
|
+
compPath: vuePath,
|
|
853
|
+
framework: "vue"
|
|
854
|
+
};
|
|
855
|
+
const angularPath = `${storyBase}.component.ts`;
|
|
856
|
+
if ((0, node_fs.existsSync)(angularPath)) return {
|
|
857
|
+
compPath: angularPath,
|
|
858
|
+
framework: "angular"
|
|
859
|
+
};
|
|
860
|
+
const projectFramework = getProjectFramework();
|
|
861
|
+
const isReactFramework = projectFramework === "react-vite" || projectFramework === "nextjs-webpack";
|
|
862
|
+
if (isReactFramework) return {
|
|
863
|
+
compPath: reactPath,
|
|
864
|
+
framework: "react"
|
|
865
|
+
};
|
|
866
|
+
if (projectFramework === "vue3-vite") return {
|
|
867
|
+
compPath: vuePath,
|
|
868
|
+
framework: "vue"
|
|
869
|
+
};
|
|
870
|
+
if (projectFramework === "angular-webpack") return {
|
|
871
|
+
compPath: angularPath,
|
|
872
|
+
framework: "angular"
|
|
873
|
+
};
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Fill a directly-created story file (the mirror of component creation). Skips
|
|
878
|
+
* non-empty files (an existing story is never clobbered) and stories whose
|
|
879
|
+
* framework/extension we don't scaffold. When the sibling component is missing,
|
|
880
|
+
* the story is filled first and the component backfilled after — the story
|
|
881
|
+
* scaffolder needs the component's path, never its contents — so the
|
|
882
|
+
* component's own watcher event then no-ops on its `ensureStoryFor` guard
|
|
883
|
+
* (the story already exists). Returns the filled story path, or `null` if
|
|
884
|
+
* nothing was scaffolded.
|
|
885
|
+
*/
|
|
886
|
+
function scaffoldStoryFromCreatedStoryFile(absStoryPath) {
|
|
887
|
+
if (!isEmptyOrWhitespace(absStoryPath)) return null;
|
|
888
|
+
const resolved = resolveComponentForStory(absStoryPath);
|
|
889
|
+
if (!resolved) return null;
|
|
890
|
+
const { compPath, framework } = resolved;
|
|
891
|
+
const { component: scaffoldFrameworkComponent, story: scaffoldFrameworkStory } = STORY_SCAFFOLDERS[framework];
|
|
892
|
+
const isComponentMissing = !(0, node_fs.existsSync)(compPath) || isEmptyOrWhitespace(compPath);
|
|
893
|
+
const createdStory = scaffoldFrameworkStory(compPath, absStoryPath);
|
|
894
|
+
if (isComponentMissing) scaffoldFrameworkComponent(compPath);
|
|
895
|
+
return createdStory;
|
|
764
896
|
}
|
|
765
897
|
function startWatcher() {
|
|
766
898
|
const root = projectRoot;
|
|
@@ -809,6 +941,9 @@ function startWatcher() {
|
|
|
809
941
|
kick("unlink", abs);
|
|
810
942
|
continue;
|
|
811
943
|
}
|
|
944
|
+
if (isStoryFileUnderSrc(abs) && ev.type === "create") {
|
|
945
|
+
if (handleStoryCreation(abs)) continue;
|
|
946
|
+
}
|
|
812
947
|
if (isComponentsTsx(abs) && ev.type === "create") {
|
|
813
948
|
await handleComponentCreation(abs, relPath);
|
|
814
949
|
continue;
|
|
@@ -832,7 +967,7 @@ function startWatcher() {
|
|
|
832
967
|
scaffoldAngularHtmlFromTs(abs, tsPath);
|
|
833
968
|
}
|
|
834
969
|
console.log("Angular component creation detected:", rel(abs));
|
|
835
|
-
const createdStory =
|
|
970
|
+
const createdStory = ensureStoryFor("angular", tsPath);
|
|
836
971
|
if (createdStory) {
|
|
837
972
|
kick("create:story", createdStory);
|
|
838
973
|
}
|
|
@@ -847,12 +982,20 @@ function startWatcher() {
|
|
|
847
982
|
info("watching… (ready)");
|
|
848
983
|
return result;
|
|
849
984
|
}).catch((e) => error(`watch init failed ${e?.message || e}`));
|
|
985
|
+
function handleStoryCreation(abs) {
|
|
986
|
+
const createdStory = scaffoldStoryFromCreatedStoryFile(abs);
|
|
987
|
+
if (createdStory) {
|
|
988
|
+
kick("create:story", createdStory);
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
850
993
|
async function handleComponentCreation(abs, relPath) {
|
|
851
994
|
if (isEmptyOrWhitespace(abs)) {
|
|
852
995
|
scaffoldComponent(abs);
|
|
853
996
|
}
|
|
854
997
|
console.log("Component creation detected:", relPath);
|
|
855
|
-
const createdStory =
|
|
998
|
+
const createdStory = ensureStoryFor("react", abs);
|
|
856
999
|
if (createdStory) {
|
|
857
1000
|
kick("create:story", createdStory);
|
|
858
1001
|
}
|
|
@@ -867,7 +1010,7 @@ function startWatcher() {
|
|
|
867
1010
|
if (isEmptyOrWhitespace(abs)) {
|
|
868
1011
|
scaffoldSvelteComponent(abs);
|
|
869
1012
|
}
|
|
870
|
-
const createdStory =
|
|
1013
|
+
const createdStory = ensureStoryFor("svelte", abs);
|
|
871
1014
|
if (createdStory) {
|
|
872
1015
|
kick("create:story", createdStory);
|
|
873
1016
|
}
|
|
@@ -877,7 +1020,7 @@ function startWatcher() {
|
|
|
877
1020
|
scaffoldVueComponent(abs);
|
|
878
1021
|
}
|
|
879
1022
|
console.log("Vue component creation detected:", relPath);
|
|
880
|
-
const createdStory =
|
|
1023
|
+
const createdStory = ensureStoryFor("vue", abs);
|
|
881
1024
|
if (createdStory) {
|
|
882
1025
|
kick("create:story", createdStory);
|
|
883
1026
|
}
|
|
@@ -887,7 +1030,7 @@ function startWatcher() {
|
|
|
887
1030
|
scaffoldAngularComponent(abs, templateStyle);
|
|
888
1031
|
}
|
|
889
1032
|
console.log("Angular component creation detected:", relPath);
|
|
890
|
-
const createdStory =
|
|
1033
|
+
const createdStory = ensureStoryFor("angular", abs);
|
|
891
1034
|
if (createdStory) {
|
|
892
1035
|
kick("create:story", createdStory);
|
|
893
1036
|
}
|
|
@@ -955,6 +1098,15 @@ async function startStorybook() {
|
|
|
955
1098
|
SRC_DIR = "src";
|
|
956
1099
|
}
|
|
957
1100
|
}
|
|
1101
|
+
const configuredStorybookFileExtension = cfg.storybookFileExtension;
|
|
1102
|
+
if (configuredStorybookFileExtension === undefined) {
|
|
1103
|
+
STORYBOOK_FILE_EXTENSION = "stories";
|
|
1104
|
+
} else if (configuredStorybookFileExtension === "story" || configuredStorybookFileExtension === "stories") {
|
|
1105
|
+
STORYBOOK_FILE_EXTENSION = configuredStorybookFileExtension;
|
|
1106
|
+
} else {
|
|
1107
|
+
error(`storybookFileExtension "${configuredStorybookFileExtension}" is invalid — must be 'story' or 'stories'. Falling back to 'stories'.`);
|
|
1108
|
+
STORYBOOK_FILE_EXTENSION = "stories";
|
|
1109
|
+
}
|
|
958
1110
|
banner("sb-deps");
|
|
959
1111
|
info(`outDir: ${rel(outDir)}`);
|
|
960
1112
|
info(configPath ? `config: ${rel(configPath)}` : "config: (none)");
|