void 0.10.11 → 0.10.12

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.
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  //#region src/cli/git-metadata.ts
3
- function git(root, args) {
3
+ function git(root, args, env) {
4
4
  try {
5
5
  return execFileSync("git", args, {
6
6
  cwd: root,
@@ -9,7 +9,8 @@ function git(root, args) {
9
9
  "ignore",
10
10
  "pipe",
11
11
  "ignore"
12
- ]
12
+ ],
13
+ env
13
14
  }).trim();
14
15
  } catch {
15
16
  return null;
@@ -60,17 +61,17 @@ function getGitHubRepositoryUrl(env) {
60
61
  if (!env.GITHUB_REPOSITORY) return null;
61
62
  return normalizeGitRemoteUrl(`${env.GITHUB_SERVER_URL ?? "https://github.com"}/${env.GITHUB_REPOSITORY}`);
62
63
  }
63
- function collectGitDeployMetadata(root, commit, env = process.env) {
64
+ function collectGitDeployMetadata(root, commit, env = process.env, childEnv = process.env) {
64
65
  const repositoryUrl = getGitHubRepositoryUrl(env) ?? normalizeGitRemoteUrl(git(root, [
65
66
  "remote",
66
67
  "get-url",
67
68
  "origin"
68
- ]));
69
+ ], childEnv));
69
70
  const gitRef = getGitRefFromEnv(env) ?? git(root, [
70
71
  "rev-parse",
71
72
  "--abbrev-ref",
72
73
  "HEAD"
73
- ]);
74
+ ], childEnv);
74
75
  return {
75
76
  commit,
76
77
  commitUrl: buildCommitUrl(repositoryUrl, commit),
@@ -1,7 +1,7 @@
1
1
  import { A as text, S as confirm, T as log, j as isCancel, k as select, u as import_picocolors, x as cancel } from "./agents-CtgBYqld.mjs";
2
2
  import { b as readProjectConfig, c as createCodeChallenge, f as getToken, h as openBrowser, l as createCodeVerifier, n as PlatformClient, o as parsePlatformErrorBody, s as awaitBrowserCallback, t as PlatformApiError } from "./client-Gb71-XkG.mjs";
3
3
  import { r as resolveProjectBySlug, t as getRequestedProjectSlug } from "./resolve-project-BBMtLLV9.mjs";
4
- import { t as collectGitDeployMetadata } from "./git-metadata-CBKaL0v5.mjs";
4
+ import { t as collectGitDeployMetadata } from "./git-metadata-D-gurjzG.mjs";
5
5
  import { randomBytes } from "node:crypto";
6
6
  //#region src/cli/github-cmd.ts
7
7
  /**
@@ -1,5 +1,5 @@
1
1
  import { a as join } from "./dist-DaKKDf8D.mjs";
2
- import { a as convertRedirectsConfig, d as parseHostPrefixedSource, i as convertHeadersConfig, o as convertRewritesConfig, r as convertFallbacksConfig, s as countSplats, t as VALID_REDIRECT_STATUSES, u as isProtocolRelative } from "./config-E03l1C_h.mjs";
2
+ import { a as convertRedirectsConfig, d as parseHostPrefixedSource, i as convertHeadersConfig, o as convertRewritesConfig, r as convertFallbacksConfig, s as countSplats, t as VALID_REDIRECT_STATUSES, u as isProtocolRelative } from "./config-BkTvs43g.mjs";
3
3
  import { i as voidWarn } from "./log-ChfPKsVd.mjs";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  //#region src/cli/headers.ts
package/dist/index.mjs CHANGED
@@ -1,24 +1,24 @@
1
1
  import { r as __toESM } from "./rolldown-runtime-DJK8HYOj.mjs";
2
2
  import { a as join, n as dirname, s as relative } from "./dist-DaKKDf8D.mjs";
3
3
  import { M as require_picocolors } from "./agents-CtgBYqld.mjs";
4
- import { A as resolveAssetRoutingPolicy, C as syncWranglerBindings, D as resolveSandboxConfig, E as isSandboxEnabled, O as hasApiRoutes, S as resolveLiveBindingConfig, T as SANDBOX_MIGRATION_TAG, _ as nextDurableObjectMigrationTag, a as prerenderPagesNode, c as filterLoadedEnv, d as LIVE_MIGRATION_TAG, g as hasDurableObjectSqliteMigration, h as ensureWranglerNodejsAlsFlag, i as prerenderPages, k as hasDocumentRoutes, l as resolveRemoteMode, m as ensureWranglerCompatibilityDate, n as resolveDistDir, o as collectRevalidate, p as ensureWranglerCompat, r as resolveWorkerDirName, s as hasRevalidate, u as stripInternalEnvKeys, v as normalizeWranglerCompatibilityFlags, x as readWranglerCompat, y as readExistingLiveBindingConfig } from "./deploy-DT8wsPZd.mjs";
4
+ import { A as resolveAssetRoutingPolicy, C as syncWranglerBindings, D as resolveSandboxConfig, E as isSandboxEnabled, O as hasApiRoutes, S as resolveLiveBindingConfig, T as SANDBOX_MIGRATION_TAG, _ as nextDurableObjectMigrationTag, a as prerenderPagesNode, c as filterLoadedEnv, d as LIVE_MIGRATION_TAG, g as hasDurableObjectSqliteMigration, h as ensureWranglerNodejsAlsFlag, i as prerenderPages, k as hasDocumentRoutes, l as resolveRemoteMode, m as ensureWranglerCompatibilityDate, n as resolveDistDir, o as collectRevalidate, p as ensureWranglerCompat, r as resolveWorkerDirName, s as hasRevalidate, u as stripInternalEnvKeys, v as normalizeWranglerCompatibilityFlags, x as readWranglerCompat, y as readExistingLiveBindingConfig } from "./deploy-u7Rv9q_q.mjs";
5
5
  import { i as resolveProjectPaths, n as readProjectPaths, t as assertNoRootSourceConventionConflicts } from "./project-paths-BQd7OmIo.mjs";
6
6
  import { a as scanQueuesSync, c as loadVoidAuthConfig, i as scanWebSocketRoutesSync, n as scanRoutePatternsSync, o as scanJobsSync, r as scanRoutes, s as findVoidAuthConfig, t as scanMiddlewareSync } from "./scan-DYXkrasO.mjs";
7
7
  import { b as readProjectConfig, f as getToken, m as isStagingMode } from "./client-Gb71-XkG.mjs";
8
8
  import { n as ensureProjectTsconfig, r as writeFileIfChanged } from "./project-tsconfig-B-QtXjLQ.mjs";
9
- import { c as getDatabaseDialect, f as readConfig, l as isNodeTarget, p as resolveBindingNames } from "./config-E03l1C_h.mjs";
9
+ import { c as getDatabaseDialect, f as readConfig, l as isNodeTarget, p as resolveBindingNames } from "./config-BkTvs43g.mjs";
10
10
  import { a as inferBindingsFromSource, i as inferProjectBindings, n as FRAMEWORK_SCAN_DIRS, o as walk, r as detectFramework, t as DEFAULT_SCAN_DIRS } from "./plugin-inference-DMeavIJ6.mjs";
11
11
  import { a as validateSsrEntries, c as isIdentifier, l as isLiteral, n as SSR_SERVER_ENTRY_RELATIVE_PATHS, o as validateSsrEntry, t as SSR_CLIENT_ENTRY_RELATIVE_PATHS, u as isMemberExpression } from "./entry-D7yy4xVH.mjs";
12
12
  import { i as voidWarn, n as voidError, r as voidLog, t as setShowTimestamp } from "./log-ChfPKsVd.mjs";
13
- import { l as parseDotenvFile } from "./preset-BGrvB4Bl.mjs";
13
+ import { l as parseDotenvFile } from "./preset-BjyR3lzz.mjs";
14
14
  import { i as generateVoidAuthDrizzleSchema, n as generateQueueTypes, o as DRIZZLE_OPERATORS, r as generateVoidAuthDatabaseModule, s as generateDbDts, t as generateRouteTypes } from "./route-types-COI2DsZv.mjs";
15
15
  import { t as discoverSchema } from "./discover-CJHyvYfR.mjs";
16
16
  import { t as MagicString } from "./magic-string.es-ZQjdJFFn.mjs";
17
17
  import { n as findEnvFileForPaths, r as generateEnvTypes, t as findEnvFile } from "./env-types-QBj-ndax.mjs";
18
- import { h as loadUserEnvFile } from "./env-validation-BdDlGhbN.mjs";
18
+ import { h as loadUserEnvFile } from "./env-validation-Dea3v3ej.mjs";
19
19
  import { i as resolveLayoutIds } from "./scan-DEwlM_Xy.mjs";
20
20
  import { n as STAGING_PROXY_URL, t as DEFAULT_PROXY_URL } from "./proxy-D-3_D-Gl.mjs";
21
- import { n as lintDuplicateSources, r as mergeRoutingRules, t as lintDestinationSplats } from "./headers-BQknpzkn.mjs";
21
+ import { n as lintDuplicateSources, r as mergeRoutingRules, t as lintDestinationSplats } from "./headers-nsHIFixA.mjs";
22
22
  import { n as INTERNAL_BINDING_PREFIXES, t as INTERNAL_BINDING_EXACT_NAMES } from "./env-raw-Cx8ElDdj.mjs";
23
23
  import { n as defer } from "./defer-YsYDUoii.mjs";
24
24
  import { VoidAssetRewriteError, basicAuth, defineHandler, defineHead, defineMiddleware, defineQueue, defineRender, defineScheduled } from "./runtime/handler.mjs";
@@ -1687,7 +1687,7 @@ function migrationPlugin(root, dialect, databaseId = "local", persistRoot = join
1687
1687
  if (applied.length > 0) voidLog(`Applied ${applied.length} migration(s): ${applied.join(", ")}`);
1688
1688
  }
1689
1689
  if (auth?.enabled) {
1690
- const { runLocalVoidAuthMigrations } = await import("./node-BDx8pmhq.mjs");
1690
+ const { runLocalVoidAuthMigrations } = await import("./node-Da0UcsGA.mjs");
1691
1691
  await runLocalVoidAuthMigrations(root, dialect, persistRoot, databaseId, auth.configPath);
1692
1692
  voidLog("Applied Better Auth migrations");
1693
1693
  }
@@ -3562,9 +3562,19 @@ function compilePageRoutes(lines, scan, pages, options) {
3562
3562
  * Class A framework wrappers. If the app returns 404, check ASSETS binding
3563
3563
  * for static files and set cache headers. Edge caching of hashed assets
3564
3564
  * happens at the dispatch layer (see design doc 0042).
3565
+ *
3566
+ * `notFound` is the RESOLVED `assetConfig.not_found_handling` for this build
3567
+ * (`resolveAssetRoutingPolicy`), and it decides what happens once the asset
3568
+ * binding has also answered 404. It is a separate argument from
3569
+ * `serveSpaFallback` because the two are not derived from each other: the
3570
+ * common worker-first app runs with `not_found_handling: 'none'` and STILL
3571
+ * wants the worker-side shell fallback (that is the whole reason the flag
3572
+ * exists). They are mutually exclusive in practice — `resolveAssetRoutingPolicy`
3573
+ * clears `workerSpaFallback` whenever an explicit `routing.notFound` asks for
3574
+ * anything other than the SPA shell.
3565
3575
  */
3566
- function compileAssetFallbackHelper(assetsDir = "assets", serveSpaFallback = false) {
3567
- const spaFallbackCode = serveSpaFallback ? ` // SPA fallback: when HTML-owning features make
3576
+ function compileAssetFallbackHelper(assetsDir = "assets", serveSpaFallback = false, notFound = "none") {
3577
+ const fallthroughCode = serveSpaFallback ? ` // SPA fallback: when HTML-owning features make
3568
3578
  // assetConfig.not_found_handling = 'none', the asset binding returns 404
3569
3579
  // instead of falling back to index.html itself. Do that fallback here, after
3570
3580
  // user routes/middleware have run, so auth gates and OAuth callbacks see the request first.
@@ -3583,6 +3593,13 @@ function compileAssetFallbackHelper(assetsDir = "assets", serveSpaFallback = fal
3583
3593
  return res;
3584
3594
  }
3585
3595
  }
3596
+ return response;` : notFound === "404-page" ? ` const accept = request.headers.get("accept") ?? "";
3597
+ const assetContentType = asset.headers.get("content-type") ?? "";
3598
+ if (accept.includes("text/html") && assetContentType.includes("text/html")) {
3599
+ const res = new Response(asset.body, asset);
3600
+ res.headers.set("Cache-Control", "no-store");
3601
+ return res;
3602
+ }
3586
3603
  return response;` : ` return response;`;
3587
3604
  return `
3588
3605
  async function serveWithAssets(request, env, response) {
@@ -3597,7 +3614,7 @@ async function serveWithAssets(request, env, response) {
3597
3614
  }
3598
3615
  return res;
3599
3616
  }
3600
- ${spaFallbackCode}
3617
+ ${fallthroughCode}
3601
3618
  }
