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.
Files changed (34) hide show
  1. package/README.md +31 -0
  2. package/dist/cli/sb-deps.cjs +204 -52
  3. package/dist/cli/sb-deps.cjs.map +1 -1
  4. package/dist/cli/sb-deps.mjs +205 -53
  5. package/dist/cli/sb-deps.mjs.map +1 -1
  6. package/dist/cli/setup/index.cjs +41 -3
  7. package/dist/cli/setup/index.cjs.map +1 -1
  8. package/dist/cli/setup/index.mjs +43 -5
  9. package/dist/cli/setup/index.mjs.map +1 -1
  10. package/dist/cli/setup/patchers/main.cjs +66 -0
  11. package/dist/cli/setup/patchers/main.cjs.map +1 -1
  12. package/dist/cli/setup/patchers/main.d.cts +23 -1
  13. package/dist/cli/setup/patchers/main.d.mts +23 -1
  14. package/dist/cli/setup/patchers/main.mjs +66 -1
  15. package/dist/cli/setup/patchers/main.mjs.map +1 -1
  16. package/dist/cli/setup/patchers/preview.cjs +1 -1
  17. package/dist/cli/setup/patchers/preview.cjs.map +1 -1
  18. package/dist/cli/setup/patchers/preview.mjs +1 -1
  19. package/dist/cli/setup/patchers/preview.mjs.map +1 -1
  20. package/dist/cli/setup/patchers/sbDepsConfig.cjs +31 -12
  21. package/dist/cli/setup/patchers/sbDepsConfig.cjs.map +1 -1
  22. package/dist/cli/setup/patchers/sbDepsConfig.mjs +31 -12
  23. package/dist/cli/setup/patchers/sbDepsConfig.mjs.map +1 -1
  24. package/dist/config.cjs.map +1 -1
  25. package/dist/config.d.cts +8 -0
  26. package/dist/config.d.mts +8 -0
  27. package/dist/config.mjs.map +1 -1
  28. package/dist/hooks/useDynamicStory.cjs +1 -1
  29. package/dist/hooks/useDynamicStory.cjs.map +1 -1
  30. package/dist/hooks/useDynamicStory.mjs +1 -1
  31. package/dist/hooks/useDynamicStory.mjs.map +1 -1
  32. package/dist/types.d.cts +1 -1
  33. package/dist/types.d.mts +1 -1
  34. 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.
@@ -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 = /\.stories?\.\w+$/i;
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}.stories.tsx`);
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}.stories.svelte`);
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}.stories.ts`);
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 → \${rel(absCompPath)}`);
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 './${componentName}'
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
- const storyPath = storyPathForComponent(absCompPath);
368
- (0, node_fs.writeFileSync)(storyPath, storyTpl, "utf8");
369
- info(`scaffolded story → \${rel(storyPath)}`);
370
- return storyPath;
378
+ (0, node_fs.writeFileSync)(targetStoryPath, storyTpl, "utf8");
379
+ info(`scaffolded story → ${rel(targetStoryPath)}`);
380
+ return targetStoryPath;
371
381
  }
372
- function ensureStoryForComponent(absCompPath) {
373
- const sPath = storyPathForComponent(absCompPath);
374
- if ((0, node_fs.existsSync)(sPath)) return null;
375
- return scaffoldStoryForComponent(absCompPath);
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 './${componentName}.svelte'
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
- const storyPath = storyPathForSvelteComponent(absCompPath);
470
- (0, node_fs.writeFileSync)(storyPath, storyTpl, "utf8");
471
- info(`scaffolded svelte story → ${rel(storyPath)}`);
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 './${componentName}.vue'
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
- const storyPath = storyPathForVueComponent(absCompPath);
555
- (0, node_fs.writeFileSync)(storyPath, storyTpl, "utf8");
556
- info(`scaffolded vue story → ${rel(storyPath)}`);
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}.stories.ts`);
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
- const storyPath = storyPathForAngularComponent(absCompPath);
756
- (0, node_fs.writeFileSync)(storyPath, storyTpl, "utf8");
757
- info(`scaffolded angular story → ${rel(storyPath)}`);
758
- return storyPath;
770
+ (0, node_fs.writeFileSync)(targetStoryPath, storyTpl, "utf8");
771
+ info(`scaffolded angular story → ${rel(targetStoryPath)}`);
772
+ return targetStoryPath;
759
773
  }
760
- function ensureStoryForAngularComponent(absCompPath) {
761
- const sPath = storyPathForAngularComponent(absCompPath);
762
- if ((0, node_fs.existsSync)(sPath)) return null;
763
- return scaffoldStoryForAngularComponent(absCompPath);
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 = ensureStoryForAngularComponent(tsPath);
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 = ensureStoryForComponent(abs);
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 = ensureStoryForSvelteComponent(abs);
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 = ensureStoryForVueComponent(abs);
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 = ensureStoryForAngularComponent(abs);
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)");