webanvil 0.0.10 → 0.0.11

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 CHANGED
@@ -56,17 +56,17 @@ wa typecheck # type-check the project
56
56
  What it includes
57
57
  ----------------
58
58
 
59
- | Project job | WebAnvil command | Tool |
60
- | -------------------------- | ------------------------------------------------------ | -------------------------------- |
61
- | Web builds and development | `wa build`, `wa dev`, `wa preview` | Vite |
62
- | Node builds and watch mode | `wa build`, `wa dev` | Rolldown |
63
- | Storybook development | `wa build --mode storybook`, `wa dev --mode storybook` | Storybook |
64
- | Tracked output cleanup | `wa clean` | WebAnvil |
65
- | Static checks | `wa check` | Oxfmt, Oxlint, TypeScript Native |
66
- | Tests | `wa test` | Vitest |
67
- | Linting | `wa lint` | Oxlint |
68
- | Formatting | `wa format` | Oxfmt |
69
- | Type checking | `wa typecheck` | TypeScript Native |
59
+ | Project job | WebAnvil command | Tool |
60
+ | -------------------------- | ---------------------------------- | -------------------------------- |
61
+ | Web builds and development | `wa build`, `wa dev`, `wa preview` | Vite |
62
+ | Node builds and watch mode | `wa build`, `wa dev` | Rolldown |
63
+ | Design-system Storybook | `wa build`, `wa dev`, `wa preview` | Storybook |
64
+ | Tracked output cleanup | `wa clean` | WebAnvil |
65
+ | Static checks | `wa check` | Oxfmt, Oxlint, TypeScript Native |
66
+ | Tests | `wa test` | Vitest |
67
+ | Linting | `wa lint` | Oxlint |
68
+ | Formatting | `wa format` | Oxfmt |
69
+ | Type checking | `wa typecheck` | TypeScript Native |
70
70
 
71
71
  Getting started
72
72
  ---------------
@@ -159,46 +159,41 @@ WebAnvil includes Storybook, the supported Vite framework adapters, Vitest's
159
159
  browser support, and Chromium. Add only a Storybook configuration and your
160
160
  project's normal framework dependencies.
161
161
 
162
- For example, a Svelte project can use this `.storybook/main.ts`:
163
-
164
- ```ts
165
- import { framework, type StorybookConfig } from "webanvil/storybook/svelte"
166
-
167
- export default {
168
- framework,
169
- stories: ["../src/**/*.stories.@(js|ts|svelte)"]
170
- } satisfies StorybookConfig
171
- ```
172
-
173
- Use `webanvil/storybook/react`, `webanvil/storybook/vue`, or
174
- `webanvil/storybook/web-components` for those frameworks. The WebAnvil wrapper
175
- uses the framework plugins already declared in `webanvil.config.*`.
176
-
177
- Set Storybook as the project's build mode when it is the primary development
178
- target:
162
+ For a Node design-system project, configure Storybook beside the package build:
179
163
 
180
164
  ```ts
165
+ import { svelte } from "@sveltejs/vite-plugin-svelte"
181
166
  import { defineConfig } from "webanvil"
182
167
 
183
168
  export default defineConfig({
184
- build: { mode: "storybook" }
169
+ build: { mode: "node", entries: { ".": "src/index.ts" }, outDir: "dist" },
170
+ storybook: { framework: "svelte", port: 6006, outDir: "storybook-static" },
171
+ vite: { plugins: [svelte()] }
185
172
  })
186
173
  ```
187
174
 
188
- Or select it for one command:
175
+ Then keep `.storybook/main.ts` focused on stories and addons:
189
176
 
190
- ```sh
191
- wa dev --mode storybook
192
- wa build --mode storybook
177
+ ```ts
178
+ import type { StorybookConfig } from "webanvil/storybook/svelte"
179
+
180
+ export default {
181
+ stories: ["../src/**/*.stories.@(js|ts|svelte)"],
182
+ addons: ["@storybook/addon-a11y"]
183
+ } satisfies StorybookConfig
193
184
  ```
194
185
 
195
- The build writes to `storybook-static` unless `storybook.outDir` in
196
- `webanvil.config.*` or `--out-dir` selects another directory. `wa test`
197
- automatically runs Storybook stories, including `play` functions, when it finds
198
- `.storybook/main.*`. Set `storybook.test: false` to exclude them. Chromium is
199
- downloaded by `@playwright/browser-chromium` when your package manager runs
200
- install scripts. If the project declares Vitest, Storybook tests require
201
- version `4.1.10` to match the bundled browser provider.
186
+ `wa dev` starts the package watcher, waits for its first successful build, then
187
+ starts Storybook. `wa build` creates the package output and static Storybook.
188
+ `wa preview` serves the static Storybook output. `wa clean` removes both sets of
189
+ tracked files. `--host` and `--port` on `wa dev` configure Storybook. The
190
+ package build still owns `--out-dir`.
191
+
192
+ Set `storybook.test: false` to exclude Storybook stories, including `play`
193
+ functions, from `wa test`. Chromium is downloaded by
194
+ `@playwright/browser-chromium` when your package manager runs install scripts.
195
+ If the project declares Vitest, Storybook tests require version `4.1.10` to
196
+ match the bundled browser provider.
202
197
 
203
198
  ### A Node project
204
199
 
@@ -534,16 +529,16 @@ That lets a project standardize on `wa` now and move settings into `webanvil.con
534
529
  Command reference
535
530
  -----------------
536
531
 