3602
3619
  `;
3603
3620
  }
@@ -3616,7 +3633,7 @@ ${spaFallbackCode}
3616
3633
  * remote mode (`__VOID_REMOTE`) and replace local bindings with
3617
3634
  * proxy-backed versions.
3618
3635
  */
3619
- function compileWorkerEntry(hasQueues = false, remote, assetsDir = "assets", webSocketClasses = [], runtimeEnvPath = "void/_env", envSchemaModule, sandbox, serveSpaFallback = false, live, isr) {
3636
+ function compileWorkerEntry(hasQueues = false, remote, assetsDir = "assets", webSocketClasses = [], runtimeEnvPath = "void/_env", envSchemaModule, sandbox, serveSpaFallback = false, live, isr, notFound = "none") {
3620
3637
  const remotePath = remote?.remotePath;
3621
3638
  const remoteBindingNames = JSON.stringify(remote?.bindingNames);
3622
3639
  const createRemoteEnvCall = `createRemoteEnv(env${remote?.bindingNames ? `, ${remoteBindingNames}` : ""})`;
@@ -3661,7 +3678,7 @@ function __filterInternalBindings(env) {
3661
3678
  });
3662
3679
  }
3663
3680
  `;
3664
- const assetHelper = compileAssetFallbackHelper(assetsDir, serveSpaFallback);
3681
+ const assetHelper = compileAssetFallbackHelper(assetsDir, serveSpaFallback, notFound);
3665
3682
  const isrImport = isr ? `import { handleIsr } from ${JSON.stringify(isr.runtimePath)};\nimport { __VOID_ISR_REVALIDATE, __VOID_ISR_DEPLOYMENT_ID } from 'virtual:void-routes';\n` : "";
