storybook-addon-dependency-previews 0.9.0 → 0.10.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 (55) hide show
  1. package/README.md +56 -11
  2. package/dist/cli/sb-deps.cjs +90 -58
  3. package/dist/cli/sb-deps.cjs.map +1 -1
  4. package/dist/cli/sb-deps.mjs +89 -57
  5. package/dist/cli/sb-deps.mjs.map +1 -1
  6. package/dist/cli/setup/detect.cjs +100 -2
  7. package/dist/cli/setup/detect.cjs.map +1 -1
  8. package/dist/cli/setup/detect.d.cts +62 -6
  9. package/dist/cli/setup/detect.d.mts +62 -6
  10. package/dist/cli/setup/detect.mjs +100 -4
  11. package/dist/cli/setup/detect.mjs.map +1 -1
  12. package/dist/cli/setup/index.cjs +61 -47
  13. package/dist/cli/setup/index.cjs.map +1 -1
  14. package/dist/cli/setup/index.mjs +62 -48
  15. package/dist/cli/setup/index.mjs.map +1 -1
  16. package/dist/cli/setup/install.cjs +23 -5
  17. package/dist/cli/setup/install.cjs.map +1 -1
  18. package/dist/cli/setup/install.d.cts +6 -0
  19. package/dist/cli/setup/install.d.mts +6 -0
  20. package/dist/cli/setup/install.mjs +23 -5
  21. package/dist/cli/setup/install.mjs.map +1 -1
  22. package/dist/cli/setup/patchers/preview.cjs +1446 -158
  23. package/dist/cli/setup/patchers/preview.cjs.map +1 -1
  24. package/dist/cli/setup/patchers/preview.d.cts +13 -2
  25. package/dist/cli/setup/patchers/preview.d.mts +13 -2
  26. package/dist/cli/setup/patchers/preview.mjs +1447 -159
  27. package/dist/cli/setup/patchers/preview.mjs.map +1 -1
  28. package/dist/cli/setup/patchers/sbDepsConfig.cjs +12 -11
  29. package/dist/cli/setup/patchers/sbDepsConfig.cjs.map +1 -1
  30. package/dist/cli/setup/patchers/sbDepsConfig.mjs +12 -11
  31. package/dist/cli/setup/patchers/sbDepsConfig.mjs.map +1 -1
  32. package/dist/cli/setup/util.cjs +478 -174
  33. package/dist/cli/setup/util.cjs.map +1 -1
  34. package/dist/cli/setup/util.d.cts +130 -8
  35. package/dist/cli/setup/util.d.mts +130 -8
  36. package/dist/cli/setup/util.mjs +471 -175
  37. package/dist/cli/setup/util.mjs.map +1 -1
  38. package/dist/config.cjs.map +1 -1
  39. package/dist/config.d.cts +47 -28
  40. package/dist/config.d.mts +47 -28
  41. package/dist/config.mjs.map +1 -1
  42. package/dist/index.cjs +43 -2
  43. package/dist/index.cjs.map +1 -0
  44. package/dist/index.d.cts +33 -2
  45. package/dist/index.d.mts +33 -2
  46. package/dist/index.mjs +38 -2
  47. package/dist/index.mjs.map +1 -0
  48. package/dist/panels/DefaultAutoDocsLayout.d.cts +1 -1
  49. package/dist/panels/DefaultAutoDocsLayout.d.mts +1 -1
  50. package/dist/preview.d.mts +1 -1
  51. package/dist/src/config.d.cts +274 -0
  52. package/dist/src/config.d.mts +274 -0
  53. package/dist/types.d.cts +21 -3
  54. package/dist/types.d.mts +21 -3
  55. package/package.json +6 -6
package/README.md CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
  ## What is this?
6
6
 
7
- > **This plugin is built for Storybook 10**
7
+ > **This plugin is built for Storybook 10 and 11**
8
8
 