537
- | Command | Description | Options |
538
- | ------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
539
- | `wa build [entry]` | Builds with Vite in web mode, Rolldown in Node mode, or Storybook. | `--mode`, `--out-dir`, `--copy`, `--bundle`, `--no-bundle`, `--formats`, `--declaration`, `--sourcemap`, `--minify`, `--platform`, `--target` |
540
- | `wa clean` | Removes files emitted by prior WebAnvil builds. | No options |
541
- | `wa check` | Checks formatting, linting, and types, stopping on the first failure. | `--fix` |
542
- | `wa dev [entry]` | Starts Vite, a Node build watcher, or Storybook. | `--mode`, `--out-dir`, `--host`, `--port`, `--copy`, `--bundle`, `--no-bundle`, `--formats`, `--declaration`, `--sourcemap`, `--minify`, `--platform`, `--target` |
543
- | `wa preview` | Serves a Vite production build. | `--out-dir`, `--host`, `--port`, `--open` |
544
- | `wa test [filters...]` | Runs Vitest once, in watch mode, with coverage, or UI. | `--environment`, `--watch`, `--coverage`, `--ui`, `--ui-port` |
545
- | `wa lint [paths...]` | Runs Oxlint and treats warnings as failures. | `--fix` |
546
- | `wa format [paths...]` | Formats with Oxfmt. | `--check` |
547
- | `wa typecheck [paths...]` | Type-checks with TypeScript Native. | No options |
532
+ | Command | Description | Options |
533
+ | ------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
534
+ | `wa build [entry]` | Builds with Vite in web mode or Rolldown in Node mode. A configured Storybook builds with a Node project. | `--mode`, `--out-dir`, `--copy`, `--bundle`, `--no-bundle`, `--formats`, `--declaration`, `--sourcemap`, `--minify`, `--platform`, `--target` |
535
+ | `wa clean` | Removes files emitted by prior WebAnvil builds. | No options |
536
+ | `wa check` | Checks formatting, linting, and types, stopping on the first failure. | `--fix` |
537
+ | `wa dev [entry]` | Starts Vite or a Node build watcher. A configured Storybook starts with the Node watcher. | `--mode`, `--out-dir`, `--host`, `--port`, `--copy`, `--bundle`, `--no-bundle`, `--formats`, `--declaration`, `--sourcemap`, `--minify`, `--platform`, `--target` |
538
+ | `wa preview` | Serves a Vite production build or configured static Storybook output. | `--out-dir`, `--host`, `--port`, `--open` |
539
+ | `wa test [filters...]` | Runs Vitest once, in watch mode, with coverage, or UI. | `--environment`, `--watch`, `--coverage`, `--ui`, `--ui-port` |
540
+ | `wa lint [paths...]` | Runs Oxlint and treats warnings as failures. | `--fix` |
541
+ | `wa format [paths...]` | Formats with Oxfmt. | `--check` |
542
+ | `wa typecheck [paths...]` | Type-checks with TypeScript Native. | No options |
548
543
 
549
544
  Run `wa <command> --help` for the complete reference for a command.
@@ -1,9 +1,9 @@
1
- import { assertSyntaxTarget, hasOxcConfig, hasStorybookConfig, hasToolConfig, loadConfig, resolveEffectiveBuildConfig, resolveRolldownPlugins, resolveVitePlugins, syntaxTargetSchema, withConfig } from "./config.mjs";
1
+ import { assertSyntaxTarget, effectiveUserConfigSchema, hasOxcConfig, hasStorybookConfig, hasToolConfig, loadConfig, resolveEffectiveBuildConfig, resolveRolldownPlugins, resolveVitePlugins, syntaxTargetSchema, withConfig } from "./config.mjs";
2
2
  import { Module, createRequire, isBuiltin } from "node:module";
3
3
  import { defineArgument, defineCommand, defineOption } from "cmdore";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "pathe";
5
5
  import { glob } from "tinyglobby";
6
- import { access, copyFile, lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, rmdir, writeFile } from "node:fs/promises";
6
+ import { access, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, rmdir, symlink, writeFile } from "node:fs/promises";
7
7
  import { z } from "zod";
8
8
  import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "node:path";
9
9
  import { getTsconfig, readTsconfig } from "get-tsconfig";
@@ -1085,15 +1085,68 @@ const useToolExecutable = async (name, toolchain = new Toolchain(process.cwd()))
1085
1085
  return tool.executable;
1086
1086
  };
1087
1087
  const storybookVitestVersion = "4.1.10";
1088
+ const relativeModule = (from, to) => {
1089
+ const path = relative(from, to).replaceAll("\\", "/");
1090
+ return path.startsWith(".") ? path : `./${path}`;
1091
+ };
1092
+ const storybookMain = async (configDirectory) => {
1093
+ const files = await readdir(configDirectory).catch(() => []);
1094
+ const file = [
1095
+ "js",
1096
+ "mjs",
1097
+ "cjs",
1098
+ "ts",
1099
+ "mts",
1100
+ "cts"
1101
+ ].map((extension) => `main.${extension}`).find((name) => files.includes(name));
1102
+ if (file === void 0) throw new Error(`Could not find a Storybook main configuration in ${configDirectory}`);
1103
+ return resolve(configDirectory, file);
1104
+ };
1105
+ const frameworkDirectory = (framework) => fileURLToPath(new URL(`../storybook/${framework}/`, import.meta.url));
1106
+ const prepareStorybookConfig = async (config = {}) => {
1107
+ if (config.framework === void 0) return void 0;
1108
+ const sourceDirectory = resolve(process.cwd(), config.configDir ?? ".storybook");
1109
+ const main = await storybookMain(sourceDirectory);
1110
+ const directory = await mkdtemp(resolve(dirname(sourceDirectory), ".webanvil-storybook-"));
1111
+ try {
1112
+ for (const entry of await readdir(sourceDirectory, { withFileTypes: true })) {
1113
+ if (resolve(sourceDirectory, entry.name) === main) continue;
1114
+ await symlink(resolve(sourceDirectory, entry.name), resolve(directory, entry.name), entry.isDirectory() ? "junction" : "file");
1115
+ }
1116
+ await mkdir(directory, { recursive: true });
1117
+ await writeFile(resolve(directory, "main.ts"), `import config from ${JSON.stringify(relativeModule(directory, main))}\n\nexport default { ...config, framework: ${JSON.stringify(frameworkDirectory(config.framework))} }\n`);
1118
+ return {
1119
+ config: {
1120
+ ...config,
1121
+ configDir: directory
1122
+ },
1123
+ cleanup: () => rm(directory, {
1124
+ force: true,
1125
+ recursive: true
1126
+ })
1127
+ };
1128
+ } catch (error) {
1129
+ await rm(directory, {
1130
+ force: true,
1131
+ recursive: true
1132
+ });
1133
+ throw error;
1134
+ }
1135
+ };
1088
1136
  const storybookOutputDir = (config = {}, options = {}) => options.outDir ?? config.outDir ?? "storybook-static";