3666
3683
  const isrEnvCapture = isr ? ` env.__VOID_ISR_DEPLOYMENT_ID ??= __VOID_ISR_DEPLOYMENT_ID;\n env.__VOID_ISR_HOST = new URL(request.url).hostname;\n` : "";
3667
3684
  const isrDeploymentStamp = isr ? ` env.__VOID_ISR_DEPLOYMENT_ID ??= __VOID_ISR_DEPLOYMENT_ID;\n` : "";
@@ -4418,6 +4435,15 @@ function routingPlugin(root, options) {
4418
4435
  if (!scan) return false;
4419
4436
  return resolveCurrentAssetRoutingPolicy().workerSpaFallback;
4420
4437
  }
4438
+ /**
4439
+ * The `not_found_handling` the generated worker entry must agree with. Read
4440
+ * off the SAME resolved policy that produces the wrangler asset config, so
4441
+ * the emitted `serveWithAssets` can never disagree with the platform switch
4442
+ * it is standing in for.
4443
+ */
4444
+ function currentNotFoundHandling() {
4445
+ return resolveCurrentAssetRoutingPolicy().assetConfig.not_found_handling ?? "none";
4446
+ }
4421
4447
  function resolveCurrentAssetRoutingPolicy() {
4422
4448
  if (!scan) return resolveAssetRoutingPolicy({
4423
4449
  isSsr: Boolean(ssr),
@@ -4427,7 +4453,8 @@ function routingPlugin(root, options) {
4427
4453
  hasMiddleware: false,
4428
4454
  hasWebSockets: false,
4429
4455
  authEnabled: compileOptions.auth !== null,
4430
- liveEnabled: options.live !== void 0
4456
+ liveEnabled: options.live !== void 0,
4457
+ notFound: options.routing?.notFound
4431
4458
  });
4432
4459
  return resolveAssetRoutingPolicy({
4433
4460
  isSsr: Boolean(ssr),
@@ -4437,7 +4464,8 @@ function routingPlugin(root, options) {
4437
4464
  hasMiddleware: scan.middleware.length > 0,
4438
4465
  hasWebSockets: hasDocumentRoutes(scan.websockets ?? []),
4439
4466
  authEnabled: compileOptions.auth !== null,
4440
- liveEnabled: options.live !== void 0
4467
+ liveEnabled: options.live !== void 0,
4468
+ notFound: options.routing?.notFound
4441
4469
  });
4442
4470
  }
4443
4471
  const outDir = join(root, ".void");
@@ -4520,7 +4548,7 @@ function routingPlugin(root, options) {
4520
4548
  bindingName: options.sandboxBindingName,
4521
4549
  className: options.sandboxClassName,
4522
4550
  runtimePath: sandboxRuntimePath
4523
- } : false, shouldServeSpaFallback(), options.live, isr));
4551
+ } : false, shouldServeSpaFallback(), options.live, isr, currentNotFoundHandling()));
4524
4552
  }
