webanvil 0.0.11 → 0.0.13

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
@@ -25,6 +25,7 @@ Table of contents
25
25
  - [A web app](#a-web-app)
26
26
  - [Storybook](#storybook)
27
27
  - [A Node project](#a-node-project)
28
+ - [Browser tests](#browser-tests)
28
29
  - [Configuration](#configuration)
29
30
  - [Tool selection](#tool-selection)
30
31
  - [Native tool configuration](#native-tool-configuration)
@@ -48,6 +49,7 @@ wa clean # remove tracked build output
48
49
  wa check # check formatting, linting, and types
49
50
  wa check --fix # format files and apply safe lint fixes before type checking
50
51
  wa test # run tests, watch them, collect coverage, or open the UI
52
+ wa e2e # build, preview, and run browser tests
51
53
  wa lint # lint files
52
54
  wa format # format files
53
55
  wa typecheck # type-check the project
@@ -56,17 +58,18 @@ wa typecheck # type-check the project
56
58
  What it includes
57
59
  ----------------
58
60
 
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 |
61
+ | Project job | WebAnvil command | Tool |
62
+ | -------------------------- | ---------------------------------- | ------------------------------------------------- |
63
+ | Web builds and development | `wa build`, `wa dev`, `wa preview` | Vite |
64
+ | Node builds and watch mode | `wa build`, `wa dev` | Rolldown |
65
+ | Design-system Storybook | `wa build`, `wa dev`, `wa preview` | Storybook |
66
+ | Tracked output cleanup | `wa clean` | WebAnvil |
67
+ | Static checks | `wa check` | Oxfmt, Oxlint, TypeScript Native, or svelte-check |
68
+ | Tests | `wa test` | Vitest |
69
+ | Browser tests | `wa e2e` | Playwright Test and Chromium |
70
+ | Linting | `wa lint` | Oxlint |
71
+ | Formatting | `wa format` | Oxfmt |
72
+ | Type checking | `wa typecheck` | TypeScript Native or svelte-check |
70
73
 
71
74
  Getting started
72
75
  ---------------
@@ -95,6 +98,7 @@ Add the scripts you want to `package.json`:
95
98
  "clean": "wa clean",
96
99
  "check": "wa check",
97
100
  "test": "wa test",
101
+ "e2e": "wa e2e",
98
102
  "lint": "wa lint",
99
103
  "format": "wa format",
100
104
  "typecheck": "wa typecheck"
@@ -114,6 +118,7 @@ npm run build
114
118
  npm run clean
115
119
  npm run check
116
120
  npm run test
121
+ npm run e2e
117
122
  npm run lint
118
123
  npm run format
119
124
  npm run typecheck
@@ -124,6 +129,12 @@ first failure. It is read-only by default. Use `wa check --fix` to format files
124
129
  and apply safe lint fixes before type checking. Tests stay separate under
125
130
  `wa test`.
126
131
 
132
+ For a Svelte project, add `svelte-check` to the package's `devDependencies`.
133
+ Then `wa typecheck` and `wa check` use it for project-wide diagnostics through
134
+ the package's `tsconfig.json`. WebAnvil otherwise uses TypeScript Native.
135
+ Explicit file paths such as `wa typecheck src/file.ts` always use TypeScript
136
+ Native because `svelte-check` checks a project rather than individual files.
137
+
127
138
  ### A web app
128
139
 
129
140
  Set the build mode to `"web"` and point it at an HTML entry point. `wa dev` starts Vite's development server, `wa build` produces a production bundle, and `wa preview` serves that bundle locally.
@@ -329,16 +340,17 @@ preflights the engines it can dispatch before `webanvil.config.*` is loaded or
329
340
  its plugins are evaluated, so a declared command engine that is missing, has
330
341
  invalid package identity, or is outside the supported range fails first.
331
342
 
332
- | Tool | Supported project/workspace versions | Exact WebAnvil fallback |
333
- | -------------------------- | ------------------------------------ | ----------------------- |
334
- | Vite | `>=8.1.5 <9` | `8.1.5` |
335
- | Vitest | `>=4.1.10 <5` | `4.1.10` |
336
- | Storybook | `>=10.5.9 <11` | `10.5.9` |
337
- | Rolldown | `>=1.2.0 <2` | `1.2.0` |
338
- | Oxlint | `>=1.75.0 <2` | `1.75.0` |
339
- | Oxfmt | `>=0.60.0 <0.61` | `0.60.0` |
340
- | TypeScript (declarations) | `>=5 <7` | `6.0.3` |
341
- | TypeScript Native (`tsgo`) | `>=7.0.0-dev.20260707.2 <7.0.0` | `7.0.0-dev.20260707.2` |
343
+ | Tool | Supported project/workspace versions | Exact WebAnvil fallback |
344
+ | ---------------------------- | ------------------------------------ | ----------------------- |
345
+ | Vite | `>=8.1.5 <9` | `8.1.5` |
346
+ | Vitest | `>=4.1.10 <5` | `4.1.10` |
347
+ | Playwright Test and Chromium | Bundled only | `1.58.2` |
348
+ | Storybook | `>=10.5.9 <11` | `10.5.9` |
349
+ | Rolldown | `>=1.2.0 <2` | `1.2.0` |
350
+ | Oxlint | `>=1.75.0 <2` | `1.75.0` |
351
+ | Oxfmt | `>=0.60.0 <0.61` | `0.60.0` |
352
+ | TypeScript (declarations) | `>=5 <7` | `6.0.3` |
353
+ | TypeScript Native (`tsgo`) | `>=7.0.0-dev.20260707.2 <7.0.0` | `7.0.0-dev.20260707.2` |
342
354
 
343
355
  When a tool is first used, the CLI reports its package, version, and source, for
344
356
  example `Using rolldown 1.2.0 (project)` or
@@ -348,6 +360,9 @@ The TypeScript compiler is selected only after configuration enables a
348
360
  declaration build, but before Rolldown starts that build. It follows the same
349
361
  direct project/workspace declaration and exact-fallback rules.
350
362
 
363
+ `wa e2e` always uses WebAnvil's bundled Playwright Test and Chromium. This keeps
364
+ the runner and `webanvil/e2e` test API on the same version.
365
+
351
366
  ### Native tool configuration
352
367
 
353
368
  The `vite`, `test`, `rolldown`, `lint`, and `format` blocks use the owning
@@ -363,6 +378,10 @@ Precedence is:
363
378
  3. the matching native block in `webanvil.config.*`;
364
379
  4. WebAnvil defaults.
365
380
 
381
+ A `playwright.config.*` is different: it takes full control of `wa e2e`,
382
+ including the server lifecycle and browser configuration. WebAnvil still runs
383
+ its bundled Playwright Test binary.
384
+
366
385
  WebAnvil-owned `build`, `copy`, cross-engine `plugins`, and CLI behavior remain
367
386
  orchestration settings. `rolldown.input` and per-format `rolldown.output`
368
387
  options extend Node builds; WebAnvil still owns the input roots, output
@@ -488,6 +507,34 @@ These are run-specific modes; keep persistent Vitest configuration in
488
507
  `vitest.config.*`. `--ui-port` selects a strict loopback port and requires
489
508
  `--ui`.
490
509
 
510
+ ### Browser tests
511
+
512
+ `wa e2e` builds a web project, starts a production preview, and runs Playwright
513
+ Test files in `e2e/`. Import the test API from WebAnvil so the runner and test
514
+ code use the bundled matching version:
515
+
516
+ ```ts
517
+ import { expect, test } from "webanvil/e2e"
518
+
519
+ test("shows the home page", async ({ page }) => {
520
+ await page.goto("/")
521
+ await expect(page.getByRole("heading")).toBeVisible()
522
+ })
523
+ ```
524
+
525
+ The generated configuration provides a `chromium` project, so `wa e2e --project
526
+ chromium` works without a Playwright config. Use `--headed`, `--debug`, or `--ui`
527
+ for an interactive run. WebAnvil ships Playwright Test and Chromium, but the host
528
+ still needs the browser system libraries. On Linux CI, install them with:
529
+
530
+ ```sh
531
+ npx playwright install --with-deps chromium
532
+ ```
533
+
534
+ For advanced configuration, add `playwright.config.*`. WebAnvil then delegates
535
+ directly to Playwright and does not build or start a server. Configure
536
+ Playwright's `webServer` option in that file when the tests need one.
537
+
491
538
  ### Cleaning build output
492
539
 
493
540
  `wa build` records the actual emitted and copied files in
@@ -537,6 +584,7 @@ Command reference
537
584
  | `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
585
  | `wa preview` | Serves a Vite production build or configured static Storybook output. | `--out-dir`, `--host`, `--port`, `--open` |
539
586
  | `wa test [filters...]` | Runs Vitest once, in watch mode, with coverage, or UI. | `--environment`, `--watch`, `--coverage`, `--ui`, `--ui-port` |
587
+ | `wa e2e [filters...]` | Builds, previews, and runs Playwright browser tests. Native Playwright configuration takes control. | `--host`, `--port`, `--ui`, `--headed`, `--debug`, `--project` |
540
588
  | `wa lint [paths...]` | Runs Oxlint and treats warnings as failures. | `--fix` |
541
589
  | `wa format [paths...]` | Formats with Oxfmt. | `--check` |
542
590
  | `wa typecheck [paths...]` | Type-checks with TypeScript Native. | No options |
@@ -17,6 +17,7 @@ import { playwright } from "@vitest/browser-playwright";
17
17
  import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
18
18
  import { consola } from "consola";
19
19
  import { randomUUID } from "node:crypto";
20
+ import { tmpdir } from "node:os";
20
21
  const entry = defineArgument({
21
22
  name: "entry",
22
23
  description: "Web entry or Node public root; unbundled Node builds emit its reachable graph with preserveModules."
@@ -103,6 +104,12 @@ const supportedTools = {
103
104
  packageName: "vitest",
104
105
  range: ">=4.1.10 <5"
105
106
  },
107
+ playwright: {
108
+ packageName: "@playwright/test",
109
+ range: ">=1.58.2 <2",
110
+ bin: "playwright",
111
+ allowProjectOverride: false
112
+ },
106
113
  storybook: {
107
114
  packageName: "storybook",
108
115
  range: ">=10.5.9 <11",
@@ -132,6 +139,11 @@ const supportedTools = {
132
139
  bin: "tsgo"
133
140
  }
134
141
  };
142
+ const optionalTools = { "svelte-check": {
143
+ packageName: "svelte-check",
144
+ range: ">=4 <5",
145
+ bin: "svelte-check"
146
+ } };
135
147
  const dependencyFields = [
136
148
  "dependencies",
137
149
  "devDependencies",
@@ -318,6 +330,7 @@ const loadResolvedTool = async (name, definition, anchor, source) => {
318
330
  var Toolchain = class {
319
331
  cwd;
320
332
  #tools = /* @__PURE__ */ new Map();
333
+ #optionalTools = /* @__PURE__ */ new Map();
321
334
  constructor(cwd = process.cwd()) {
322
335
  this.cwd = resolve$1(cwd);
323
336
  }
@@ -328,13 +341,26 @@ var Toolchain = class {
328
341
  this.#tools.set(name, selected);
329
342
  return selected;
330
343
  }
344
+ resolveOptional(name) {
345
+ const existing = this.#optionalTools.get(name);
346
+ if (existing !== void 0) return existing;
347
+ const selected = this.#resolveOptional(name);
348
+ this.#optionalTools.set(name, selected);
349
+ return selected;
350
+ }
331
351
  async #resolve(name) {
332
352
  const definition = supportedTools[name];
333
- const declaration = await findDeclaration(this.cwd, definition.packageName);
353
+ const declaration = definition.allowProjectOverride === false ? void 0 : await findDeclaration(this.cwd, definition.packageName);
334
354
  if (declaration !== void 0) return loadResolvedTool(name, definition, declaration.directory, "project");
335
355
  const webanvilPackageRoot = dirname$1(await findContainingManifest(fileURLToPath(import.meta.url)));
336
356
  return loadResolvedTool(name, definition, webanvilPackageRoot, "webanvil");
337
357
  }
358
+ async #resolveOptional(name) {
359
+ const definition = optionalTools[name];
360
+ const declaration = await findDeclaration(this.cwd, definition.packageName);
361
+ if (declaration === void 0) return;
362
+ return loadResolvedTool(name, definition, declaration.directory, "project");
363
+ }
338
364
  };
339
365
  const formatResolvedTool = (tool) => `${tool.packageName} ${tool.version} (${tool.source})`;
340
366
  const declarationDefaults = {
@@ -1078,9 +1104,20 @@ const useTool = async (name, toolchain = new Toolchain(process.cwd())) => {
1078
1104
  }
1079
1105
  return tool;
1080
1106
  };
1107
+ const useOptionalTool = async (name, toolchain = new Toolchain(process.cwd())) => {
1108
+ const tool = await toolchain.resolveOptional(name);
1109
+ if (tool === void 0) return;
1110
+ const identity = `${tool.packageRoot}:${tool.version}`;
1111
+ if (!announced.has(identity)) {
1112
+ announced.add(identity);
1113
+ logger.info(`Using ${formatResolvedTool(tool)}`);
1114
+ }
1115
+ return tool;
1116
+ };
1081
1117
  const useToolApi = async (name, subpath, toolchain = new Toolchain(process.cwd())) => (await useTool(name, toolchain)).import(subpath);
1082
1118
  const useToolExecutable = async (name, toolchain = new Toolchain(process.cwd())) => {
1083
- const tool = await useTool(name, toolchain);
1119
+ const tool = name === "svelte-check" ? await useOptionalTool(name, toolchain) : await useTool(name, toolchain);
1120
+ if (tool === void 0) throw new Error(`${name} is not declared by the active project or workspace`);
1084
1121
  if (tool.executable === void 0) throw new Error(`${tool.packageName} does not expose an executable`);
1085
1122
  return tool.executable;
1086
1123
  };
@@ -1225,6 +1262,11 @@ const declaration = defineOption({
1225
1262
  arity: 1,
1226
1263
  schema: z.enum(["true", "false"]).transform((value) => value === "true")
1227
1264
  });
1265
+ const debug = defineOption({
1266
+ name: "debug",
1267
+ description: "Run browser tests in Playwright debug mode.",
1268
+ arity: 0
1269
+ });
1228
1270
  const environment = defineOption({
1229
1271
  name: "environment",
1230
1272
  description: "Vitest environment to use for this run.",
@@ -1246,6 +1288,11 @@ const host = defineOption({
1246
1288
  description: "Host interface for the web server.",
1247
1289
  arity: 1
1248
1290
  });
1291
+ const headed = defineOption({
1292
+ name: "headed",
1293
+ description: "Run browser tests with a visible browser window.",
1294
+ arity: 0
1295
+ });
1249
1296
  const minify = defineOption({
1250
1297
  name: "minify",
1251
1298
  description: "Minify the build output: true or false.",
@@ -1278,6 +1325,12 @@ const platform = defineOption({
1278
1325
  "neutral"
1279
1326
  ])
1280
1327
  });
1328
+ const project = defineOption({
1329
+ name: "project",
1330
+ description: "Run one named Playwright project.",
1331
+ arity: 1,
1332
+ schema: z.string().min(1)
1333
+ });
1281
1334
  const port = defineOption({
1282
1335
  name: "port",
1283
1336
  description: "Port for the web server.",
@@ -1301,7 +1354,7 @@ const target = defineOption({
1301
1354
  });
1302
1355
  const ui = defineOption({
1303
1356
  name: "ui",
1304
- description: "Start the Vitest user interface.",
1357
+ description: "Start the test user interface.",
1305
1358
  arity: 0
1306
1359
  });
1307
1360
  const uiPort = defineOption({
@@ -1332,7 +1385,7 @@ const build = async (mode, entry, outDir, options = {}, plugins = [], viteConfig
1332
1385
  const rolldown = await useToolApi("rolldown", void 0, toolchain);
1333
1386
  await runNodeBuild(await createNodeBuildPlan(entry, outDir, options, resolveRolldownPlugins(plugins), rolldownConfig, toolchain), rolldown.rolldown);
1334
1387
  logger.success(`Built ${entry} to ${outDir}`);
1335
- return;
1388
+ return outDir;
1336
1389
  }
1337
1390
  const web = await build.webConfig(entry, outDir, options, plugins, viteConfig, toolchain, explicit);
1338
1391
  const target = web.outDir;
@@ -1349,6 +1402,7 @@ const build = async (mode, entry, outDir, options = {}, plugins = [], viteConfig
1349
1402
  ...copied
1350
1403
  ]);
1351
1404
  logger.success(`Built ${entry} to ${outDir}`);
1405
+ return target;
1352
1406
  };
1353
1407
  build.webConfig = async (entry, outDir, options, plugins, viteConfig = {}, toolchain = new Toolchain(process.cwd()), explicit = {}) => {
1354
1408
  assertSyntaxTarget(options.target);
@@ -1521,7 +1575,7 @@ const rebaseOxlintConfig = (config, cwd, configDirectory) => ({
1521
1575
  }) } : {}
1522
1576
  });
1523
1577
  const runTool = async (name, arguments_, config) => {
1524
- if (name !== "tsgo" && await hasOxcConfig(name)) config = void 0;
1578
+ if ((name === "oxfmt" || name === "oxlint") && await hasOxcConfig(name)) config = void 0;
1525
1579
  const executable = await useToolExecutable(name === "tsgo" ? "typescript-native" : name);
1526
1580
  const cwd = process.cwd();
1527
1581
  const configDirectory = join(cwd, ".webanvil");
@@ -1532,7 +1586,7 @@ const runTool = async (name, arguments_, config) => {
1532
1586
  const generatedConfig = configWithoutIgnores === void 0 || configPath === void 0 ? configWithoutIgnores : name === "oxfmt" ? rebaseOxfmtConfig(configWithoutIgnores, cwd, configDirectory) : rebaseOxlintConfig(configWithoutIgnores, cwd, configDirectory);
1533
1587
  const ignoreArguments = name === "oxfmt" ? ignorePatterns.map((pattern) => pattern.startsWith("!") ? pattern.slice(1) : `!${pattern}`) : ignorePatterns.flatMap((pattern) => ["--ignore-pattern", pattern]);
1534
1588
  const internalIgnoreArguments = name === "oxfmt" ? ["!**/.webanvil/**"] : ["--ignore-pattern", ".webanvil/**"];
1535
- const toolArguments = name === "tsgo" ? arguments_ : [
1589
+ const toolArguments = name === "tsgo" || name === "svelte-check" ? arguments_ : [
1536
1590
  ...configPath === void 0 ? [] : ["--config", configPath],
1537
1591
  ...ignoreArguments,
1538
1592
  ...internalIgnoreArguments,
@@ -1594,9 +1648,10 @@ const typecheckArguments = async (paths) => {
1594
1648
  ];
1595
1649
  return getTsconfig(process.cwd(), { typescriptVersion: false })?.config.references?.length ? ["-b", "--noEmit"] : ["--noEmit"];
1596
1650
  };
1597
- const typecheck = async (paths) => {
1598
- logger.start("Type checking");
1599
- await runTool("tsgo", await typecheckArguments(paths));
1651
+ const typecheck = async (paths, options) => {
1652
+ const svelteCheck = paths.length === 0 ? options === void 0 ? await useOptionalTool("svelte-check") : options.svelteCheck : void 0;
1653
+ logger.start(svelteCheck === void 0 ? "Type checking" : "Checking Svelte");
1654
+ await runTool(svelteCheck === void 0 ? "tsgo" : "svelte-check", svelteCheck === void 0 ? await typecheckArguments(paths) : []);
1600
1655
  logger.success("Type check passed");
1601
1656
  };
1602
1657
  var typecheck_default = defineCommand({
@@ -1609,23 +1664,25 @@ const fix = defineOption({
1609
1664
  description: "Format files and apply safe lint fixes.",
1610
1665
  arity: 0
1611
1666
  });
1612
- const checkProject = async (fixFiles = false, config = {}) => {
1667
+ const checkProject = async (fixFiles = false, config = {}, typecheckOptions) => {
1613
1668
  await format([], !fixFiles, config.format);
1614
1669
  await lint([], fixFiles, config.lint);
1615
- await typecheck([]);
1670
+ if (typecheckOptions === void 0) await typecheck([]);
1671
+ else await typecheck([], typecheckOptions);
1616
1672
  };
1617
1673
  var check_default = defineCommand({
1618
1674
  name: "check",
1619
1675
  description: "Check formatting, linting, and types, stopping at the first failure.",
1620
1676
  options: [fix],
1621
1677
  run: async ({ fix }) => {
1622
- await Promise.all([
1678
+ const [svelteCheck] = await Promise.all([
1679
+ useOptionalTool("svelte-check"),
1623
1680
  useTool("oxfmt"),
1624
- useTool("oxlint"),
1625
- useTool("typescript-native")
1681
+ useTool("oxlint")
1626
1682
  ]);
1683
+ if (svelteCheck === void 0) await useTool("typescript-native");
1627
1684
  const { config } = await loadConfig();
1628
- await checkProject(fix, config);
1685
+ await checkProject(fix, config, { svelteCheck });
1629
1686
  }
1630
1687
  });
1631
1688
  const clean = async () => {
@@ -1798,7 +1855,7 @@ var dev_default = defineCommand({
1798
1855
  return commandRun(toolchain)(arguments_, config);
1799
1856
  }
1800
1857
  });
1801
- const preview = async (outDir, host, port, useOutDir = false, waitForTermination = untilTerminated, openBrowser, viteConfig = {}) => {
1858
+ const startPreview = async (outDir, host, port, useOutDir = false, openBrowser, viteConfig = {}) => {
1802
1859
  logger.start("Starting web preview");
1803
1860
  const vite = await useToolApi("vite");
1804
1861
  const hasViteConfig = await hasToolConfig("vite");
@@ -1815,7 +1872,10 @@ const preview = async (outDir, host, port, useOutDir = false, waitForTermination
1815
1872
  ...openBrowser === void 0 ? {} : { open: openBrowser }
1816
1873
  }
1817
1874
  });
1818
- const server = await vite.preview(config);
1875
+ return vite.preview(config);
1876
+ };
1877
+ const preview = async (outDir, host, port, useOutDir = false, waitForTermination = untilTerminated, openBrowser, viteConfig = {}) => {
1878
+ const server = await startPreview(outDir, host, port, useOutDir, openBrowser, viteConfig);
1819
1879
  try {
1820
1880
  server.printUrls();
1821
1881
  await waitForTermination();
@@ -1839,6 +1899,94 @@ var preview_default = defineCommand({
1839
1899
  return preview(outDir ?? (storybook === void 0 ? config.build?.outDir ?? "dist" : storybookOutputDir(storybook)), host, port, outDir !== void 0 || storybook !== void 0, untilTerminated, open, config.vite);
1840
1900
  }
1841
1901
  });
1902
+ const localUrl = (server) => {
1903
+ const url = server.resolvedUrls?.local?.[0] ?? server.resolvedUrls?.network?.[0];
1904
+ if (url === void 0) throw new Error("WebAnvil could not determine the preview URL for browser tests");
1905
+ return url;
1906
+ };
1907
+ const argumentsFor = (filters, options, config, passWithNoTests = false) => [
1908
+ "test",
1909
+ ...config === void 0 ? [] : ["--config", config],
1910
+ ...filters,
1911
+ ...passWithNoTests ? ["--pass-with-no-tests"] : [],
1912
+ ...options.ui ? ["--ui"] : [],
1913
+ ...options.headed ? ["--headed"] : [],
1914
+ ...options.debug ? ["--debug"] : [],
1915
+ ...options.project === void 0 ? [] : ["--project", options.project]
1916
+ ];
1917
+ const defaultE2EConfig = (testDir, baseURL, outputDir) => ({
1918
+ outputDir,
1919
+ projects: [{
1920
+ name: "chromium",
1921
+ use: { browserName: "chromium" }
1922
+ }],
1923
+ testDir,
1924
+ use: { baseURL }
1925
+ });
1926
+ const defaultConfig = (testDir, baseURL, outputDir) => `${JSON.stringify(defaultE2EConfig(testDir, baseURL, outputDir), void 0, 4)}\n`;
1927
+ const runPlaywright = async (filters, options, config, baseURL, passWithNoTests = false) => {
1928
+ await execa(await useToolExecutable("playwright"), argumentsFor(filters, options, config, passWithNoTests), {
1929
+ cwd: process.cwd(),
1930
+ env: {
1931
+ ...process.env,
1932
+ ...baseURL === void 0 ? {} : { WEBANVIL_E2E_URL: baseURL },
1933
+ PLAYWRIGHT_TEST: "1"
1934
+ },
1935
+ stdio: "inherit"
1936
+ });
1937
+ };
1938
+ const e2e = async (filters, options = {}) => {
1939
+ logger.start("Running end-to-end tests");
1940
+ if (await hasToolConfig("playwright")) {
1941
+ await runPlaywright(filters, options);
1942
+ logger.success("End-to-end tests passed");
1943
+ return;
1944
+ }
1945
+ const { config } = await loadConfig();
1946
+ const effective = resolveEffectiveBuildConfig(config, {}, false);
1947
+ if (effective.mode !== "web") throw new Error("wa e2e requires build.mode: \"web\" or a native playwright.config.* file");
1948
+ const toolchain = new Toolchain(process.cwd());
1949
+ const server = await startPreview(await build(effective.mode, effective.entry, effective.outDir, effective, config.plugins ?? [], config.vite, config.rolldown, toolchain), options.host, options.port, true, false, config.vite);
1950
+ let directory;
1951
+ try {
1952
+ const baseURL = localUrl(server);
1953
+ directory = await mkdtemp(join$1(tmpdir(), "webanvil-playwright-"));
1954
+ const configPath = join$1(directory, "playwright.config.mjs");
1955
+ await writeFile(configPath, `export default ${defaultConfig(resolve$1(process.cwd(), "e2e"), baseURL, join$1(directory, "test-results"))}`);
1956
+ await runPlaywright(filters, options, configPath, baseURL, true);
1957
+ } finally {
1958
+ await Promise.all([server.close(), ...directory === void 0 ? [] : [rm(directory, {
1959
+ force: true,
1960
+ recursive: true
1961
+ })]]);
1962
+ }
1963
+ logger.success("End-to-end tests passed");
1964
+ };
1965
+ var e2e_default = defineCommand({
1966
+ name: "e2e",
1967
+ description: "Run end-to-end browser tests.",
1968
+ arguments: [filters],
1969
+ options: [
1970
+ host,
1971
+ port,
1972
+ ui,
1973
+ headed,
1974
+ debug,
1975
+ project
1976
+ ],
1977
+ run: async ({ filters, host, port, ui, headed, debug, project }) => {
1978
+ const nativePlaywrightConfig = await hasToolConfig("playwright");
1979
+ await Promise.all([useTool("playwright"), ...nativePlaywrightConfig ? [] : [useTool("vite")]]);
1980
+ return e2e(filters, {
1981
+ debug,
1982
+ headed,
1983
+ host,
1984
+ port,
1985
+ project,
1986
+ ui
1987
+ });
1988
+ }
1989
+ });
1842
1990
  const test = async (filters, config = {}, options = {}, waitForTermination = untilTerminated, storybook = {}) => {
1843
1991
  if (options.uiPort !== void 0 && options.ui !== true) throw new Error("--ui-port requires --ui");
1844
1992
  logger.start("Running tests");
@@ -1923,4 +2071,4 @@ var test_default = defineCommand({
1923
2071
  return runTest(arguments_);
1924
2072
  }
1925
2073
  });
1926
- export { build, build_default, bundle, check, checkProject, check_default, clean, clean_default, copy, coverage, declaration, dev, dev_default, entry, environment, filters, fix$1 as fix, format, format_default, formats, host, lint, lint_default, logger, minify, mode, open, outDir, paths, platform, port, preview, preview_default, sourcemap, target, test, test_default, typecheck, typecheck_default, ui, uiPort, watch };
2074
+ export { build, build_default, bundle, check, checkProject, check_default, clean, clean_default, copy, coverage, debug, declaration, dev, dev_default, e2e, e2e_default, entry, environment, filters, fix$1 as fix, format, format_default, formats, headed, host, lint, lint_default, logger, minify, mode, open, outDir, paths, platform, port, preview, preview_default, project, sourcemap, target, test, test_default, typecheck, typecheck_default, ui, uiPort, watch };
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { build_default, check_default, clean_default, dev_default, format_default, lint_default, logger, preview_default, test_default, typecheck_default } from "./_chunks/commands.mjs";
1
+ import { build_default, check_default, clean_default, dev_default, e2e_default, format_default, lint_default, logger, preview_default, test_default, typecheck_default } from "./_chunks/commands.mjs";
2
2
  import { execute } from "cmdore";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { readPackageJSON } from "pkg-types";
@@ -9,6 +9,7 @@ const main = async (...varargs) => {
9
9
  check_default,
10
10
  clean_default,
11
11
  dev_default,
12
+ e2e_default,
12
13
  format_default,
13
14
  lint_default,
14
15
  preview_default,
package/dist/e2e.d.mts ADDED
@@ -0,0 +1,2 @@
1
+ import { defineConfig, devices, expect, test } from "@playwright/test";
2
+ export { defineConfig, devices, expect, test };
package/dist/e2e.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { defineConfig, devices, expect, test } from "@playwright/test";
2
+ export { defineConfig, devices, expect, test };
package/dist/index.d.mts CHANGED
@@ -35,10 +35,12 @@ declare const isUnpluginAdapter: (plugin: unknown) => plugin is UnpluginAdapter;
35
35
  declare const isWebAnvilPlugin: (plugin: unknown) => plugin is WebAnvilPlugin;
36
36
  declare const resolveRolldownPlugins: (plugins: WebAnvilPlugin[]) => Plugin[];
37
37
  declare const resolveVitePlugins: (plugins: WebAnvilPlugin[]) => PluginOption[];
38
- type ToolName = "vite" | "vitest" | "rolldown" | "oxlint" | "oxfmt" | "storybook" | "typescript" | "typescript-native";
38
+ type ToolName = "vite" | "vitest" | "playwright" | "rolldown" | "oxlint" | "oxfmt" | "storybook" | "typescript" | "typescript-native";
39
+ type OptionalToolName = "svelte-check";
40
+ type AnyToolName = ToolName | OptionalToolName;
39
41
  type ToolSource = "project" | "webanvil";
40
42
  type ResolvedTool = {
41
- name: ToolName;
43
+ name: AnyToolName;
42
44
  packageName: string;
43
45
  version: string;
44
46
  source: ToolSource;
@@ -51,6 +53,7 @@ declare class Toolchain {
51
53
  readonly cwd: string;
52
54
  constructor(cwd?: string);
53
55
  resolve(name: ToolName): Promise<ResolvedTool>;
56
+ resolveOptional(name: OptionalToolName): Promise<ResolvedTool | undefined>;
54
57
  }
55
58
  type DeclarationLogger = {
56
59
  info: (...arguments_: unknown[]) => void;
@@ -290,7 +293,7 @@ type WebBuild = {
290
293
  vite: ViteApi;
291
294
  };
292
295
  declare const build: {
293
- (mode: "web" | "node", entry: string, outDir: string, options?: BuildOptions, plugins?: WebAnvilPlugin[], viteConfig?: UserConfig$1, rolldownConfig?: RolldownConfig, toolchain?: Toolchain, explicit?: ExplicitWebBuild): Promise<void>;
296
+ (mode: "web" | "node", entry: string, outDir: string, options?: BuildOptions, plugins?: WebAnvilPlugin[], viteConfig?: UserConfig$1, rolldownConfig?: RolldownConfig, toolchain?: Toolchain, explicit?: ExplicitWebBuild): Promise<string>;
294
297
  webConfig(entry: string, outDir: string, options: BuildOptions, plugins: WebAnvilPlugin[], viteConfig?: UserConfig$1, toolchain?: Toolchain, explicit?: ExplicitWebBuild): Promise<WebBuild>;
295
298
  publicOutputFiles({ outDir, publicDir }: WebBuild): Promise<string[]>;
296
299
  web(web: WebBuild): Promise<string[]>;
@@ -372,8 +375,17 @@ declare const _default: import("cmdore").Command<readonly [{
372
375
  readonly name: "entry";
373
376
  readonly description: "Web entry or Node public root; unbundled Node builds emit its reachable graph with preserveModules.";
374
377
  }]>;
378
+ type TypecheckOptions = {
379
+ svelteCheck: ResolvedTool | undefined;
380
+ };
381
+ declare const typecheck: (paths: string[], options?: TypecheckOptions) => Promise<void>;
382
+ declare const _default$9: import("cmdore").Command<readonly import("cmdore").Option[], readonly [{
383
+ readonly name: "paths";
384
+ readonly description: "Files or directories to check.";
385
+ readonly variadic: true;
386
+ }]>;
375
387
  type CheckConfig = Pick<UserConfig, "format" | "lint">;
376
- declare const checkProject: (fixFiles?: boolean, config?: CheckConfig) => Promise<void>;
388
+ declare const checkProject: (fixFiles?: boolean, config?: CheckConfig, typecheckOptions?: TypecheckOptions) => Promise<void>;
377
389
  declare const _default$1: import("cmdore").Command<readonly [{
378
390
  readonly name: "fix";
379
391
  readonly description: "Format files and apply safe lint fixes.";
@@ -472,8 +484,48 @@ declare const _default$3: import("cmdore").Command<readonly [{
472
484
  readonly name: "entry";
473
485
  readonly description: "Web entry or Node public root; unbundled Node builds emit its reachable graph with preserveModules.";
474
486
  }]>;
475
- declare const format: (paths: string[], check?: boolean, config?: FormatConfig) => Promise<void>;
487
+ type E2EOptions = {
488
+ debug?: boolean;
489
+ headed?: boolean;
490
+ host?: string;
491
+ port?: number;
492
+ project?: string;
493
+ ui?: boolean;
494
+ };
495
+ declare const e2e: (filters: string[], options?: E2EOptions) => Promise<void>;
476
496
  declare const _default$4: import("cmdore").Command<readonly [{
497
+ readonly name: "host";
498
+ readonly description: "Host interface for the web server.";
499
+ readonly arity: 1;
500
+ }, {
501
+ readonly name: "port";
502
+ readonly description: "Port for the web server.";
503
+ readonly arity: 1;
504
+ readonly schema: import("zod").ZodCoercedNumber<unknown>;
505
+ }, {
506
+ readonly name: "ui";
507
+ readonly description: "Start the test user interface.";
508
+ readonly arity: 0;
509
+ }, {
510
+ readonly name: "headed";
511
+ readonly description: "Run browser tests with a visible browser window.";
512
+ readonly arity: 0;
513
+ }, {
514
+ readonly name: "debug";
515
+ readonly description: "Run browser tests in Playwright debug mode.";
516
+ readonly arity: 0;
517
+ }, {
518
+ readonly name: "project";
519
+ readonly description: "Run one named Playwright project.";
520
+ readonly arity: 1;
521
+ readonly schema: import("zod").ZodString;
522
+ }], readonly [{
523
+ readonly name: "filters";
524
+ readonly description: "Test files or names to run.";
525
+ readonly variadic: true;
526
+ }]>;
527
+ declare const format: (paths: string[], check?: boolean, config?: FormatConfig) => Promise<void>;
528
+ declare const _default$5: import("cmdore").Command<readonly [{
477
529
  readonly name: "check";
478
530
  readonly description: "Check formatting without writing files.";
479
531
  readonly arity: 0;
@@ -483,7 +535,7 @@ declare const _default$4: import("cmdore").Command<readonly [{
483
535
  readonly variadic: true;
484
536
  }]>;
485
537
  declare const lint: (paths: string[], fix?: boolean, config?: LintConfig) => Promise<void>;
486
- declare const _default$5: import("cmdore").Command<readonly [{
538
+ declare const _default$6: import("cmdore").Command<readonly [{
487
539
  readonly name: "fix";
488
540
  readonly description: "Apply safe lint fixes.";
489
541
  readonly arity: 0;
@@ -493,7 +545,7 @@ declare const _default$5: import("cmdore").Command<readonly [{
493
545
  readonly variadic: true;
494
546
  }]>;
495
547
  declare const preview: (outDir: string, host?: string, port?: number, useOutDir?: boolean, waitForTermination?: () => Promise<void>, openBrowser?: boolean, viteConfig?: UserConfig$1) => Promise<void>;
496
- declare const _default$6: import("cmdore").Command<readonly [{
548
+ declare const _default$7: import("cmdore").Command<readonly [{
497
549
  readonly name: "out-dir";
498
550
  readonly description: "Directory where the build output is written.";
499
551
  readonly arity: 1;
@@ -518,7 +570,7 @@ declare const test: (filters: string[], config?: TestConfig, options?: {
518
570
  uiPort?: number;
519
571
  watch?: boolean;
520
572
  }, waitForTermination?: () => Promise<void>, storybook?: StorybookConfig) => Promise<void>;
521
- declare const _default$7: import("cmdore").Command<readonly [{
573
+ declare const _default$8: import("cmdore").Command<readonly [{
522
574
  readonly name: "environment";
523
575
  readonly description: "Vitest environment to use for this run.";
524
576
  readonly arity: 1;
@@ -532,7 +584,7 @@ declare const _default$7: import("cmdore").Command<readonly [{
532
584
  readonly arity: 0;
533
585
  }, {
534
586
  readonly name: "ui";
535
- readonly description: "Start the Vitest user interface.";
587
+ readonly description: "Start the test user interface.";
536
588
  readonly arity: 0;
537
589
  }, {
538
590
  readonly name: "ui-port";
@@ -544,12 +596,6 @@ declare const _default$7: import("cmdore").Command<readonly [{
544
596
  readonly description: "Test files or names to run.";
545
597
  readonly variadic: true;
546
598
  }]>;
547
- declare const typecheck: (paths: string[]) => Promise<void>;
548
- declare const _default$8: import("cmdore").Command<readonly import("cmdore").Option[], readonly [{
549
- readonly name: "paths";
550
- readonly description: "Files or directories to check.";
551
- readonly variadic: true;
552
- }]>;
553
599
  declare const bundle$1: {
554
600
  readonly name: "bundle";
555
601
  readonly description: "Bundle the Node public roots; without it, emit their reachable graph with preserveModules.";
@@ -582,6 +628,11 @@ declare const declaration: {
582
628
  false: "false";
583
629
  }>, z.ZodTransform<boolean, "true" | "false">>;
584
630
  };
631
+ declare const debug: {
632
+ readonly name: "debug";
633
+ readonly description: "Run browser tests in Playwright debug mode.";
634
+ readonly arity: 0;
635
+ };
585
636
  declare const environment: {
586
637
  readonly name: "environment";
587
638
  readonly description: "Vitest environment to use for this run.";
@@ -606,6 +657,11 @@ declare const host: {
606
657
  readonly description: "Host interface for the web server.";
607
658
  readonly arity: 1;
608
659
  };
660
+ declare const headed: {
661
+ readonly name: "headed";
662
+ readonly description: "Run browser tests with a visible browser window.";
663
+ readonly arity: 0;
664
+ };
609
665
  declare const minify$1: {
610
666
  readonly name: "minify";
611
667
  readonly description: "Minify the build output: true or false.";
@@ -644,6 +700,12 @@ declare const platform$1: {
644
700
  neutral: "neutral";
645
701
  }>;
646
702
  };
703
+ declare const project: {
704
+ readonly name: "project";
705
+ readonly description: "Run one named Playwright project.";
706
+ readonly arity: 1;
707
+ readonly schema: z.ZodString;
708
+ };
647
709
  declare const port: {
648
710
  readonly name: "port";
649
711
  readonly description: "Port for the web server.";
@@ -667,7 +729,7 @@ declare const target$1: {
667
729
  };
668
730
  declare const ui: {
669
731
  readonly name: "ui";
670
- readonly description: "Start the Vitest user interface.";
732
+ readonly description: "Start the test user interface.";
671
733
  readonly arity: 0;
672
734
  };
673
735
  declare const uiPort: {
@@ -683,4 +745,4 @@ declare const watch: {
683
745
  };
684
746
  declare function defineConfig(config: UserConfig): UserConfig;
685
747
  declare function defineConfig(config: UserConfigFactory): UserConfigFactory;
686
- export { BuildConfig, ConfigExport, CopyMapping, FormatConfig, LintConfig, ResolvedConfig, RolldownConfig, StorybookConfig, SyntaxTarget, TestConfig, UnpluginAdapter, UserConfig, UserConfigFactory, ViteConfig, WebAnvilPlugin, WebAnvilUnplugin, assertSyntaxTarget, build, _default as buildCommand, buildConfigSchema, bundle$1 as bundle, check, _default$1 as checkCommand, checkProject, clean, _default$2 as cleanCommand, copy, copyMappingSchema, coverage, declaration, defaultConfig, defineConfig, definePlugin, dev, _default$3 as devCommand, effectiveUserConfigSchema, entry$1 as entry, environment, filters, fix, format, _default$4 as formatCommand, formatConfigSchema, formats$1 as formats, host, isUnpluginAdapter, isWebAnvilPlugin, lint, _default$5 as lintCommand, lintConfigSchema, loadConfig, minify$1 as minify, mode, open, outDir$1 as outDir, paths, platform$1 as platform, port, preview, _default$6 as previewCommand, resolveEffectiveBuildConfig, resolveRolldownPlugins, resolveVitePlugins, rolldownConfigSchema, sourcemap$1 as sourcemap, storybookConfigSchema, syntaxTargetSchema, target$1 as target, test, _default$7 as testCommand, testConfigSchema, typecheck, _default$8 as typecheckCommand, ui, uiPort, userConfigSchema, viteConfigSchema, watch, withConfig };
748
+ export { BuildConfig, ConfigExport, CopyMapping, FormatConfig, LintConfig, ResolvedConfig, RolldownConfig, StorybookConfig, SyntaxTarget, TestConfig, UnpluginAdapter, UserConfig, UserConfigFactory, ViteConfig, WebAnvilPlugin, WebAnvilUnplugin, assertSyntaxTarget, build, _default as buildCommand, buildConfigSchema, bundle$1 as bundle, check, _default$1 as checkCommand, checkProject, clean, _default$2 as cleanCommand, copy, copyMappingSchema, coverage, debug, declaration, defaultConfig, defineConfig, definePlugin, dev, _default$3 as devCommand, e2e, _default$4 as e2eCommand, effectiveUserConfigSchema, entry$1 as entry, environment, filters, fix, format, _default$5 as formatCommand, formatConfigSchema, formats$1 as formats, headed, host, isUnpluginAdapter, isWebAnvilPlugin, lint, _default$6 as lintCommand, lintConfigSchema, loadConfig, minify$1 as minify, mode, open, outDir$1 as outDir, paths, platform$1 as platform, port, preview, _default$7 as previewCommand, project, resolveEffectiveBuildConfig, resolveRolldownPlugins, resolveVitePlugins, rolldownConfigSchema, sourcemap$1 as sourcemap, storybookConfigSchema, syntaxTargetSchema, target$1 as target, test, _default$8 as testCommand, testConfigSchema, typecheck, _default$9 as typecheckCommand, ui, uiPort, userConfigSchema, viteConfigSchema, watch, withConfig };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { build, build_default, bundle, check, checkProject, check_default, clean, clean_default, copy, coverage, declaration, dev, dev_default, entry, environment, filters, fix, format, format_default, formats, host, lint, lint_default, minify, mode, open, outDir, paths, platform, port, preview, preview_default, sourcemap, target, test, test_default, typecheck, typecheck_default, ui, uiPort, watch } from "./_chunks/commands.mjs";
1
+ import { build, build_default, bundle, check, checkProject, check_default, clean, clean_default, copy, coverage, debug, declaration, dev, dev_default, e2e, e2e_default, entry, environment, filters, fix, format, format_default, formats, headed, host, lint, lint_default, minify, mode, open, outDir, paths, platform, port, preview, preview_default, project, sourcemap, target, test, test_default, typecheck, typecheck_default, ui, uiPort, watch } from "./_chunks/commands.mjs";
2
2
  import { assertSyntaxTarget, buildConfigSchema, copyMappingSchema, defaultConfig, defineConfig as defineConfig$1, definePlugin, effectiveUserConfigSchema, formatConfigSchema, isUnpluginAdapter, isWebAnvilPlugin, lintConfigSchema, loadConfig, resolveEffectiveBuildConfig, resolveRolldownPlugins, resolveVitePlugins, rolldownConfigSchema, storybookConfigSchema, syntaxTargetSchema, testConfigSchema, userConfigSchema, viteConfigSchema, withConfig } from "./_chunks/config.mjs";
3
3
  function defineConfig(config) {
4
4
  return defineConfig$1(config);
5
5
  }
6
- export { assertSyntaxTarget, build, build_default as buildCommand, buildConfigSchema, bundle, check, check_default as checkCommand, checkProject, clean, clean_default as cleanCommand, copy, copyMappingSchema, coverage, declaration, defaultConfig, defineConfig, definePlugin, dev, dev_default as devCommand, effectiveUserConfigSchema, entry, environment, filters, fix, format, format_default as formatCommand, formatConfigSchema, formats, host, isUnpluginAdapter, isWebAnvilPlugin, lint, lint_default as lintCommand, lintConfigSchema, loadConfig, minify, mode, open, outDir, paths, platform, port, preview, preview_default as previewCommand, resolveEffectiveBuildConfig, resolveRolldownPlugins, resolveVitePlugins, rolldownConfigSchema, sourcemap, storybookConfigSchema, syntaxTargetSchema, target, test, test_default as testCommand, testConfigSchema, typecheck, typecheck_default as typecheckCommand, ui, uiPort, userConfigSchema, viteConfigSchema, watch, withConfig };
6
+ export { assertSyntaxTarget, build, build_default as buildCommand, buildConfigSchema, bundle, check, check_default as checkCommand, checkProject, clean, clean_default as cleanCommand, copy, copyMappingSchema, coverage, debug, declaration, defaultConfig, defineConfig, definePlugin, dev, dev_default as devCommand, e2e, e2e_default as e2eCommand, effectiveUserConfigSchema, entry, environment, filters, fix, format, format_default as formatCommand, formatConfigSchema, formats, headed, host, isUnpluginAdapter, isWebAnvilPlugin, lint, lint_default as lintCommand, lintConfigSchema, loadConfig, minify, mode, open, outDir, paths, platform, port, preview, preview_default as previewCommand, project, resolveEffectiveBuildConfig, resolveRolldownPlugins, resolveVitePlugins, rolldownConfigSchema, sourcemap, storybookConfigSchema, syntaxTargetSchema, target, test, test_default as testCommand, testConfigSchema, typecheck, typecheck_default as typecheckCommand, ui, uiPort, userConfigSchema, viteConfigSchema, watch, withConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webanvil",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
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",
@@ -41,6 +41,11 @@
41
41
  "import": "./dist/index.mjs",
42
42
  "default": "./dist/index.mjs"
43
43
  },
44
+ "./e2e": {
45
+ "types": "./dist/e2e.d.mts",
46
+ "import": "./dist/e2e.mjs",
47
+ "default": "./dist/e2e.mjs"
48
+ },
44
49
  "./storybook/test/preset": {
45
50
  "types": "./dist/storybook/test/preset.d.mts",
46
51
  "import": "./dist/storybook/test/preset.mjs",
@@ -106,6 +111,7 @@
106
111
  },
107
112
  "dependencies": {
108
113
  "@playwright/browser-chromium": "1.58.2",
114
+ "@playwright/test": "1.58.2",
109
115
  "@storybook/addon-vitest": "10.5.9",
110
116
  "@storybook/react-vite": "10.5.9",
111
117
  "@storybook/svelte-vite": "10.5.9",