1089
1137
  const exitWithError = (action, exitCode) => {
1090
1138
  throw new Error(`storybook ${action} exited with code ${exitCode ?? "unknown"}`);
1091
1139
  };
1092
- const runStorybook = async (action, config = {}, options = {}, toolchain = new Toolchain(process.cwd())) => {
1140
+ const startStorybook = async (action, config = {}, options = {}, toolchain = new Toolchain(process.cwd())) => {
1093
1141
  const executable = await useToolExecutable("storybook", toolchain);
1094
- const configDir = options.configDir ?? config.configDir;
1095
- const outDir = storybookOutputDir(config, options);
1096
- const result = await execa(executable, [
1142
+ const prepared = await prepareStorybookConfig({
1143
+ ...config,
1144
+ configDir: options.configDir ?? config.configDir
1145
+ });
1146
+ const effective = prepared?.config ?? config;
1147
+ const configDir = effective.configDir;
1148
+ const outDir = storybookOutputDir(effective, options);
1149
+ const child = execa(executable, [
1097
1150
  action,
1098
1151
  ...configDir === void 0 ? [] : ["--config-dir", configDir],
1099
1152
  ...action === "dev" && options.host !== void 0 ? ["--host", options.host] : [],
@@ -1103,8 +1156,16 @@ const runStorybook = async (action, config = {}, options = {}, toolchain = new T
1103
1156
  reject: false,
1104
1157
  stdio: "inherit"
1105
1158
  });
1106
- if (result.exitCode !== 0) exitWithError(action, result.exitCode);
1159
+ return {
1160
+ completed: child.then((result) => {
1161
+ if (result.exitCode !== 0) exitWithError(action, result.exitCode);
1162
+ }, (error) => {
1163
+ throw error;
1164
+ }).finally(() => prepared?.cleanup()),
1165
+ stop: () => child.kill("SIGTERM")
1166
+ };
1107
1167
  };
1168
+ const runStorybook = async (action, config = {}, options = {}, toolchain = new Toolchain(process.cwd())) => (await startStorybook(action, config, options, toolchain)).completed;
1108
1169
  const createStorybookTestProject = async (config = {}, vitestVersion = storybookVitestVersion) => {
1109
1170
  if (vitestVersion !== storybookVitestVersion) throw new Error(`Storybook tests require Vitest ${storybookVitestVersion}; Webanvil bundles @vitest/browser-playwright ${storybookVitestVersion}`);
1110
1171
  const previousVitest = process.env.VITEST;
@@ -1193,13 +1254,9 @@ const minify = defineOption({
1193
1254
  });
1194
1255
  const mode = defineOption({
1195
1256
  name: "mode",
1196
- description: "Build mode: web uses Vite, node uses Rolldown, and storybook runs Storybook.",
1257
+ description: "Build mode: web uses Vite and node uses Rolldown.",
1197
1258
  arity: 1,
1198
- schema: z.enum([
1199
- "web",
1200
- "node",
1201
- "storybook"
1202
- ])
1259
+ schema: z.enum(["web", "node"])
1203
1260
  });
1204
1261
  const open = defineOption({
1205
1262
  name: "open",
@@ -1263,24 +1320,6 @@ const noBundle$1 = defineOption({
1263
1320
  description: "Emit the reachable Node graph with preserveModules, overriding configuration that enables bundling.",
1264
1321
  arity: 0
1265
1322
  });
1266
- const assertStorybookArguments$1 = (explicit) => {
1267
- const option = [
1268
- "bundle",
1269
- "copy",
1270
- "declaration",
1271
- "entry",
1272
- "formats",
1273
- "minify",
1274
- "no-bundle",
1275
- "platform",
1276
- "sourcemap",
1277
- "target"
1278
- ].find((name) => {
1279
- const value = explicit[name];
1280
- return name === "bundle" || name === "no-bundle" ? value === true : value !== void 0;
1281
- });
1282
- if (option !== void 0) throw new Error(`--${option} is not available in Storybook mode`);
1283
- };
1284
1323
  const outputFiles = (result, outDir) => {
1285
1324
  if ("on" in result) throw new Error("Web builds cannot use watch mode");
1286
1325
  return (Array.isArray(result) ? result : [result]).flatMap((output) => output.output.map((file) => resolve(outDir, file.fileName)));
@@ -1356,22 +1395,27 @@ build.publicOutputFiles = async ({ outDir, publicDir }) => publicDir ? (await gl
1356
1395
  dot: true
1357
1396
  })).map((file) => resolve(outDir, file)) : [];
1358
1397
  build.web = async (web) => [...outputFiles(await web.vite.build(web.config), web.outDir), ...await build.publicOutputFiles(web)];
1398
+ const outputContains = (directory, path) => {
1399
+ const location = relative(directory, path);
1400
+ return location === "" || location !== ".." && !location.startsWith("../");
1401
+ };
1402
+ const assertSeparateStorybookOutput = (outDir, storybook) => {
1403
+ const output = resolve(process.cwd(), outDir);
1404
+ const storybookOutput = resolve(process.cwd(), storybookOutputDir(storybook));
1405
+ if (outputContains(output, storybookOutput) || outputContains(storybookOutput, output)) throw new Error("storybook.outDir must not overlap build.outDir");
1406
+ };
1407
+ const buildStorybook = async (storybook, toolchain) => {
1408
+ const outputDirectory = storybookOutputDir(storybook);
1409
+ const existing = await removeOutputsIn(outputDirectory);
1410
+ await runStorybook("build", storybook, {}, toolchain);
1411
+ const output = await glob("**/*", {
1412
+ cwd: resolve(process.cwd(), outputDirectory),
1413
+ dot: true,
1414
+ onlyFiles: true
1415
+ });
1416
+ await writeBuildInfo([...existing.output, ...output.map((file) => resolve(outputDirectory, file))]);
1417
+ };
1359
1418
  const commandRun$1 = (toolchain) => withConfig((config) => config.build, ({ copy, declaration, formats, minify, mode, entry, "out-dir": outDir, platform, sourcemap, target }, buildConfig, resolvedConfig, explicit) => {
1360
- if (mode === "storybook") {
1361
- assertStorybookArguments$1(explicit);
1362
- const storybookOptions = { outDir: explicit["out-dir"] === void 0 ? void 0 : outDir };
1363
- const outputDirectory = storybookOutputDir(resolvedConfig.storybook, storybookOptions);
1364
- return (async () => {
1365
- const existing = await removeOutputsIn(outputDirectory);
1366
- await runStorybook("build", resolvedConfig.storybook, storybookOptions, toolchain);
1367
- const output = await glob("**/*", {
1368
- cwd: resolve(process.cwd(), outputDirectory),
1369
- dot: true,
1370
- onlyFiles: true
1371
- });
1372
- await writeBuildInfo([...existing.output, ...output.map((file) => resolve(outputDirectory, file))]);
1373
- })();
1374
- }
1375
1419
  if (explicit.bundle && explicit["no-bundle"]) throw new Error("--bundle and --no-bundle cannot be used together");
1376
1420
  const effective = resolveEffectiveBuildConfig(resolvedConfig, {
1377
1421
  bundle: explicit["no-bundle"] ? false : explicit.bundle ? true : buildConfig.bundle,
@@ -1389,13 +1433,17 @@ const commandRun$1 = (toolchain) => withConfig((config) => config.build, ({ copy
1389
1433
  }, explicit.entry !== void 0);
1390
1434
  const executableMode = effective.mode;
1391
1435
  if (executableMode !== "web" && executableMode !== "node") throw new Error("Expected a web or Node build mode");
1392
- return build(executableMode, effective.entry, effective.outDir, effective, resolvedConfig.plugins ?? [], resolvedConfig.vite, resolvedConfig.rolldown, toolchain, {
1393
- ...explicit.entry === void 0 ? {} : { entry },
1394
- ...explicit["out-dir"] === void 0 ? {} : { outDir },
1395
- ...explicit.minify === void 0 ? {} : { minify },
1396
- ...explicit.sourcemap === void 0 ? {} : { sourcemap },
1397
- ...explicit.target === void 0 ? {} : { target }
1398
- });
1436
+ return (async () => {
1437
+ if (resolvedConfig.storybook !== void 0) assertSeparateStorybookOutput(effective.outDir, resolvedConfig.storybook);
1438
+ await build(executableMode, effective.entry, effective.outDir, effective, resolvedConfig.plugins ?? [], resolvedConfig.vite, resolvedConfig.rolldown, toolchain, {
1439
+ ...explicit.entry === void 0 ? {} : { entry },
1440
+ ...explicit["out-dir"] === void 0 ? {} : { outDir },
1441
+ ...explicit.minify === void 0 ? {} : { minify },
1442
+ ...explicit.sourcemap === void 0 ? {} : { sourcemap },
1443
+ ...explicit.target === void 0 ? {} : { target }
1444
+ });
1445
+ if (resolvedConfig.storybook !== void 0) await buildStorybook(resolvedConfig.storybook, toolchain);
1446
+ })();
1399
1447
  });
1400
1448
  var build_default = defineCommand({
1401
1449
  name: "build",
@@ -1414,11 +1462,11 @@ var build_default = defineCommand({
1414
1462
  target
1415
1463
  ],
1416
1464
  run: async (arguments_) => {
1417
- const config = arguments_.mode === void 0 ? (await loadConfig()).config : void 0;
1418
- const selectedMode = arguments_.mode ?? config?.build?.mode;
1465
+ const configured = arguments_.mode === void 0 ? (await loadConfig()).config : void 0;
1419
1466
  const toolchain = new Toolchain(process.cwd());
1420
- if (selectedMode === "storybook") await toolchain.resolve("storybook");
1421
- else await Promise.all([toolchain.resolve("vite"), toolchain.resolve("rolldown")]);
1467
+ await Promise.all([toolchain.resolve("vite"), toolchain.resolve("rolldown")]);
1468
+ const config = configured ?? (await loadConfig()).config;
1469
+ if (config.storybook !== void 0) await toolchain.resolve("storybook");
1422
1470
  return commandRun$1(toolchain)(arguments_, config);
1423
1471
  }
1424
1472
  });
@@ -1604,25 +1652,6 @@ const noBundle = defineOption({
1604
1652
  description: "Emit the reachable Node graph with preserveModules, overriding configuration that enables bundling.",
1605
1653
  arity: 0
1606
1654
  });
1607
- const assertStorybookArguments = (explicit) => {
1608
- const option = [
1609
- "bundle",
1610
- "copy",
1611
- "declaration",
1612
- "entry",
1613
- "formats",
1614
- "minify",
1615
- "no-bundle",
1616
- "out-dir",
1617
- "platform",
1618
- "sourcemap",
1619
- "target"
1620
- ].find((name) => {
1621
- const value = explicit[name];
1622
- return name === "bundle" || name === "no-bundle" ? value === true : value !== void 0;
1623
- });
1624
- if (option !== void 0) throw new Error(`--${option} is not available in Storybook mode`);
1625
- };
1626
1655
  const dev = async (mode, entry, outDir, host, port, plugins = [], options = {}, viteConfig = {}, rolldownConfig = {}, toolchain = new Toolchain(process.cwd())) => {
1627
1656
  assertSyntaxTarget(options.target);
1628
1657
  if (mode === "web" && options.platform !== void 0) throw new Error("Web development does not accept platform; platform applies only to Node builds");
@@ -1654,7 +1683,7 @@ dev.web = async (host, port, plugins = [], waitForTermination = untilTerminated,
1654
1683
  await server.close();
1655
1684
  }
1656
1685
  };
1657
- dev.node = async (entry, outDir, plugins = [], waitForTermination = untilTerminated, options = {}, rolldownConfig = {}, toolchain = new Toolchain(process.cwd())) => {
1686
+ dev.node = async (entry, outDir, plugins = [], waitForTermination = untilTerminated, options = {}, rolldownConfig = {}, toolchain = new Toolchain(process.cwd()), onBuild) => {
1658
1687
  assertSyntaxTarget(options.target);
1659
1688
  const rolldown = await useToolApi("rolldown", void 0, toolchain);
1660
1689
  const plan = await createNodeBuildPlan(entry, outDir, options, resolveRolldownPlugins(plugins), rolldownConfig, toolchain);
@@ -1676,7 +1705,10 @@ dev.node = async (entry, outDir, plugins = [], waitForTermination = untilTermina
1676
1705
  }
1677
1706
  if (event.code === "BUNDLE_END") await event.result.close();
1678
1707
  if (event.code === "END" && !failed) try {
1679
- if (await lifecycle.complete() !== void 0) logger.success(`Built ${entry} to ${outDir}`);
1708
+ if (await lifecycle.complete() !== void 0) {
1709
+ logger.success(`Built ${entry} to ${outDir}`);
1710
+ await onBuild?.();
1711
+ }
1680
1712
  } catch (error) {
1681
1713
  logger.error(error);
1682
1714
  }
@@ -1694,14 +1726,31 @@ dev.node = async (entry, outDir, plugins = [], waitForTermination = untilTermina
1694
1726
  await watcher.close();
1695
1727
  }
1696
1728
  };
1697
- const commandRun = (toolchain) => withConfig((config) => config.build, ({ copy, declaration, formats, minify, mode, entry, "out-dir": outDir, host, platform, port, sourcemap, target }, buildConfig, resolvedConfig, explicit) => {
1698
- if (mode === "storybook") {
1699
- assertStorybookArguments(explicit);
1700
- return runStorybook("dev", resolvedConfig.storybook, {
1701
- host,
1702
- port
1729
+ const devWithStorybook = async (entry, outDir, storybook, host, port, plugins = [], options = {}, rolldownConfig = {}, toolchain = new Toolchain(process.cwd()), waitForTermination = untilTerminated) => {
1730
+ let stopNodeWatcher = () => {};
1731
+ const nodeStopped = new Promise((resolve) => {
1732
+ stopNodeWatcher = resolve;
1733
+ });
1734
+ let initialBuild = () => {};
1735
+ const built = new Promise((resolve) => {
1736
+ initialBuild = resolve;
1737
+ });
1738
+ const node = dev.node(entry, outDir, plugins, () => nodeStopped, options, rolldownConfig, toolchain, initialBuild);
1739
+ let storybookProcess;
1740
+ try {
1741
+ await Promise.race([built, node]);
1742
+ storybookProcess = await startStorybook("dev", storybook, {
1743
+ host: host ?? storybook.host,
1744
+ port: port ?? storybook.port
1703
1745
  }, toolchain);
1746
+ if (await Promise.race([waitForTermination().then(() => "terminated"), storybookProcess.completed.then(() => "storybook")]) === "storybook") throw new Error("Storybook development stopped");
1747
+ } finally {
1748
+ stopNodeWatcher();
1749
+ storybookProcess?.stop();
1750
+ await Promise.allSettled([node, ...storybookProcess === void 0 ? [] : [storybookProcess.completed]]);
1704
1751
  }
1752
+ };
1753
+ const commandRun = (toolchain) => withConfig((config) => config.build, ({ copy, declaration, formats, minify, mode, entry, "out-dir": outDir, host, platform, port, sourcemap, target }, buildConfig, resolvedConfig, explicit) => {
1705
1754
  if (explicit.bundle && explicit["no-bundle"]) throw new Error("--bundle and --no-bundle cannot be used together");
1706
1755
  const effective = resolveEffectiveBuildConfig(resolvedConfig, {
1707
1756
  bundle: explicit["no-bundle"] ? false : explicit.bundle ? true : buildConfig.bundle,
@@ -1719,6 +1768,7 @@ const commandRun = (toolchain) => withConfig((config) => config.build, ({ copy,
1719
1768
  }, explicit.entry !== void 0);
1720
1769
  const executableMode = effective.mode;
1721
1770
  if (executableMode !== "web" && executableMode !== "node") throw new Error("Expected a web or Node build mode");
1771
+ if (resolvedConfig.storybook !== void 0) return devWithStorybook(effective.entry, effective.outDir, resolvedConfig.storybook, explicit.host === void 0 ? void 0 : host, explicit.port === void 0 ? void 0 : port, resolvedConfig.plugins ?? [], effective, resolvedConfig.rolldown, toolchain);
1722
1772
  return dev(executableMode, effective.entry, effective.outDir, host, port, resolvedConfig.plugins ?? [], effective, resolvedConfig.vite, resolvedConfig.rolldown, toolchain);
1723
1773
  });
1724
1774
  var dev_default = defineCommand({
@@ -1740,11 +1790,11 @@ var dev_default = defineCommand({
1740
1790
  target
1741
1791
  ],
1742
1792
  run: async (arguments_) => {
1743
- const config = arguments_.mode === void 0 ? (await loadConfig()).config : void 0;
1744
- const selectedMode = arguments_.mode ?? config?.build?.mode;
1793
+ const configured = arguments_.mode === void 0 ? (await loadConfig()).config : void 0;
1745
1794
  const toolchain = new Toolchain(process.cwd());
1746
- if (selectedMode === "storybook") await toolchain.resolve("storybook");
1747
- else await Promise.all([toolchain.resolve("vite"), toolchain.resolve("rolldown")]);
1795
+ await Promise.all([toolchain.resolve("vite"), toolchain.resolve("rolldown")]);
1796
+ const config = configured ?? (await loadConfig()).config;
1797
+ if (config.storybook !== void 0) await toolchain.resolve("storybook");
1748
1798
  return commandRun(toolchain)(arguments_, config);
1749
1799
  }
1750
1800
  });
@@ -1784,7 +1834,9 @@ var preview_default = defineCommand({
1784
1834
  run: async ({ "out-dir": outDir, host, port, open }) => {
1785
1835
  await useTool("vite");
1786
1836
  const { config } = await loadConfig();
1787
- return preview(outDir ?? config.build?.outDir ?? "dist", host, port, outDir !== void 0, untilTerminated, open, config.vite);
1837
+ resolveEffectiveBuildConfig(config, {}, false);
1838
+ const storybook = config.storybook;
1839
+ return preview(outDir ?? (storybook === void 0 ? config.build?.outDir ?? "dist" : storybookOutputDir(storybook)), host, port, outDir !== void 0 || storybook !== void 0, untilTerminated, open, config.vite);
1788
1840
  }
1789
1841
  });
1790
1842
  const test = async (filters, config = {}, options = {}, waitForTermination = untilTerminated, storybook = {}) => {
@@ -1797,57 +1849,65 @@ const test = async (filters, config = {}, options = {}, waitForTermination = unt
1797
1849
  const nativeConfig = hasVitestConfig ? {} : config;
1798
1850
  const nativeCoverage = typeof nativeConfig.coverage === "object" && nativeConfig.coverage !== null ? nativeConfig.coverage : {};
1799
1851
  const nativeApi = typeof nativeConfig.api === "object" && nativeConfig.api !== null ? nativeConfig.api : {};
1800
- const storybookProject = storybook.test === false || !await hasStorybookConfig(storybook.configDir) ? void 0 : await createStorybookTestProject(storybook, vitestTool.version);
1801
- const vitestOptions = {
1802
- ...nativeConfig,
1803
- passWithNoTests: true,
1804
- run: !persistent,
1805
- watch: persistent,
1806
- ...options.environment === void 0 ? {} : { environment: options.environment },
1807
- ...options.coverage ? { coverage: {
1808
- ...nativeCoverage,
1809
- enabled: true,
1810
- provider: "v8"
1811
- } } : {},
1812
- ...options.ui ? { ui: true } : {},
1813
- ...options.uiPort === void 0 ? {} : { api: {
1814
- ...nativeApi,
1815
- host: "127.0.0.1",
1816
- port: options.uiPort,
1817
- strictPort: true
1818
- } }
1819
- };
1820
- const vitests = hasVitestConfig && storybookProject !== void 0 ? [await startVitest("test", filters, vitestOptions), await startVitest("test", filters, {
1821
- ...vitestOptions,
1822
- ...options.ui ? {
1823
- api: false,
1824
- ui: false
1825
- } : {},
1826
- projects: [storybookProject]
1827
- })] : [await startVitest("test", filters, {
1828
- ...vitestOptions,
1829
- ...storybookProject === void 0 ? {} : { projects: [...nativeConfig.projects ?? [], storybookProject] }
1830
- })];
1831
- if (persistent) {
1832
- try {
1833
- await waitForTermination();
1834
- } finally {
1835
- await Promise.all(vitests.map((vitest) => vitest.close()));
1852
+ const preparedStorybook = storybook.framework === void 0 || storybook.test === false || !await hasStorybookConfig(storybook.configDir) ? void 0 : await prepareStorybookConfig(storybook);
1853
+ try {
1854
+ const storybookProject = preparedStorybook === void 0 ? void 0 : await createStorybookTestProject(preparedStorybook.config, vitestTool.version);
1855
+ const vitestOptions = {
1856
+ ...nativeConfig,
1857
+ passWithNoTests: true,
1858
+ run: !persistent,
1859
+ watch: persistent,
1860
+ ...options.environment === void 0 ? {} : { environment: options.environment },
1861
+ ...options.coverage ? { coverage: {
1862
+ ...nativeCoverage,
1863
+ enabled: true,
1864
+ provider: "v8"
1865
+ } } : {},
1866
+ ...options.ui ? { ui: true } : {},
1867
+ ...options.uiPort === void 0 ? {} : { api: {
1868
+ ...nativeApi,
1869
+ host: "127.0.0.1",
1870
+ port: options.uiPort,
1871
+ strictPort: true
1872
+ } }
1873
+ };
1874
+ const vitests = hasVitestConfig && storybookProject !== void 0 ? [await startVitest("test", filters, vitestOptions), await startVitest("test", filters, {
1875
+ ...vitestOptions,
1876
+ ...options.ui ? {
1877
+ api: false,
1878
+ ui: false
1879
+ } : {},
1880
+ projects: [storybookProject]
1881
+ })] : [await startVitest("test", filters, {
1882
+ ...vitestOptions,
1883
+ ...storybookProject === void 0 ? {} : { projects: [...nativeConfig.projects ?? [], storybookProject] }
1884
+ })];
1885
+ if (persistent) {
1886
+ try {
1887
+ await waitForTermination();
1888
+ } finally {
1889
+ await Promise.all(vitests.map((vitest) => vitest.close()));
1890
+ }
1891
+ return;
1836
1892
  }
1837
- return;
1893
+ const failed = vitests.some((vitest) => vitest.state.getFiles().some((file) => file.result?.state === "fail") || vitest.state.getUnhandledErrors().length > 0);
1894
+ await Promise.all(vitests.map((vitest) => vitest.close()));
1895
+ if (failed) throw new Error("Tests failed");
1896
+ logger.success("Tests passed");
1897
+ } finally {
1898
+ await preparedStorybook?.cleanup();
1838
1899
  }
1839
- const failed = vitests.some((vitest) => vitest.state.getFiles().some((file) => file.result?.state === "fail") || vitest.state.getUnhandledErrors().length > 0);
1840
- await Promise.all(vitests.map((vitest) => vitest.close()));
1841
- if (failed) throw new Error("Tests failed");
1842
- logger.success("Tests passed");
1843
- };
1844
- const runTest = withConfig((config) => config.test, ({ filters, environment, coverage, ui, "ui-port": uiPort, watch }, config, resolvedConfig, explicitArguments) => test(filters, config, {
1845
- coverage,
1846
- environment: explicitArguments.environment === void 0 ? void 0 : environment,
1847
- ui,
1848
- uiPort,
1849
- watch
1850
- }, untilTerminated, resolvedConfig.storybook));
1900
+ };
1901
+ const runTest = withConfig((config) => config.test, ({ filters, environment, coverage, ui, "ui-port": uiPort, watch }, config, resolvedConfig, explicitArguments) => {
1902
+ effectiveUserConfigSchema.parse(resolvedConfig);
1903
+ return test(filters, config, {
1904
+ coverage,
1905
+ environment: explicitArguments.environment === void 0 ? void 0 : environment,
1906
+ ui,
1907
+ uiPort,
1908
+ watch
1909
+ }, untilTerminated, resolvedConfig.storybook);
1910
+ });
1851
1911
  var test_default = defineCommand({
1852
1912
  name: "test",
1853
1913
  arguments: [filters],
@@ -59,11 +59,7 @@ const syntaxTargetSchema = z.union([z.string().min(1), z.array(z.string().min(1)
59
59
  const nativeConfigSchema = () => z.custom((value) => typeof value === "object" && value !== null && !Array.isArray(value), "Expected a configuration object");
60
60
  const buildConfigSchema = z.strictObject({
61
61
  bundle: z.boolean().optional(),
62
- mode: z.enum([
63
- "web",
64
- "node",
65
- "storybook"
66
- ]).optional(),
62
+ mode: z.enum(["web", "node"]).optional(),
67
63
  entry: z.string().min(1).optional(),
68
64
  entries: z.record(z.string().min(1), z.string().min(1)).optional(),
69
65
  outDir: z.string().min(1).optional(),
@@ -86,7 +82,15 @@ const testConfigSchema = nativeConfigSchema();
86
82
  const viteConfigSchema = nativeConfigSchema();
87
83
  const storybookConfigSchema = z.strictObject({
88
84
  configDir: z.string().min(1).optional(),
85
+ framework: z.enum([
86
+ "react",
87
+ "svelte",
88
+ "vue",
89
+ "web-components"
90
+ ]).optional(),
91
+ host: z.string().min(1).optional(),
89
92
  outDir: z.string().min(1).optional(),
93
+ port: z.number().int().min(1).max(65535).optional(),
90
94
  test: z.boolean().optional()
91
95
  });
92
96
  const pluginSchema = z.custom(isWebAnvilPlugin, "Expected a Vite plugin or a WebAnvil plugin created with definePlugin()");
@@ -119,6 +123,18 @@ const effectiveUserConfigSchema = userConfigSchema.superRefine((config, context)
119
123
  message: NODE_PLUGIN_ERROR
120
124
  });
121
125
  }
126
+ if (config.storybook !== void 0) {
127
+ if (build.mode !== "node") context.addIssue({
128
+ code: "custom",
129
+ path: ["storybook"],
130
+ message: "storybook is available for Node projects; set build.mode to \"node\""
131
+ });
132
+ if (config.storybook.framework === void 0) context.addIssue({
133
+ code: "custom",
134
+ path: ["storybook", "framework"],
135
+ message: "storybook.framework selects the Storybook framework adapter"
136
+ });
137
+ }
122
138
  });
123
139
  const defaultConfig = {
124
140
  build: {
package/dist/index.d.mts CHANGED
@@ -103,7 +103,6 @@ declare const buildConfigSchema: z.ZodObject<{
103
103
  mode: z.ZodOptional<z.ZodEnum<{
104
104
  web: "web";
105
105
  node: "node";
106
- storybook: "storybook";
107
106
  }>>;
108
107
  entry: z.ZodOptional<z.ZodString>;
109
108
  entries: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -140,7 +139,15 @@ declare const testConfigSchema: z.ZodCustom<TestUserConfig, TestUserConfig>;
140
139
  declare const viteConfigSchema: z.ZodCustom<UserConfig$1, UserConfig$1>;
141
140
  declare const storybookConfigSchema: z.ZodObject<{
142
141
  configDir: z.ZodOptional<z.ZodString>;
142
+ framework: z.ZodOptional<z.ZodEnum<{
143
+ react: "react";
144
+ svelte: "svelte";
145
+ vue: "vue";
146
+ "web-components": "web-components";
147
+ }>>;
148
+ host: z.ZodOptional<z.ZodString>;
143
149
  outDir: z.ZodOptional<z.ZodString>;
150
+ port: z.ZodOptional<z.ZodNumber>;
144
151
  test: z.ZodOptional<z.ZodBoolean>;
145
152
  }, z.core.$strict>;
146
153
  declare const userConfigSchema: z.ZodObject<{
@@ -149,7 +156,6 @@ declare const userConfigSchema: z.ZodObject<{
149
156
  mode: z.ZodOptional<z.ZodEnum<{
150
157
  web: "web";
151
158
  node: "node";
152
- storybook: "storybook";
153
159
  }>>;
154
160
  entry: z.ZodOptional<z.ZodString>;
155
161
  entries: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -177,7 +183,15 @@ declare const userConfigSchema: z.ZodObject<{
177
183
  rolldown: z.ZodOptional<z.ZodCustom<RolldownConfig, RolldownConfig>>;
178
184
  storybook: z.ZodOptional<z.ZodObject<{
179
185
  configDir: z.ZodOptional<z.ZodString>;
186
+ framework: z.ZodOptional<z.ZodEnum<{
187
+ react: "react";
188
+ svelte: "svelte";
189
+ vue: "vue";
190
+ "web-components": "web-components";
191
+ }>>;
192
+ host: z.ZodOptional<z.ZodString>;
180
193
  outDir: z.ZodOptional<z.ZodString>;
194
+ port: z.ZodOptional<z.ZodNumber>;
181
195
  test: z.ZodOptional<z.ZodBoolean>;
182
196
  }, z.core.$strict>>;
183
197
  test: z.ZodOptional<z.ZodCustom<TestUserConfig, TestUserConfig>>;
@@ -190,7 +204,6 @@ declare const effectiveUserConfigSchema: z.ZodObject<{
190
204
  mode: z.ZodOptional<z.ZodEnum<{
191
205
  web: "web";
192
206
  node: "node";
193
- storybook: "storybook";
194
207
  }>>;
195
208
  entry: z.ZodOptional<z.ZodString>;
196
209
  entries: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -218,7 +231,15 @@ declare const effectiveUserConfigSchema: z.ZodObject<{
218
231
  rolldown: z.ZodOptional<z.ZodCustom<RolldownConfig, RolldownConfig>>;
219
232
  storybook: z.ZodOptional<z.ZodObject<{
220
233
  configDir: z.ZodOptional<z.ZodString>;
234
+ framework: z.ZodOptional<z.ZodEnum<{
235
+ react: "react";
236
+ svelte: "svelte";
237
+ vue: "vue";
238
+ "web-components": "web-components";
239
+ }>>;
240
+ host: z.ZodOptional<z.ZodString>;
221
241
  outDir: z.ZodOptional<z.ZodString>;
242
+ port: z.ZodOptional<z.ZodNumber>;
222
243
  test: z.ZodOptional<z.ZodBoolean>;
223
244
  }, z.core.$strict>>;
224
245
  test: z.ZodOptional<z.ZodCustom<TestUserConfig, TestUserConfig>>;
@@ -276,12 +297,11 @@ declare const build: {
276
297
  };
277
298
  declare const _default: import("cmdore").Command<readonly [{
278
299
  readonly name: "mode";
279
- readonly description: "Build mode: web uses Vite, node uses Rolldown, and storybook runs Storybook.";
300
+ readonly description: "Build mode: web uses Vite and node uses Rolldown.";
280
301
  readonly arity: 1;
281
302
  readonly schema: import("zod").ZodEnum<{
282
303
  web: "web";
283
304
  node: "node";
284
- storybook: "storybook";
285
305
  }>;
286
306
  }, {
287
307
  readonly name: "out-dir";
@@ -364,16 +384,15 @@ declare const _default$2: import("cmdore").Command<readonly import("cmdore").Opt
364
384
  declare const dev: {
365
385
  (mode: "web" | "node", entry: string, outDir: string, host?: string, port?: number, plugins?: WebAnvilPlugin[], options?: NodeBuildOptions, viteConfig?: UserConfig$1, rolldownConfig?: RolldownConfig, toolchain?: Toolchain): Promise<void>;
366
386
  web(host?: string, port?: number, plugins?: WebAnvilPlugin[], waitForTermination?: () => Promise<void>, viteConfig?: UserConfig$1, toolchain?: Toolchain): Promise<void>;
367
- node(entry: string, outDir: string, plugins?: WebAnvilPlugin[], waitForTermination?: () => Promise<void>, options?: NodeBuildOptions, rolldownConfig?: RolldownConfig, toolchain?: Toolchain): Promise<void>;
387
+ node(entry: string, outDir: string, plugins?: WebAnvilPlugin[], waitForTermination?: () => Promise<void>, options?: NodeBuildOptions, rolldownConfig?: RolldownConfig, toolchain?: Toolchain, onBuild?: () => void | Promise<void>): Promise<void>;
368
388
  };
369
389
  declare const _default$3: import("cmdore").Command<readonly [{
370
390
  readonly name: "mode";
371
- readonly description: "Build mode: web uses Vite, node uses Rolldown, and storybook runs Storybook.";
391
+ readonly description: "Build mode: web uses Vite and node uses Rolldown.";
372
392
  readonly arity: 1;
373
393
  readonly schema: import("zod").ZodEnum<{
374
394
  web: "web";
375
395
  node: "node";
376
- storybook: "storybook";
377
396
  }>;
378
397
  }, {
379
398
  readonly name: "out-dir";
@@ -598,12 +617,11 @@ declare const minify$1: {
598
617
  };
599
618
  declare const mode: {
600
619
  readonly name: "mode";
601
- readonly description: "Build mode: web uses Vite, node uses Rolldown, and storybook runs Storybook.";
620
+ readonly description: "Build mode: web uses Vite and node uses Rolldown.";
602
621
  readonly arity: 1;
603
622
  readonly schema: z.ZodEnum<{
604
623
  web: "web";
605
624
  node: "node";
606
- storybook: "storybook";
607
625
  }>;
608
626
  };
609
627
  declare const open: {
@@ -1,3 +1,6 @@
1
- import { StorybookConfig } from "@storybook/react-vite/node";
1
+ import { StorybookConfig as StorybookConfig$1 } from "@storybook/react-vite/node";
2
+ type StorybookConfig = Omit<StorybookConfig$1, "framework"> & {
3
+ framework?: StorybookConfig$1["framework"];
4
+ };
2
5
  declare const framework: string;
3
- export { type StorybookConfig, framework };
6
+ export { StorybookConfig, framework };
@@ -1,3 +1,6 @@
1
- import { StorybookConfig } from "@storybook/svelte-vite/node";
1
+ import { StorybookConfig as StorybookConfig$1 } from "@storybook/svelte-vite/node";
2
+ type StorybookConfig = Omit<StorybookConfig$1, "framework"> & {
3
+ framework?: StorybookConfig$1["framework"];
4
+ };
2
5
  declare const framework: string;
3
- export { type StorybookConfig, framework };
6
+ export { StorybookConfig, framework };
@@ -1,3 +1,6 @@
1
- import { StorybookConfig } from "@storybook/vue3-vite/node";
1
+ import { StorybookConfig as StorybookConfig$1 } from "@storybook/vue3-vite/node";
2
+ type StorybookConfig = Omit<StorybookConfig$1, "framework"> & {
3
+ framework?: StorybookConfig$1["framework"];
4
+ };
2
5
  declare const framework: string;
3
- export { type StorybookConfig, framework };
6
+ export { StorybookConfig, framework };
@@ -1,3 +1,6 @@
1
- import { StorybookConfig } from "@storybook/web-components-vite/node";
1
+ import { StorybookConfig as StorybookConfig$1 } from "@storybook/web-components-vite/node";
2
+ type StorybookConfig = Omit<StorybookConfig$1, "framework"> & {
3
+ framework?: StorybookConfig$1["framework"];
4
+ };
2
5
  declare const framework: string;
3
- export { type StorybookConfig, framework };
6
+ export { StorybookConfig, framework };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webanvil",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "A unified CLI for building, testing, linting, formatting, and type-checking JavaScript and TypeScript projects.",
5
5
  "keywords": [
6
6
  "build-tool",