4525
4553
  },
4526
4554
  resolveId: {
@@ -4571,7 +4599,7 @@ function routingPlugin(root, options) {
4571
4599
  bindingName: options.sandboxBindingName,
4572
4600
  className: options.sandboxClassName,
4573
4601
  runtimePath: sandboxRuntimePath
4574
- } : false, shouldServeSpaFallback(), options.live));
4602
+ } : false, shouldServeSpaFallback(), options.live, void 0, currentNotFoundHandling()));
4575
4603
  const structural = event !== "change";
4576
4604
  const pageManifestChanged = isInDirectory(normalizedPath, normalizedPagesDir) && (structural || normalizedPath.endsWith(".md"));
4577
4605
  for (const env of Object.values(devServer.environments)) {
@@ -4706,10 +4734,10 @@ function warnIfPackageJsonIsNotEsm(root) {
4706
4734
  * dead-code-eliminated in `vite build`. Production deploys go through
4707
4735
  * `cli/wrapper.ts` instead.
4708
4736
  */
4709
- function getFrameworkEntry(framework, root, devTriggerToken) {
4737
+ function getFrameworkEntry(framework, root, devTriggerToken, notFound) {
4710
4738
  const isBuild = isViteBuildOrPreview();
4711
4739
  if (!isBuild) writeFrameworkTriggersDispatch(root, devTriggerToken);
4712
- return generateClassATriggerWrapper(root, framework, getOriginalFrameworkEntry(framework, root), isBuild);
4740
+ return generateClassATriggerWrapper(root, framework, getOriginalFrameworkEntry(framework, root), isBuild, void 0, notFound);
4713
4741
  }
4714
4742
  /** The framework's own server entry, before our trigger wrapper. */
4715
4743
  function getOriginalFrameworkEntry(framework, root) {
@@ -4738,7 +4766,7 @@ function getOriginalFrameworkEntry(framework, root) {
4738
4766
  * dispatch file (and its transitive user-cron/queue imports + dev token)
4739
4767
  * would still ship in the production bundle.
4740
4768
  */
4741
- function generateClassATriggerWrapper(root, framework, original, isBuild, assetsDir = "assets") {
4769
+ function generateClassATriggerWrapper(root, framework, original, isBuild, assetsDir = "assets", notFound) {
4742
4770
  const outDir = join(root, ".void");
4743
4771
  mkdirSync(outDir, { recursive: true });
4744
4772
  const wrapperPath = join(outDir, `${framework}-trigger-wrapper.ts`);
@@ -4751,7 +4779,7 @@ export default {
4751
4779
  return serveWithAssets(request, env, await original.fetch(request, env, ctx));
4752
4780
  },
4753
4781
  };
4754
- ${compileAssetFallbackHelper(assetsDir)}
4782
+ ${compileAssetFallbackHelper(assetsDir, false, notFound)}
4755
4783
  ` : `// @ts-nocheck — Auto-generated by Void in dev. Do NOT edit.
4756
4784
  import original from ${JSON.stringify(original)};
4757
4785
 
@@ -5210,9 +5238,10 @@ function voidPlugin(options) {
5210
5238
  mkdirSync(outDir, { recursive: true });
5211
5239
  function mergeBindings(resolved) {
5212
5240
  const result = { ...workerConfig };
5213
- if (workerConfig.assets && resolved.assets) {
5214
- delete resolved.assets.not_found_handling;
5215
- delete resolved.assets.run_worker_first;
5241
+ const generatedAssets = workerConfig.assets;
5242
+ if (generatedAssets && resolved.assets) {
5243
+ if (generatedAssets.not_found_handling !== void 0) delete resolved.assets.not_found_handling;
5244
+ if (generatedAssets.run_worker_first !== void 0) delete resolved.assets.run_worker_first;
5216
5245
  }
5217
5246
  const d1Name = bindingNames?.d1 ?? "DB";
5218
5247
  const d1 = [];
@@ -5448,12 +5477,20 @@ function voidPlugin(options) {
5448
5477
  }
5449
5478
  };
5450
5479
  if (framework) {
5451
- workerConfig.main = getFrameworkEntry(framework, root, devTriggerToken);
5480
+ workerConfig.main = getFrameworkEntry(framework, root, devTriggerToken, config.routing?.notFound);
5452
5481
  const frameworkBuildWrapperPlugin = {
5453
5482
  name: "void:framework-build-wrapper",
5483
+ config(_userConfig, env) {
5484
+ const notFound = config.routing?.notFound;
5485
+ if (env.command !== "build" || notFound === void 0) return;
5486
+ workerConfig.assets = {
5487
+ ...workerConfig.assets,
5488
+ not_found_handling: notFound
5489
+ };
5490
+ },
5454
5491
  configResolved(resolved) {
5455
5492
  if (resolved.command !== "build") return;
5456
- generateClassATriggerWrapper(root, framework, getOriginalFrameworkEntry(framework, root), true, resolved.build.assetsDir);
5493
+ generateClassATriggerWrapper(root, framework, getOriginalFrameworkEntry(framework, root), true, resolved.build.assetsDir, config.routing?.notFound);
5457
5494
  }
5458
5495
  };
5459
5496
  const existingFlags = workerConfig.compatibility_flags ?? [];
@@ -5554,7 +5591,8 @@ function voidPlugin(options) {
5554
5591
  hasMiddleware,
5555
5592
  hasWebSockets: hasDocumentRoutes(websocketRoutes),
5556
5593
  authEnabled,
5557
- liveEnabled: liveConfig !== void 0
5594
+ liveEnabled: liveConfig !== void 0,
5595
+ notFound: config.routing?.notFound
5558
5596
  });
5559
5597
  workerConfig.main = join(root, ".void", "entry.ts");
5560
5598
  if (jobs.length > 0) workerConfig.triggers = { crons: jobs.flatMap((job) => job.crons) };
@@ -3,9 +3,9 @@ import { A as text, D as outro, E as note, S as confirm, T as log, a as cliTitle
3
3
  import { n as readProjectPaths } from "./project-paths-BQd7OmIo.mjs";
4
4
  import { S as writeProjectConfig, b as readProjectConfig, f as getToken, m as isStagingMode, n as PlatformClient } from "./client-Gb71-XkG.mjs";
5
5
  import { n as ensureProjectTsconfig, t as createProjectTsconfig } from "./project-tsconfig-B-QtXjLQ.mjs";
6
- import { c as getDatabaseDialect, f as readConfig } from "./config-E03l1C_h.mjs";
6
+ import { c as getDatabaseDialect, f as readConfig } from "./config-BkTvs43g.mjs";
7
7
  import { r as detectFramework } from "./plugin-inference-DMeavIJ6.mjs";
8
- import { l as parseDotenvFile, r as formatProjectCommand, s as expandDotenv } from "./preset-BGrvB4Bl.mjs";
8
+ import { l as parseDotenvFile, r as formatProjectCommand, s as expandDotenv } from "./preset-BjyR3lzz.mjs";
9
9
  import { t as MagicString } from "./magic-string.es-ZQjdJFFn.mjs";
10
10
  import { n as writeDrizzleConfig } from "./config-CutEMNGJ.mjs";
11
11
  import { n as promptAndCreateProject, r as promptProjectSelection } from "./create-project-DsYvl3TB.mjs";
@@ -1,6 +1,6 @@
1
1
  import { a as join } from "./dist-DaKKDf8D.mjs";
2
2
  import { c as loadVoidAuthConfig, s as findVoidAuthConfig } from "./scan-DYXkrasO.mjs";
3
- import { f as readConfig } from "./config-E03l1C_h.mjs";
3
+ import { f as readConfig } from "./config-BkTvs43g.mjs";
4
4
  import { a as resolveVoidAuthMigrationSchema } from "./route-types-COI2DsZv.mjs";
5
5
  import { n as applySqliteAuthMigrations, t as applyPgAuthMigrations } from "./auth-migrations-BTZ-ATvQ.mjs";
6
6
  import { getMiniflareDatabasePath } from "./runner-kapo9aPs.mjs";
@@ -1,8 +1,8 @@
1
1
  import { a as join } from "../dist-DaKKDf8D.mjs";
2
2
  import { r as resolvePagesDir } from "../project-paths-BQd7OmIo.mjs";
3
- import { f as readConfig } from "../config-E03l1C_h.mjs";
3
+ import { f as readConfig } from "../config-BkTvs43g.mjs";
4
4
  import { a as resolveNamedLayout, i as resolveLayoutIds, n as resolveEffectiveLayoutChain, r as resolveLayoutChain, t as scanPages } from "../scan-DEwlM_Xy.mjs";
5
- import { i as parseRedirectsFile } from "../headers-BQknpzkn.mjs";
5
+ import { i as parseRedirectsFile } from "../headers-nsHIFixA.mjs";
6
6
  import { renderBodyAttrs, renderHeadToString, renderHtmlAttrs, resolveHead } from "./head.mjs";
7
7
  import { serializePageData } from "./serialize.mjs";
8
8
  //#region src/pages/manifest.ts
@@ -3,7 +3,7 @@ import { D as outro, E as note, T as log, a as cliTitle, w as intro } from "./ag
3
3
  import { i as resolveProjectPaths } from "./project-paths-BQd7OmIo.mjs";
4
4
  import { c as loadVoidAuthConfig, r as scanRoutes, s as findVoidAuthConfig } from "./scan-DYXkrasO.mjs";
5
5
  import { n as ensureProjectTsconfig, r as writeFileIfChanged } from "./project-tsconfig-B-QtXjLQ.mjs";
6
- import { c as getDatabaseDialect, f as readConfig } from "./config-E03l1C_h.mjs";
6
+ import { c as getDatabaseDialect, f as readConfig } from "./config-BkTvs43g.mjs";
7
7
  import { i as inferProjectBindings, n as FRAMEWORK_SCAN_DIRS, r as detectFramework } from "./plugin-inference-DMeavIJ6.mjs";
8
8
  import { i as generateVoidAuthDrizzleSchema, n as generateQueueTypes, r as generateVoidAuthDatabaseModule, s as generateDbDts, t as generateRouteTypes } from "./route-types-COI2DsZv.mjs";
9
9
  import { t as discoverSchema } from "./discover-CJHyvYfR.mjs";
@@ -1,6 +1,6 @@
1
- import { a as join, c as resolve, n as dirname } from "./dist-DaKKDf8D.mjs";
1
+ import { a as join, c as resolve, n as dirname, s as relative } from "./dist-DaKKDf8D.mjs";
2
2
  import { n as readProjectPaths } from "./project-paths-BQd7OmIo.mjs";
3
- import { i as hasSsrEntry } from "./entry-D7yy4xVH.mjs";
3
+ import { i as hasSsrEntry, r as getSsrServerEntryPath } from "./entry-D7yy4xVH.mjs";
4
4
  import { i as readPackageJson, n as hasPackageDependency, t as getPackageDependencies } from "./package-json-Cx1osYo6.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { existsSync, readFileSync } from "node:fs";
@@ -446,6 +446,15 @@ function formatProjectCommand(root, command) {
446
446
  /**
447
447
  * Detect a static deploy preset based on the project root.
448
448
  * Returns null when the project requires a full worker deploy.
449
+ *
450
+ * `strictBackend` refuses (throws) instead of returning an SSG preset when the
451
+ * project ALSO has backend sources. SSG detection (step 2) deliberately runs
452
+ * before the backend check (step 3), so without this an SSG repo that grew a
453
+ * single `routes/api/*.ts` would deploy as a pure static site with the backend
454
+ * silently dropped and no diagnostic. Opt-in because only the UNCONFIGURED
455
+ * auto-detect path is ambiguous: `--dir` and a configured
456
+ * `inference.appType: "static" | "spa"` are the user stating what they want, and
457
+ * must keep working silently.
449
458
  */
450
459
  function detectPreset(root, options) {
451
460
  if (options?.dir) return {
@@ -457,6 +466,10 @@ function detectPreset(root, options) {
457
466
  if (pkg) {
458
467
  const allDeps = getPackageDependencies(pkg);
459
468
  for (const preset of Object.values(SSG_PRESETS)) if (preset.dep in allDeps) {
469
+ if (options?.strictBackend) {
470
+ const backendSources = findBackendSources(root);
471
+ if (backendSources.length > 0) throw new Error(ssgWithBackendMessage(preset.dep, backendSources));
472
+ }
460
473
  const overrides = preset.resolve?.(root);
461
474
  const cmd = overrides?.buildCommand ?? preset.buildCommand;
462
475
  const out = overrides?.outputDir ?? preset.outputDir;
@@ -486,6 +499,25 @@ function detectPreset(root, options) {
486
499
  return null;
487
500
  }
488
501
  /**
502
+ * The refusal shown when an SSG project also carries backend sources and no
503
+ * `inference.appType` says which one to deploy. Names the exact directories
504
+ * found and both ways out.
505
+ */
506
+ function ssgWithBackendMessage(ssgDep, backendSources) {
507
+ return [
508
+ `deploy: "${ssgDep}" was detected, but this project also has backend code: ${backendSources.join(", ")}.`,
509
+ "A static deploy would silently drop it, so Void will not guess. Pick one in void.json:",
510
+ "",
511
+ " Deploy the static output only (the backend is NOT deployed):",
512
+ " { \"inference\": { \"appType\": \"static\" } }",
513
+ "",
514
+ " Deploy both — the SSG emits the site, then Void packages it with the worker:",
515
+ " { \"inference\": { \"appType\": \"void\", \"build\": \"<ssg> build && vite build\" } }",
516
+ " ...and in vite.config.ts add voidPlugin() plus `publicDir: \"<ssg output dir>\"`,",
517
+ " so the generated site is copied into the client build alongside the worker."
518
+ ].join("\n");
519
+ }
520
+ /**
489
521
  * Resolve a local node_modules/.bin executable path.
490
522
  * Falls back to bare command name if the bin doesn't exist locally.
491
523
  */
@@ -523,6 +555,27 @@ function detectViteBinary(root) {
523
555
  return "vite";
524
556
  }
525
557
  /**
558
+ * Every backend source in the project that requires a full worker deploy, named
559
+ * relative to the root (e.g. `routes/`, `src/main.ssr.tsx`).
560
+ *
561
+ * Returned as a list rather than a boolean so a caller that has to REJECT a
562
+ * static deploy can tell the user exactly what would have been dropped.
563
+ */
564
+ function findBackendSources(root) {
565
+ const paths = readProjectPaths(root);
566
+ const found = [];
567
+ for (const dir of [
568
+ paths.routesDir,
569
+ paths.pagesDir,
570
+ paths.middlewareDir,
571
+ paths.cronsDir,
572
+ paths.queuesDir
573
+ ]) if (existsSync(dir)) found.push(`${relative(root, dir)}/`);
574
+ const ssrEntry = getSsrServerEntryPath(root, paths);
575
+ if (ssrEntry) found.push(relative(root, ssrEntry));
576
+ return found;
577
+ }
578
+ /**
526
579
  * Check whether the project has backend files that require a full worker deploy.
527
580
  */
528
581
  function hasBackendFiles(root) {
@@ -1,7 +1,7 @@
1
1
  import { a as join, c as resolve, n as dirname, o as normalize, t as basename } from "./dist-DaKKDf8D.mjs";
2
- import { b as readProvisionedBindingEntry, f as assertNoDuplicateMembers, w as writeProvisionedWranglerConfig } from "./deploy-DT8wsPZd.mjs";
2
+ import { b as readProvisionedBindingEntry, f as assertNoDuplicateMembers, w as writeProvisionedWranglerConfig } from "./deploy-u7Rv9q_q.mjs";
3
3
  import { i as voidWarn, r as voidLog } from "./log-ChfPKsVd.mjs";
4
- import { o as resolveWranglerArgv, u as scanWranglerDotenvKeys } from "./preset-BGrvB4Bl.mjs";
4
+ import { o as resolveWranglerArgv, u as scanWranglerDotenvKeys } from "./preset-BjyR3lzz.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { closeSync, existsSync, fsyncSync, lstatSync, openSync, readFileSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
7
7
  import { randomUUID } from "node:crypto";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "void",
3
- "version": "0.10.11",
3
+ "version": "0.10.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -356,7 +356,7 @@
356
356
  "valibot": ">=1.0.0-beta.7",
357
357
  "vite": "^8.0.0",
358
358
  "zod": "^3.25.0 || ^4.0.0",
359
- "@void/md": "0.10.11"
359
+ "@void/md": "0.10.12"
360
360
  },
361
361
  "peerDependenciesMeta": {
362
362
  "@void/md": {
package/schema.json CHANGED
@@ -144,6 +144,11 @@
144
144
  "type": "object",
145
145
  "description": "Routing configuration — headers, redirects, rewrites, fallbacks, revalidation, prerendering.",
146
146
  "properties": {
147
+ "notFound": {
148
+ "type": "string",
149
+ "enum": ["single-page-application", "404-page", "none"],
150
+ "description": "Override how the asset layer answers a request that matched no asset and no worker route. Void infers \"single-page-application\" when the worker does not own HTML routing; set \"404-page\" for a static-site-generator build so unknown URLs serve 404.html with a real HTTP 404 instead of index.html with a 200. Does not change run_worker_first. Ignored for SvelteKit, Nuxt, Analog, and Astro deploys, which pin not_found_handling to \"none\" because the framework's own worker owns unmatched HTML."
151
+ },
147
152
  "revalidate": {
148
153
  "description": "Edge caching TTL in seconds. A number sets a global TTL for all SSR pages. An object maps URL patterns to TTLs (first match wins). Set to 0 to disable caching for a path.",
149
154
  "oneOf": [
@@ -73,6 +73,48 @@ At the edge, the resolution order is:
73
73
 
74
74
  **Deploy:** `void deploy` or `void deploy --dir <path>` uploads static files directly. Assets are served from the edge with automatic caching.
75
75
 
76
+ ## Adding a backend to a static site
77
+
78
+ A static site generator plus a few API routes is a Void app, not a static site. Auto-detection sees the SSG dependency first (priority 2 below), so if you leave `inference.appType` unset, `void deploy` refuses rather than deploying the site and silently dropping `routes/`. Tell it which you meant.
79
+
80
+ To deploy the site **and** the backend, run both builds from one command and let Void's build fold the generated site into its client output:
81
+
82
+ ```json
83
+ // void.json
84
+ {
85
+ "inference": {
86
+ "appType": "void",
87
+ "build": "vitepress build && vite build"
88
+ }
89
+ }
90
+ ```
91
+
92
+ ```ts
93
+ // vite.config.ts
94
+ import { defineConfig } from 'vite';
95
+ import { voidPlugin } from 'void';
96
+
97
+ export default defineConfig({
98
+ plugins: [voidPlugin()],
99
+ // The SSG's output becomes the static assets of the Void build.
100
+ publicDir: '.vitepress/dist',
101
+ });
102
+ ```
103
+
104
+ `vitepress build` emits the site, then `vite build` copies `publicDir` into `dist/client` and emits the worker into `dist/ssr`. Keep `voidPlugin()` in the **root** `vite.config.ts` only — not in the generator's own config (e.g. `.vitepress/config.ts`).
105
+
106
+ Because the worker now owns unmatched requests, add [`routing.notFound`](../reference/config.md#routing-notfound) if you want the generator's `404.html` instead of the SPA-style `index.html` fallback:
107
+
108
+ ```json
109
+ { "routing": { "notFound": "404-page" } }
110
+ ```
111
+
112
+ To deploy the static output only and **not** the backend, say so explicitly:
113
+
114
+ ```json
115
+ { "inference": { "appType": "static" } }
116
+ ```
117
+
76
118
  ## Auto-Detection
77
119
 
78
120
  When running `void deploy` and no `inference.appType` is set in `void.json`, the detection logic runs in this order:
@@ -81,6 +123,7 @@ When running `void deploy` and no `inference.appType` is set in `void.json`, the
81
123
  | -------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
82
124
  | 1 | `--dir` flag | Static (or SPA with `--spa`) |
83
125
  | 2 | Known SSG in dependencies (`vitepress`, `@docusaurus/core`) | Static, builds with SSG CLI |
126
+ | 2a | ...and backend files also exist | Refused — set `inference.appType` yourself |
84
127
  | 3 | `@tanstack/react-start`, `@react-router/dev`, `@sveltejs/kit`, `nuxt`, `@analogjs/platform`, or `astro` in deps | Framework |
85
128
  | 4 | Backend files exist (`routes/`, `pages/`, `middleware/`, `crons/`, `queues/`, SSR entry) | Void app |
86
129
  | 5 | `vite` or `vite-plus` in dependencies, no backend files | SPA, builds with `vite build` or `vp build` |
@@ -97,6 +97,29 @@ This is the path needed for preview auth and other middleware. Cloudflare's plat
97
97
 
98
98
  For Void apps with only `/api` routes, Void keeps the platform SPA fallback and scopes `run_worker_first` to `/api` and `/api/*`. Static assets and non-API SPA navigations stay on the asset platform. API requests, including browser document navigations such as OAuth callbacks, reach the worker instead of being rewritten to `index.html`.
99
99
 
100
+ ### Unmatched requests
101
+
102
+ `not_found_handling` decides what the asset layer does with a request that matched no asset and no worker route. Void infers it:
103
+
104
+ | App shape | Inferred value | Result for an unknown URL |
105
+ | ------------------------------------------------- | -------------------------------- | -------------------------------- |
106
+ | Pages or SSR | `none` | The worker's own 404 |
107
+ | Worker owns HTML, no `pages/`, no SSR entry | `none` + worker fallback | `index.html` with status **200** |
108
+ | Asset-first (only `/api` routes, or none) | `single-page-application` | `index.html` with status **200** |
109
+ | Framework deploy (SvelteKit, Nuxt, Analog, Astro) | `none` — pinned, not overridable | The framework worker's own 404 |
110
+
111
+ The middle row is a worker with `middleware/`, a route outside `/api`, a document websocket, or Live, but no `pages/` and no SSR entry. `run_worker_first: ['/**']` sends everything to the worker, which bypasses the platform's SPA switch, so the generated worker does that fallback itself for HTML navigations — after your middleware has run, so auth gates and OAuth callbacks still see the request first.
112
+
113
+ The SPA fallback is right for a single-page app, where deep links must boot the client router. It is wrong for a site whose HTML was generated per page: unknown URLs return 200 instead of 404, and the generator's `404.html` is never served. Nothing in a built asset tree distinguishes the two, so Void does not guess — override it with [`routing.notFound`](../../reference/config.md#routing-notfound):
114
+
115
+ ```json
116
+ { "routing": { "notFound": "404-page" } }
117
+ ```
118
+
119
+ Accepted values are `"single-page-application"`, `"404-page"`, and `"none"`. `run_worker_first` keeps its inferred value, so API routes, auth, and `/__void/*` still reach the worker first. Setting anything other than `"single-page-application"` also turns off the worker-side `index.html` fallback described above — otherwise it would answer the request before `not_found_handling` was ever consulted. With `"404-page"` the worker serves whatever the asset binding returns for the unmatched path, which is Cloudflare's nearest `404.html` — but only for HTML navigations (requests whose `Accept` includes `text/html`), so an API route that deliberately returns a 404 keeps its own body, status and headers. If the build has no `404.html`, the worker's own 404 is kept.
120
+
121
+ The last row is the exception: `routing.notFound` is **ignored** for SvelteKit, Nuxt, Analog, and Astro deploys, and `void deploy` warns when you set it. Those deploys emit no `run_worker_first`, so the asset layer already answers first and real prerendered files win — `not_found_handling` would only change what the framework's own `env.ASSETS.fetch()` delegation returns, where `"single-page-application"` turns genuine 404s into the prerendered home page at 200 and `"404-page"` takes the 404 away from the framework's own error route. TanStack Start and React Router deploys are not affected; they follow the rows above.
122
+
100
123
  ### Generated config
101
124
 
102
125
  Void owns the generated asset routing policy during dev and build. If a root `wrangler.jsonc` contains stale `not_found_handling` or `run_worker_first` values, Void replaces those fields so generated config cannot accidentally change which layer sees a request first.
@@ -77,6 +77,7 @@ Most configuration is inferred automatically. Use `void.json` to override defaul
77
77
  | `routing.prerender` | Paths to prerender as static HTML at deploy time |
78
78
  | `routing.revalidate` | Default revalidation TTL in seconds for cached responses |
79
79
  | `inference.bindings` | Override inferred bindings. You only need this if auto-detection is not doing what you want. Accepts `true` or a custom binding name such as `"db": "MY_DB"`. |
80
+ | `routing.notFound` | **Ignored.** SvelteKit, Nuxt, Analog, and Astro pin `not_found_handling` to `"none"` — the framework worker owns unmatched HTML. `void deploy` warns if set. |
80
81
 
81
82
  See [Configuration](../../reference/config.md) for the full reference.
82
83
 
@@ -378,6 +378,24 @@ Paths to prerender as static HTML at deploy time. Each path must start with `/`.
378
378
  }
379
379
  ```
380
380
 
381
+ #### `routing.notFound`
382
+
383
+ Override how the asset layer answers a request that matched no asset and no worker route. One of `"single-page-application"`, `"404-page"`, or `"none"`. If omitted, Void infers it — see [Static Assets](../guide/edge/static-assets.md#unmatched-requests).
384
+
385
+ ```json
386
+ {
387
+ "routing": {
388
+ "notFound": "404-page"
389
+ }
390
+ }
391
+ ```
392
+
393
+ Set `"404-page"` when your assets come from a static site generator (VitePress, Docusaurus, …) and you want unknown URLs to serve the generator's own `404.html` with a real HTTP 404. Without it, an app whose only backend is API routes keeps the inferred SPA fallback, so every unknown URL returns `index.html` with a `200` — correct for a single-page app, wrong for a generated site, and bad for crawlers.
394
+
395
+ `run_worker_first` is untouched, so API routes, auth, and `/__void/*` still reach the worker first. When the worker owns HTML routing it also serves the resolved behavior itself, since `run_worker_first: ["/**"]` means the platform switch never runs — see [Unmatched requests](../guide/edge/static-assets.md#unmatched-requests).
396
+
397
+ This does **not** apply to SvelteKit, Nuxt, Analog, or Astro deploys: those pin `not_found_handling` to `"none"` because the framework's own worker owns unmatched HTML, and `void deploy` warns if you set `routing.notFound` anyway.
398
+
381
399
  ### `inference`
382
400
 
383
401
  Configuration for build-time inference, including how Void detects your app type, bindings, and build process.
@@ -403,12 +421,20 @@ Custom names are used in the deploy manifest, Wrangler config generation/sync, r
403
421
 
404
422
  #### `inference.build`
405
423
 
406
- Override the build command. Useful for frameworks with their own CLIs (Nuxt, Astro) or static apps with custom build scripts. If omitted, the CLI uses the detected default build command for the current app type.
424
+ Override the build command. Useful for frameworks with their own CLIs (Nuxt, Astro) or static apps with custom build scripts. If omitted, the CLI uses the detected default build command for the current app type. It applies to every app type, including `"void"` (where the default is `vite build`).
407
425
 
408
426
  ```json
409
427
  { "inference": { "build": "nuxt build --preset cloudflare-module" } }
410
428
  ```
411
429
 
430
+ The command is run through a shell from the project root, with the project's own `node_modules/.bin` on `PATH`, so bare binaries and `&&` chains work:
431
+
432
+ ```json
433
+ { "inference": { "build": "vitepress build && vite build" } }
434
+ ```
435
+
436
+ That chain is how a static site generator and a Void backend ship together — see [Adding a backend to a static site](../guide/app-types.md#adding-a-backend-to-a-static-site).
437
+
412
438
  #### `inference.scanDirs`
413
439
 
414
440
  Override which directories are scanned for [binding inference](./resource-inference.md). Paths are relative to the project root. Replaces the default directories for the current mode.