9
9
  A plugin for [Storybook](https://storybook.js.org/) that shows the full dependency tree in both directions (built with and used by) the components in your application.
10
10
 
11
- Currently works with **React**, **Svelte**, **Vue 3**, **Solid**, **Angular**, and **Next.js**. The automated `sb-deps setup` wizard handles Vite-based projects (React, Svelte, Vue 3, Solid) end-to-end. Webpack-based projects (Angular, Next.js) need a one-time manual setup — see the [manual-setup-webpack guide](https://github.com/Dan503/storybook-addon-dependency-previews/blob/main/storybook-addon-package/docs/manual-setup-webpack.md) below.
11
+ Currently works with **React**, **Preact**, **Svelte**, **Vue 3**, **Solid**, **Angular**, and **Next.js**. The automated `sb-deps setup` wizard handles Vite-based projects (React, Preact, Svelte, Vue 3, Solid) end-to-end. Webpack-based projects (Angular, Next.js) need a one-time manual setup — see the [manual-setup-webpack guide](https://github.com/Dan503/storybook-addon-dependency-previews/blob/main/storybook-addon-package/docs/manual-setup-webpack.md) below.
12
12
 
13
13
  This is what you will see in Storybook after Dependency Previews have been installed and configured:
14
14
 
@@ -26,6 +26,12 @@ The below image demonstrates what you will see when you open up some of the depe
26
26
  - [React rendered example website](https://dependency-previews-demo-site-react.netlify.app/)
27
27
  - [React demo source code](https://github.com/Dan503/storybook-addon-dependency-previews/tree/main/example-site/react)
28
28
 
29
+ #### Preact demos
30
+
31
+ - [Preact Storybook demo site](https://dependency-previews-storybook-preact.netlify.app/?path=/docs/04-templates-home-template--docs)
32
+ - [Preact rendered example website](https://dependency-previews-demo-site-preact.netlify.app/)
33
+ - [Preact demo source code](https://github.com/Dan503/storybook-addon-dependency-previews/tree/main/example-site/preact)
34
+
29
35
  #### Svelte demos
30
36
 
31
37
  - [Svelte Storybook demo site](https://dependency-previews-storybook-svelte.netlify.app/?path=/docs/04-templates-home-template--docs)
@@ -56,7 +62,7 @@ Vue version built for Vue 3.
56
62
 
57
63
  ## Installation guide
58
64
 
59
- ### Quick start (React, Svelte, Vue 3, and Solid)
65
+ ### Quick start (React, Preact, Svelte, Vue 3, and Solid)
60
66
 
61
67
  After running `npx storybook@latest init` in your project, run the setup wizard:
62
68
 
@@ -98,11 +104,44 @@ When it finishes, run `npm run sb` (or your package manager's equivalent) to sta
98
104
 
99
105
  ### Manual setup
100
106
 
101
- The wizard supports React (`@storybook/react-vite`), Svelte (`@storybook/sveltekit`, `@storybook/svelte-vite`), Vue 3 (`@storybook/vue3-vite`), and Solid (`storybook-solidjs-vite`) — all Vite-based. **Angular (`@storybook/angular`) and Next.js (`@storybook/nextjs`) projects are both webpack-based and require manual setup** — the wizard's preview-patcher relies on Vite's `import.meta.glob`, which webpack doesn't expose. Follow the matching guide below:
107
+ The wizard supports React (`@storybook/react-vite`), Preact (`@storybook/preact-vite`), Svelte (`@storybook/sveltekit`, `@storybook/svelte-vite`), Vue 3 (`@storybook/vue3-vite`), and Solid (`storybook-solidjs-vite`) — all Vite-based. **Angular (`@storybook/angular`) and Next.js (`@storybook/nextjs`) projects are both webpack-based and require manual setup** — the wizard's preview-patcher relies on Vite's `import.meta.glob`, which webpack doesn't expose. Follow the matching guide below:
102
108
 
103
- - [Manual setup — Vite (React, Svelte, Vue 3, Solid)](https://github.com/Dan503/storybook-addon-dependency-previews/blob/main/storybook-addon-package/docs/manual-setup-vite.md)
109
+ - [Manual setup — Vite (React, Preact, Svelte, Vue 3, Solid)](https://github.com/Dan503/storybook-addon-dependency-previews/blob/main/storybook-addon-package/docs/manual-setup-vite.md)
104
110
  - [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)
105
111
 
112
+ ### Storybook 11 / CSF Next preview configs
113
+
114
+ From Storybook 11 the default `.storybook/preview.ts` style is CSF Next — a `definePreview({ ... })` call with an `addons` list. Register the addon there by calling the `dependencyPreviews()` function the package exports, alongside `@storybook/addon-docs`:
115
+
116
+ ```ts
117
+ /// <reference types="vite/client" />
118
+
119
+ import { definePreview } from '@storybook/react-vite'
120
+ import addonDocs from '@storybook/addon-docs'
121
+ import { dependencyPreviews } from 'storybook-addon-dependency-previews'
122
+
123
+ import dependenciesJson from './dependency-previews.json'
124
+
125
+ export default definePreview({
126
+ addons: [addonDocs(), dependencyPreviews()],
127
+ parameters: {
128
+ // The same settings block as the hand-spread form — the manual setup
129
+ // guides explain each value.
130
+ dependencyPreviews: {
131
+ dependenciesJson,
132
+ storyModules: import.meta.glob(
133
+ '/src/**/*.{story,stories}.{tsx,ts,jsx,js,svelte}',
134
+ { eager: false },
135
+ ),
136
+ sourceRootUrl: 'https://github.com/your-org/your-repo/blob/main',
137
+ projectRootPath: new URL('..', import.meta.url).pathname,
138
+ },
139
+ },
140
+ })
141
+ ```
142
+
143
+ The setup wizard recognises both preview styles: an existing `definePreview({ ... })` file gets `addonDocs()` and `dependencyPreviews()` added to its `addons` list and the settings block added to its `parameters`, and a classic `const preview = { ... }` file gets the hand-spread form (spreading `defaultPreviewParameters` and `dependencyPreviewDecorators`), which remains supported. When there is no preview file yet, the wizard writes the `definePreview` style on Storybook 11 for frameworks whose package exports `definePreview` (React, Vue 3 and Solid), and the hand-spread style everywhere else — it works on both majors. Both manual setup guides show the full CSF Next config for their frameworks.
144
+
106
145
  ## Auto-scaffolding new components and stories
107
146
 
108
147
  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:
@@ -110,9 +149,9 @@ While `sb-deps` is watching (`npm run sb`), creating an **empty** source file fi
110
149
  - **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.
111
150
  - **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.
112
151
 
113
- 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, Solid, Vue, or Angular from your project's framework (Svelte stories use a `.svelte` file, so `.ts` isn't scaffolded for Svelte).
152
+ 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, Preact, Solid, Vue, or Angular from your project's framework (Svelte stories use a `.svelte` file, so `.ts` isn't scaffolded for Svelte).
114
153
 
115
- React and Solid both author components in `.tsx`, so the extension alone can't tell them apart. `sb-deps` works it out from your project, so a Solid project gets Solid templates (`solid-js`, `storybook-solidjs-vite`) without being told; anything it does not read as Solid gets React ones. Set `tsxFramework: 'solid'` in your `sb-deps` config to say so outright — worth doing where your project's framework isn't obvious from its files.
154
+ React, Preact and Solid all author components in `.tsx`, so the extension alone can't tell them apart. `sb-deps` works it out from your project, so a Solid project gets Solid templates (`solid-js`, `storybook-solidjs-vite`) and a Preact project gets Preact ones (`preact/hooks`, `@storybook/preact-vite`) without being told; anything it reads as neither gets React templates. Set `tsxFramework` in your `sb-deps` config to say so outright — worth doing where your project's framework isn't obvious from its files.
116
155
 
117
156
  ### What the scaffolded components assume
118
157
 
@@ -122,7 +161,7 @@ Each generated component is written the way its framework currently recommends,
122
161
  - **Svelte 5** — `$props()`, `$state()` and the `Snippet` type.
123
162
  - **Angular 17.1+** — `input()` signals.
124
163
 
125
- React and Solid add no version floor beyond what the addon itself needs. In a **Next.js** project the React component is written with a `'use client'` line at the top, because it holds state and the App Router renders on the server; you will not see that line in any other React project.
164
+ React, Preact and Solid add no version floor beyond what the addon itself needs. In a **Next.js** project the React component is written with a `'use client'` line at the top, because it holds state and the App Router renders on the server; you will not see that line in any other React project.
126
165
 
127
166
  None of this affects the dependency graph or anything else `sb-deps` does — it only describes the starter code it writes, so on an older version replace the generated body with whatever your project uses. Or replace the template outright via [`scaffold`](#scaffold), which is what that option is for.
128
167
 
@@ -250,9 +289,9 @@ export default defineSbDepsConfig({
250
289
 
251
290
  ### `tsxFramework`
252
291
 
253
- Which flavor to scaffold for `.tsx` component and story files — `'react'` or `'solid'`. React and Solid both author components in `.tsx`, so the extension alone can't tell them apart; set this to `'solid'` in a Solid project and scaffolded `.tsx` files get Solid templates (`solid-js` `createSignal`/`mergeProps`, `storybook-solidjs-vite` story imports) instead of React. The setup wizard sets it for you when it detects a Solid project. Per-template overrides for Solid go under [`scaffold.solid`](#scaffold).
292
+ Which flavor to scaffold for `.tsx` component and story files — `'react'`, `'solid'` or `'preact'`. All three author components in `.tsx`, so the extension alone can't tell them apart; set this to `'solid'` in a Solid project and scaffolded `.tsx` files get Solid templates (`solid-js` `createSignal`/`mergeProps`, `storybook-solidjs-vite` story imports), or to `'preact'` in a Preact project for Preact ones (`preact/hooks` `useState`, `@storybook/preact-vite` story imports), instead of React. The setup wizard sets it for you when it detects a Solid or Preact project. Per-template overrides go under the matching [`scaffold.solid` / `scaffold.preact`](#scaffold) key.
254
293
 
255
- **Default:** `'solid'` when `sb-deps` detects a Solid project, `'react'` otherwise — so a Solid project gets Solid templates without the key being set. Setting the key is worth it where the framework isn't obvious from your project's files.
294
+ **Default:** `'solid'` when `sb-deps` detects a Solid project, `'preact'` when it detects a Preact one, `'react'` otherwise — so either gets its own templates without the key being set. Setting the key is worth it where the framework isn't obvious from your project's files.
256
295
 
257
296
  ```js
258
297
  // sb-deps.config.mjs
@@ -303,7 +342,7 @@ An entry that isn't a non-empty string makes the whole option invalid — the CL
303
342
 
304
343
  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.
305
344
 
306
- For `.tsx` files the override key follows [`tsxFramework`](#tsxframework): a React project reads `scaffold.react`, a Solid project reads `scaffold.solid` — overrides placed under the wrong key are silently ignored.
345
+ For `.tsx` files the override key follows [`tsxFramework`](#tsxframework): a React project reads `scaffold.react`, a Solid project reads `scaffold.solid`, a Preact project reads `scaffold.preact` — overrides placed under the wrong key are silently ignored.
307
346
 
308
347
  ```js
309
348
  // sb-deps.config.mjs
@@ -349,6 +388,12 @@ export function ${componentName}({}: ${propsName}) {
349
388
  /** Customize the generated .stories.tsx file */
350
389
  story: ({ componentName, propsName, title, tags, base }) => '...',
351
390
  },
391
+ preact: {
392
+ /** Customize the generated .tsx component file */
393
+ component: ({ componentName, propsName }) => '...',
394
+ /** Customize the generated .stories.tsx file */
395
+ story: ({ componentName, propsName, title, tags, base }) => '...',
396
+ },
352
397
  angular: {
353
398
  /** Customize the generated .component.ts file */
354
399
  component: ({
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
3
3
  const require_fileNames = require('./scripts/fileNames.cjs');
4
+ const require_cli_setup_util = require('./setup/util.cjs');
4
5
  const require_cli_setup_detect = require('./setup/detect.cjs');
5
6
  const require_cli_setup_index = require('./setup/index.cjs');
6
7
  let node_fs = require("node:fs");
@@ -100,10 +101,21 @@ async function loadSbDepsConfig() {
100
101
  function validateConfigChoice(fieldName, value, allowed, fallback) {
101
102
  if (value === undefined) return fallback;
102
103
  if (allowed.includes(value)) return value;
103
- const choices = allowed.map((choice) => `'${choice}'`).join(" or ");
104
- error(`${fieldName} "${value}" is invalid — must be ${choices}. Falling back to '${fallback}'.`);
104
+ error(`${fieldName} "${value}" is invalid — must be ${getChoicesPhrase(allowed)}. Falling back to '${fallback}'.`);
105
105
  return fallback;
106
106
  }
107
+ /**
108
+ * The allowed values as they read in a sentence: `'a'`, `'a' or 'b'`, or
109
+ * `'a', 'b' or 'c'`. Joining every pair with "or" reads as a stutter past two
110
+ * values, which `tsxFramework` now has.
111
+ */
112
+ function getChoicesPhrase(choices) {
113
+ const quotedChoices = choices.map((choice) => `'${choice}'`);
114
+ const lastChoice = quotedChoices.at(-1);
115
+ if (quotedChoices.length < 2 || !lastChoice) return quotedChoices.join("");
116
+ const earlierChoices = quotedChoices.slice(0, -1).join(", ");
117
+ return `${earlierChoices} or ${lastChoice}`;
118
+ }
107
119
  let ANGULAR_SELECTOR_PREFIX = "app-";
108
120
  let SCAFFOLD_CONFIG = {};
109
121
  let SRC_DIR = "src";
@@ -117,23 +129,25 @@ function getProjectFramework() {
117
129
  }
118
130
  const IS_WIN = process.platform === "win32";
119
131
  /**
120
- * Locate the `dependency-cruiser` CLI binary in the user's `node_modules/.bin`.
121
- * Returns the absolute path with the right extension for the platform (`.cmd`
122
- * shim on Windows, bare name elsewhere). Falls back to `null` if it can't be
123
- * found — caller decides what to do.
132
+ * Locate the JavaScript entry file of the `dependency-cruiser` CLI the
133
+ * project installed (its `bin.depcruise`), found by walking `node_modules`
134
+ * upwards from the project root so a hoisted workspace install is covered
135
+ * too. Falls back to `null` if it can't be found — caller decides what to do.
124
136
  *
125
- * Going through the resolved binary lets us call `execFileSync` directly
126
- * (with `shell: false`) and pass each flag as its own array element — no
127
- * shell quoting, no `cmd.exe` metacharacter mangling (`^` is a `cmd.exe`
128
- * escape character, which would silently strip the `^` anchor from our
129
- * `--include-only` regex if we went through a shell). It also avoids spawning
130
- * `npx`, which on Windows is a `.cmd` shim that requires `shell: true` to
131
- * launch — which would reintroduce the very quoting problem we're trying to
132
- * avoid.
137
+ * Running that file under the current `node` directly, rather than the
138
+ * `node_modules/.bin/depcruise` shim, means the exact installed
139
+ * `dependency-cruiser` runs with each flag as its own array element and no
140
+ * shell on any platform. On Windows the shim is a `.cmd` file that needs
141
+ * `shell: true`, and cmd.exe then mangles arguments (`^` is its escape
142
+ * character, spaces split a path, `%NAME%` is expanded even inside quotes);
143
+ * skipping the shim removes all of that.
133
144
  */
134
- function resolveDepCruiseBin() {
135
- const bin = (0, node_path.join)(projectRoot, "node_modules", ".bin", IS_WIN ? "depcruise.cmd" : "depcruise");
136
- return (0, node_fs.existsSync)(bin) ? bin : null;
145
+ function resolveDepCruiseEntry() {
146
+ const found = require_cli_setup_util.findInstalledPackage(projectRoot, "dependency-cruiser");
147
+ const bin = found?.pkg.bin;
148
+ const relativeEntry = bin && typeof bin === "object" ? bin.depcruise : undefined;
149
+ if (!found || typeof relativeEntry !== "string") return null;
150
+ return (0, node_path.join)(found.dir, relativeEntry);
137
151
  }
138
152
  function runDepCruiseOnce() {
139
153
  const escapedSrcDir = escapeForFolderRegex(SRC_DIR);
@@ -145,12 +159,12 @@ function runDepCruiseOnce() {
145
159
  args.push("--no-config");
146
160
  }
147
161
  args.push("--include-only", includeOnly, "--output-type", "json");
148
- const depcruiseBin = resolveDepCruiseBin();
149
- if (!depcruiseBin) {
150
- throw new Error("Could not locate `dependency-cruiser` in node_modules/.bin. Run the setup wizard (`sb-deps setup`) or install `dependency-cruiser` as a dev dependency.");
162
+ const depcruiseEntry = resolveDepCruiseEntry();
163
+ if (!depcruiseEntry) {
164
+ throw new Error("Could not locate `dependency-cruiser` in node_modules. Run the setup wizard (`sb-deps setup`) or install `dependency-cruiser` as a dev dependency.");
151
165
  }
152
166
  const start = Date.now();
153
- const stdout = (0, node_child_process.execFileSync)(depcruiseBin, IS_WIN ? args.map(escapeForCmdExe) : args, {
167
+ const stdout = (0, node_child_process.execFileSync)(process.execPath, [depcruiseEntry, ...args], {
154
168
  cwd: projectRoot,
155
169
  stdio: [
156
170
  "ignore",
@@ -158,7 +172,6 @@ function runDepCruiseOnce() {
158
172
  "inherit"
159
173
  ],
160
174
  encoding: "utf8",
161
- shell: IS_WIN,
162
175
  env: {
163
176
  ...process.env,
164
177
  SB_DEPS_SRC_DIR: SRC_DIR
@@ -194,7 +207,7 @@ function postprocessOnce() {
194
207
  * exact, so a flag would loosen the half that must not loosen.
195
208
  */
196
209
  function escapeForFolderRegex(folderName) {
197
- return IS_CASE_INSENSITIVE_PATH_FS ? escapeForRegexIgnoringCase(folderName) : escapeForRegex(folderName);
210
+ return IS_CASE_INSENSITIVE_PATH_FS ? escapeForRegexIgnoringCase(folderName) : require_cli_setup_util.escapeForRegex(folderName);
198
211
  }
199
212
  /**
200
213
  * Like `escapeForRegex`, but every letter becomes a pair matching both of its
@@ -204,24 +217,9 @@ function escapeForFolderRegex(folderName) {
204
217
  function escapeForRegexIgnoringCase(text) {
205
218
  return text.split("").map((char) => {
206
219
  const isLetter = /[a-z]/i.test(char);
207
- return isLetter ? `[${char.toLowerCase()}${char.toUpperCase()}]` : escapeForRegex(char);
220
+ return isLetter ? `[${char.toLowerCase()}${char.toUpperCase()}]` : require_cli_setup_util.escapeForRegex(char);
208
221
  }).join("");
209
222
  }
210
- /** Backslash-escape every character that has a special meaning in a regex, so the text only matches itself. */
211
- function escapeForRegex(text) {
212
- return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
213
- }
214
- /**
215
- * Escape an argument so that it survives `cmd.exe` parsing when `execFileSync`
216
- * is invoked with `shell: true` on Windows. `^` is the cmd.exe escape character,
217
- * even inside double quotes — doubling it makes cmd.exe pass through a literal
218
- * `^`. Other shell metacharacters (`&`, `|`, `<`, `>`, `(`, `)`, `%`, `!`) are
219
- * already wrapped in double quotes by Node's internal arg-quoter when
220
- * `shell: true`, so we only need to handle `^` ourselves.
221
- */
222
- function escapeForCmdExe(arg) {
223
- return arg.replace(/\^/g, "^^");
224
- }
225
223
  function buildOnce() {
226
224
  try {
227
225
  runDepCruiseOnce();
@@ -698,7 +696,7 @@ function makeTitleFromComponent(absCompPath, base) {
698
696
  }
699
697
  /**
700
698
  * The default `.tsx` component template, in the flavor `TSX_FRAMEWORK` selects.
701
- * React and Solid share the `.tsx` extension but need different code.
699
+ * React, Solid and Preact share the `.tsx` extension but need different code.
702
700
  */
703
701
  function tsxComponentTemplate(flavor, componentName, propsName) {
704
702
  if (flavor === "solid") {
@@ -730,12 +728,39 @@ export function ${componentName}(props: ${propsName}) {
730
728
  }
731
729
  `;
732
730
  }
731
+ if (flavor === "preact") {
732
+ return reactLikeComponentTemplate({
733
+ componentName,
734
+ propsName,
735
+ imports: `import type { ComponentChildren } from 'preact'\nimport { useState } from 'preact/hooks'\n`,
736
+ childrenType: "ComponentChildren",
737
+ classAttribute: "class"
738
+ });
739
+ }
733
740
  const clientDirective = getProjectFramework() === "nextjs-webpack" ? "'use client'\n\n" : "";
734
- return `${clientDirective}import { useState, type ReactNode } from 'react'
735
-
741
+ return reactLikeComponentTemplate({
742
+ componentName,
743
+ propsName,
744
+ imports: `${clientDirective}import { useState, type ReactNode } from 'react'\n`,
745
+ childrenType: "ReactNode",
746
+ classAttribute: "className"
747
+ });
748
+ }
749
+ /**
750
+ * The `.tsx` component template React and Preact both emit. The two frameworks
751
+ * write the same component — destructured props with defaults, a counter, a
752
+ * children slot — and differ only in the pieces named above, so the body is
753
+ * written once here rather than twice.
754
+ *
755
+ * Solid is not written from this. Its component reads its props through
756
+ * `mergeProps` instead of destructuring them, so it shares no body to speak of
757
+ * and keeps its own template.
758
+ */
759
+ function reactLikeComponentTemplate({ componentName, propsName, imports, childrenType, classAttribute }) {
760
+ return `${imports}
736
761
  export interface ${propsName} {
737
762
  text?: string
738
- children?: ReactNode
763
+ children?: ${childrenType}
739
764
  }
740
765
 
741
766
  export function ${componentName}({
@@ -745,7 +770,7 @@ export function ${componentName}({
745
770
  const [count, setCount] = useState(0)
746
771
 
747
772
  return (
748
- <div className="${componentName}">
773
+ <div ${classAttribute}="${componentName}">
749
774
  <p>{text}</p>
750
775
  <button type="button" onClick={() => setCount(count + 1)}>
751
776
  count: {count}
@@ -757,8 +782,8 @@ export function ${componentName}({
757
782
  `;
758
783
  }
759
784
  /**
760
- * The default `.tsx` story template. React and Solid stories are identical apart
761
- * from which package the Storybook types come from (see
785
+ * The default `.tsx` story template. React, Solid and Preact stories are
786
+ * identical apart from which package the Storybook types come from (see
762
787
  * `getTsxStoryTypesPackage`).
763
788
  *
764
789
  * The component is imported from `./${base}` (the actual filename), not
@@ -792,13 +817,14 @@ export const Primary: Story = {
792
817
  }
793
818
  /**
794
819
  * The package a scaffolded `.tsx` story imports its Storybook types from. Solid
795
- * has its own package. React's depends on how the project builds: a Next.js
796
- * project's Storybook types live in `@storybook/nextjs`, not in the Vite
820
+ * and Preact each have their own. React's depends on how the project builds: a
821
+ * Next.js project's Storybook types live in `@storybook/nextjs`, not in the Vite
797
822
  * package, so importing the Vite one there produces a story that doesn't
798
823
  * type-check.
799
824
  */
800
825
  function getTsxStoryTypesPackage(flavor) {
801
826
  if (flavor === "solid") return "storybook-solidjs-vite";
827
+ if (flavor === "preact") return "@storybook/preact-vite";
802
828
  if (getProjectFramework() === "nextjs-webpack") return "@storybook/nextjs";
803
829
  return "@storybook/react-vite";
804
830
  }
@@ -1382,7 +1408,8 @@ function getFrameworkFamily(framework) {
1382
1408
  switch (framework) {
1383
1409
  case "react-vite":
1384
1410
  case "nextjs-webpack":
1385
- case "solid-vite": return "react";
1411
+ case "solid-vite":
1412
+ case "preact-vite": return "react";
1386
1413
  case "vue3-vite": return "vue";
1387
1414
  case "sveltekit":
1388
1415
  case "svelte-vite": return "svelte";
@@ -1446,9 +1473,10 @@ function ensureStoryFor(framework, absCompPath) {
1446
1473
  }
1447
1474
  /**
1448
1475
  * Work out the component a created story file belongs to, and which framework's
1449
- * scaffolders to use. `.tsx` → React or Solid, `.svelte` → Svelte, `.ts` →
1450
- * React, Solid, Vue, or Angular (disambiguated in `resolveTsStoryComponent`).
1451
- * React and Solid share the `.tsx` route and are told apart by `tsxFramework`.
1476
+ * scaffolders to use. `.tsx` → React, Solid or Preact, `.svelte` → Svelte,
1477
+ * `.ts` → React, Solid, Preact, Vue, or Angular (disambiguated in
1478
+ * `resolveTsStoryComponent`). Every framework that writes `.tsx` shares that
1479
+ * route, and they are told apart by `tsxFramework`.
1452
1480
  * Any extension with no entry in `STORY_COMPONENT_RESOLVERS` returns `null` —
1453
1481
  * not something we scaffold.
1454
1482
  */
@@ -1541,11 +1569,11 @@ function getComponentForStoryByExtension(absStoryPath) {
1541
1569
  return resolveComponent(storyBase, absStoryPath);
1542
1570
  }
1543
1571
  /**
1544
- * A `.stories.ts` story can be React, Solid, Vue, or Angular — all of them use
1545
- * `.ts` story files (the React and Solid scaffolded templates are JSX-free, so
1546
- * they're valid as `.ts` even though both write stories as `.tsx` by
1547
- * convention). Prefer an existing sibling component to decide
1548
- * (`<base>.tsx` → React or Solid, told apart by `tsxFramework`;
1572
+ * A `.stories.ts` story can be React, Solid, Preact, Vue, or Angular — all of
1573
+ * them use `.ts` story files (the React, Solid and Preact scaffolded templates
1574
+ * are JSX-free, so they're valid as `.ts` even though all three write stories as
1575
+ * `.tsx` by convention). Prefer an existing sibling component to decide
1576
+ * (`<base>.tsx` → React, Solid or Preact, told apart by `tsxFramework`;
1549
1577
  * `<base>.vue` → Vue; `<base>.component.ts` → Angular); with none present,
1550
1578
  * fall back to the project's detected framework. Svelte is intentionally excluded: its story
1551
1579
  * template is `.svelte`-specific, so a `.ts` Svelte story can't be scaffolded
@@ -1920,7 +1948,11 @@ async function startStorybook() {
1920
1948
  }
1921
1949
  }
1922
1950
  STORYBOOK_FILE_EXTENSION = validateConfigChoice("storybookFileExtension", cfg.storybookFileExtension, ["story", "stories"], "stories");
1923
- TSX_FRAMEWORK = validateConfigChoice("tsxFramework", cfg.tsxFramework, ["react", "solid"], getProjectFramework() === "solid-vite" ? "solid" : "react");
1951
+ TSX_FRAMEWORK = validateConfigChoice("tsxFramework", cfg.tsxFramework, [
1952
+ "react",
1953
+ "solid",
1954
+ "preact"
1955
+ ], require_cli_setup_detect.tsxFrameworkFromFramework(getProjectFramework()));
1924
1956
  const configuredScaffoldIgnore = cfg.scaffoldIgnore;
1925
1957
  const isScaffoldIgnoreUsable = Array.isArray(configuredScaffoldIgnore) && configuredScaffoldIgnore.every((pattern) => typeof pattern === "string" && pattern.trim() !== "");
1926
1958
  if (configuredScaffoldIgnore === undefined) {