void 0.10.12 → 0.10.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.
Files changed (28) hide show
  1. package/dist/cli/cli.mjs +10 -10
  2. package/dist/{config-BkTvs43g.mjs → config-qHGgPuWT.mjs} +10 -0
  3. package/dist/{db-DOiJMRt2.mjs → db-C0i0sYMS.mjs} +3 -3
  4. package/dist/{deploy-u7Rv9q_q.mjs → deploy-jBJT1fUT.mjs} +227 -21
  5. package/dist/{gen-o_w-8yI8.mjs → gen-BzXf3Jh2.mjs} +1 -1
  6. package/dist/{headers-nsHIFixA.mjs → headers-ChAADPQu.mjs} +1 -1
  7. package/dist/index.mjs +49 -39
  8. package/dist/{init-3rgHKBVi.mjs → init-7JCAcKNQ.mjs} +1 -1
  9. package/dist/{node-Da0UcsGA.mjs → node-pRg81HqV.mjs} +3 -3
  10. package/dist/pages/index.mjs +3 -3
  11. package/dist/pages/islands-plugin.mjs +1 -1
  12. package/dist/{prepare-BoKHgMNx.mjs → prepare-BvvgAz-3.mjs} +3 -3
  13. package/dist/{provision-rShh6MKY.mjs → provision-CPx2ZxsH.mjs} +1 -1
  14. package/dist/{route-types-COI2DsZv.mjs → route-types-z1jtHEi_.mjs} +8 -1
  15. package/dist/{scan-DYXkrasO.mjs → scan-ChWt4pX1.mjs} +12 -13
  16. package/dist/{scan-DEwlM_Xy.mjs → scan-NU4xKGci.mjs} +4 -1
  17. package/package.json +2 -2
  18. package/schema.json +13 -0
  19. package/skills/migrate-vite-cloudflare-to-void/SKILL.md +1 -1
  20. package/skills/void/docs/guide/deployment.md +68 -1
  21. package/skills/void/docs/guide/edge/static-assets.md +15 -2
  22. package/skills/void/docs/guide/server-routing.md +21 -1
  23. package/skills/void/docs/guide/websockets.md +3 -0
  24. package/skills/void/docs/node_modules/void/CLAUDE.md +3 -1
  25. package/skills/void/docs/node_modules/void/skills/migrate-vite-cloudflare-to-void/SKILL.md +1 -1
  26. package/skills/void/docs/reference/cli.md +24 -0
  27. package/skills/void/docs/reference/config.md +15 -0
  28. package/skills/void/docs/reference/structure.md +1 -0
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-u7Rv9q_q.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-jBJT1fUT.mjs";
5
5
  import { i as resolveProjectPaths, n as readProjectPaths, t as assertNoRootSourceConventionConflicts } from "./project-paths-BQd7OmIo.mjs";
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";
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-ChWt4pX1.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-BkTvs43g.mjs";
9
+ import { c as getDatabaseDialect, f as readConfig, l as isNodeTarget, p as resolveBindingNames } from "./config-qHGgPuWT.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
13
  import { l as parseDotenvFile } from "./preset-BjyR3lzz.mjs";
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";
14
+ import { i as generateVoidAuthDrizzleSchema, n as generateQueueTypes, o as DRIZZLE_OPERATORS, r as generateVoidAuthDatabaseModule, s as generateDbDts, t as generateRouteTypes } from "./route-types-z1jtHEi_.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
18
  import { h as loadUserEnvFile } from "./env-validation-Dea3v3ej.mjs";
19
- import { i as resolveLayoutIds } from "./scan-DEwlM_Xy.mjs";
19
+ import { i as resolveLayoutIds, o as isRouteEnabledForEnvironment } from "./scan-NU4xKGci.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-nsHIFixA.mjs";
21
+ import { n as lintDuplicateSources, r as mergeRoutingRules, t as lintDestinationSplats } from "./headers-ChAADPQu.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-Da0UcsGA.mjs");
1690
+ const { runLocalVoidAuthMigrations } = await import("./node-pRg81HqV.mjs");
1691
1691
  await runLocalVoidAuthMigrations(root, dialect, persistRoot, databaseId, auth.configPath);
1692
1692
  voidLog("Applied Better Auth migrations");
1693
1693
  }
@@ -4252,6 +4252,7 @@ const RESOLVE_ID_FILTER = /^(virtual:void-(routes|client-worker(?::(react|vue|sv
4252
4252
  const LOAD_ID_FILTER = /^virtual:void-(routes|client-worker(?::(react|vue|svelte|solid))?|client-browser(?::(react|vue|svelte|solid))?)$/;
4253
4253
  function routingPlugin(root, options) {
4254
4254
  let scan = null;
4255
+ let routeEnvironment = "prod";
4255
4256
  let ssr = null;
4256
4257
  let authConfigPath = null;
4257
4258
  const paths = readProjectPaths(root);
@@ -4286,7 +4287,8 @@ function routingPlugin(root, options) {
4286
4287
  async function rescan() {
4287
4288
  scan = await scanRoutes(root, {
4288
4289
  output: options.output,
4289
- paths
4290
+ paths,
4291
+ environment: routeEnvironment
4290
4292
  });
4291
4293
  authConfigPath = findVoidAuthConfig(root, paths);
4292
4294
  const entries = validateSsrEntries(root, paths);
@@ -4493,6 +4495,7 @@ function routingPlugin(root, options) {
4493
4495
  }
4494
4496
  },
4495
4497
  config(_, env) {
4498
+ routeEnvironment = env.command === "serve" && !env.isPreview ? "dev" : "prod";
4496
4499
  if (env.command === "serve" && options.isAuthEnabled()) return { resolve: { alias: [
4497
4500
  "client",
4498
4501
  "react",
@@ -5063,6 +5066,7 @@ function voidPlugin(options) {
5063
5066
  const devTriggerToken = isViteBuildOrPreview() ? crypto.randomUUID() : getOrCreateDevTriggerToken(root);
5064
5067
  const config = readConfig(root);
5065
5068
  const paths = resolveProjectPaths(root, config);
5069
+ let routeEnvironment = "prod";
5066
5070
  assertNoRootSourceConventionConflicts(paths);
5067
5071
  const dialect = getDatabaseDialect(config);
5068
5072
  const detected = detectFramework(root);
@@ -5224,6 +5228,7 @@ function voidPlugin(options) {
5224
5228
  }
5225
5229
  const remoteMode = resolveRemoteMode(config.remote);
5226
5230
  const workerConfig = {};
5231
+ let assetRouting;
5227
5232
  if (config.worker?.vars) workerConfig.vars = { ...config.worker.vars };
5228
5233
  let viteCommand = "build";
5229
5234
  const wranglerCompat = isNodeTarget(config.target) ? void 0 : readWranglerCompat(root, config);
@@ -5231,17 +5236,18 @@ function voidPlugin(options) {
5231
5236
  workerConfig.compatibility_date = wranglerCompat.compatibilityDate;
5232
5237
  if (wranglerCompat.compatibilityFlags) workerConfig.compatibility_flags = wranglerCompat.compatibilityFlags;
5233
5238
  }
5239
+ if (config.worker?.limits?.cpu_ms) workerConfig.limits = { cpu_ms: config.worker.limits.cpu_ms };
5234
5240
  const inspectorPortEnv = process.env.VOID_INSPECTOR_PORT;
5235
5241
  const inspectorPort = inspectorPortEnv === "false" ? false : typeof inspectorPortEnv === "string" && inspectorPortEnv.length > 0 ? Number.parseInt(inspectorPortEnv, 10) : void 0;
5236
5242
  const inspectorPortOption = inspectorPort === false ? { inspectorPort: false } : typeof inspectorPort === "number" ? { inspectorPort } : {};
5237
5243
  const outDir = join(root, ".void");
5238
5244
  mkdirSync(outDir, { recursive: true });
5239
5245
  function mergeBindings(resolved) {
5246
+ const environmentWebsocketRoutes = websocketRoutes.filter((route) => isRouteEnabledForEnvironment(route, routeEnvironment));
5240
5247
  const result = { ...workerConfig };
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;
5248
+ if (workerConfig.assets && resolved.assets) {
5249
+ delete resolved.assets.not_found_handling;
5250
+ delete resolved.assets.run_worker_first;
5245
5251
  }
5246
5252
  const d1Name = bindingNames?.d1 ?? "DB";
5247
5253
  const d1 = [];
@@ -5282,16 +5288,16 @@ function voidPlugin(options) {
5282
5288
  ...q.retryDelay != null && { retry_delay: q.retryDelay }
5283
5289
  }))
5284
5290
  };
5285
- if (websocketRoutes.length > 0) {
5291
+ if (environmentWebsocketRoutes.length > 0) {
5286
5292
  const existingDurableObjectBindings = resolved.durable_objects?.bindings ?? [];
5287
- const missingDurableObjectBindings = websocketRoutes.filter((route) => !existingDurableObjectBindings.some((binding) => binding.name === route.bindingName)).map((route) => ({
5293
+ const missingDurableObjectBindings = environmentWebsocketRoutes.filter((route) => !existingDurableObjectBindings.some((binding) => binding.name === route.bindingName)).map((route) => ({
5288
5294
  name: route.bindingName,
5289
5295
  class_name: route.className
5290
5296
  }));
5291
5297
  if (missingDurableObjectBindings.length > 0) result.durable_objects = { bindings: [...existingDurableObjectBindings, ...missingDurableObjectBindings] };
5292
5298
  if (!Array.isArray(resolved.migrations)) result.migrations = [{
5293
5299
  tag: "void-ws-v1",
5294
- new_classes: websocketRoutes.map((route) => route.className)
5300
+ new_classes: environmentWebsocketRoutes.map((route) => route.className)
5295
5301
  }];
5296
5302
  }
5297
5303
  if (bindings.needsSandbox && sandboxConfig) {
@@ -5361,8 +5367,13 @@ function voidPlugin(options) {
5361
5367
  const envPlugin = {
5362
5368
  name: "void:env",
5363
5369
  api: { getVoidDeployBuildConfig: () => voidDeployBuildConfig },
5364
- config(userConfig, { mode, command }) {
5370
+ config(userConfig, { mode, command, isPreview }) {
5365
5371
  viteCommand = command;
5372
+ routeEnvironment = command === "serve" && !isPreview ? "dev" : "prod";
5373
+ if (!framework && !isNodeTarget(config.target)) {
5374
+ assetRouting = resolveConfiguredAssetRoutingPolicy();
5375
+ applyAssetRoutingPolicy(assetRouting);
5376
+ }
5366
5377
  const ignored = ["**/.void/**"];
5367
5378
  const rawEnvDir = typeof userConfig.envDir === "string" && userConfig.envDir.length > 0 ? userConfig.envDir : void 0;
5368
5379
  voidDeployBuildConfig = {
@@ -5376,7 +5387,7 @@ function voidPlugin(options) {
5376
5387
  ...env
5377
5388
  };
5378
5389
  const assets = workerConfig.assets;
5379
- if (command === "serve" && assets?.run_worker_first?.includes("/**")) assets.run_worker_first = [...DEV_ASSET_ROUTING_EXCLUSIONS, "/**"];
5390
+ if (command === "serve" && !isPreview && assets?.run_worker_first?.includes("/**")) assets.run_worker_first = [...DEV_ASSET_ROUTING_EXCLUSIONS, "/**"];
5380
5391
  const watchConfig = { server: { watch: { ignored } } };
5381
5392
  if (!isNodeTarget(config.target) && config.output !== "static") {
5382
5393
  const envNames = framework === "vinext-app" ? ["rsc", "ssr"] : ["ssr"];
@@ -5478,18 +5489,15 @@ function voidPlugin(options) {
5478
5489
  };
5479
5490
  if (framework) {
5480
5491
  workerConfig.main = getFrameworkEntry(framework, root, devTriggerToken, config.routing?.notFound);
5492
+ let notFoundIgnoredWarned = false;
5481
5493
  const frameworkBuildWrapperPlugin = {
5482
5494
  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
- },
5491
5495
  configResolved(resolved) {
5492
5496
  if (resolved.command !== "build") return;
5497
+ if (config.routing?.notFound !== void 0 && !process.env.VOID_DEPLOY_BUILD && !notFoundIgnoredWarned) {
5498
+ notFoundIgnoredWarned = true;
5499
+ voidWarn(`Void generates no asset routing policy for a ${framework} build, so "routing.notFound" in void.json is not written to the built worker config. On a self-hosted \`vite build && wrangler deploy\` it takes effect only if your own wrangler config declares a complete \`assets\` policy (\`binding\`, \`directory\`, \`not_found_handling\`, \`run_worker_first\`); otherwise Cloudflare's default applies. Managed \`void deploy\` supplies the policy for you.`);
5500
+ }
5493
5501
  generateClassATriggerWrapper(root, framework, getOriginalFrameworkEntry(framework, root), true, resolved.build.assetsDir, config.routing?.notFound);
5494
5502
  }
5495
5503
  };
@@ -5580,20 +5588,22 @@ function voidPlugin(options) {
5580
5588
  const ssrEntry = validateSsrEntry(root, paths);
5581
5589
  const hasPages = existsSync(paths.pagesDir);
5582
5590
  const hasMiddleware = scanMiddlewareSync(root, paths).length > 0;
5583
- const routePatterns = scanRoutePatternsSync(root, paths);
5584
- const hasDocumentRoutes$1 = hasDocumentRoutes(routePatterns);
5585
- const hasApiRoutes$1 = hasApiRoutes(routePatterns) || hasApiRoutes(websocketRoutes) || authEnabled;
5586
- let assetRouting = resolveAssetRoutingPolicy({
5587
- isSsr: Boolean(ssrEntry),
5588
- hasPages,
5589
- hasApiRoutes: hasApiRoutes$1,
5590
- hasDocumentRoutes: hasDocumentRoutes$1,
5591
- hasMiddleware,
5592
- hasWebSockets: hasDocumentRoutes(websocketRoutes),
5593
- authEnabled,
5594
- liveEnabled: liveConfig !== void 0,
5595
- notFound: config.routing?.notFound
5596
- });
5591
+ function resolveConfiguredAssetRoutingPolicy() {
5592
+ const routePatterns = scanRoutePatternsSync(root, paths, routeEnvironment);
5593
+ const environmentWebsocketRoutes = websocketRoutes.filter((route) => isRouteEnabledForEnvironment(route, routeEnvironment));
5594
+ return resolveAssetRoutingPolicy({
5595
+ isSsr: Boolean(ssrEntry),
5596
+ hasPages,
5597
+ hasApiRoutes: hasApiRoutes(routePatterns) || hasApiRoutes(environmentWebsocketRoutes) || authEnabled,
5598
+ hasDocumentRoutes: hasDocumentRoutes(routePatterns),
5599
+ hasMiddleware,
5600
+ hasWebSockets: hasDocumentRoutes(environmentWebsocketRoutes),
5601
+ authEnabled,
5602
+ liveEnabled: liveConfig !== void 0,
5603
+ notFound: config.routing?.notFound
5604
+ });
5605
+ }
5606
+ assetRouting = resolveConfiguredAssetRoutingPolicy();
5597
5607
  workerConfig.main = join(root, ".void", "entry.ts");
5598
5608
  if (jobs.length > 0) workerConfig.triggers = { crons: jobs.flatMap((job) => job.crons) };
5599
5609
  function applyAssetRoutingPolicy(policy) {
@@ -3,7 +3,7 @@ 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-BkTvs43g.mjs";
6
+ import { c as getDatabaseDialect, f as readConfig } from "./config-qHGgPuWT.mjs";
7
7
  import { r as detectFramework } from "./plugin-inference-DMeavIJ6.mjs";
8
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";
@@ -1,7 +1,7 @@
1
1
  import { a as join } from "./dist-DaKKDf8D.mjs";
2
- import { c as loadVoidAuthConfig, s as findVoidAuthConfig } from "./scan-DYXkrasO.mjs";
3
- import { f as readConfig } from "./config-BkTvs43g.mjs";
4
- import { a as resolveVoidAuthMigrationSchema } from "./route-types-COI2DsZv.mjs";
2
+ import { c as loadVoidAuthConfig, s as findVoidAuthConfig } from "./scan-ChWt4pX1.mjs";
3
+ import { f as readConfig } from "./config-qHGgPuWT.mjs";
4
+ import { a as resolveVoidAuthMigrationSchema } from "./route-types-z1jtHEi_.mjs";
5
5
  import { n as applySqliteAuthMigrations, t as applyPgAuthMigrations } from "./auth-migrations-BTZ-ATvQ.mjs";
6
6
  import { getMiniflareDatabasePath } from "./runner-kapo9aPs.mjs";
7
7
  import { loadEnv } from "vite";
@@ -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-BkTvs43g.mjs";
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-nsHIFixA.mjs";
3
+ import { f as readConfig } from "../config-qHGgPuWT.mjs";
4
+ import { a as resolveNamedLayout, i as resolveLayoutIds, n as resolveEffectiveLayoutChain, r as resolveLayoutChain, t as scanPages } from "../scan-NU4xKGci.mjs";
5
+ import { i as parseRedirectsFile } from "../headers-ChAADPQu.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
@@ -1,5 +1,5 @@
1
1
  import { c as resolve, n as dirname } from "../dist-DaKKDf8D.mjs";
2
- import { n as resolveEffectiveLayoutChain, t as scanPages } from "../scan-DEwlM_Xy.mjs";
2
+ import { n as resolveEffectiveLayoutChain, t as scanPages } from "../scan-NU4xKGci.mjs";
3
3
  import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { normalizePath } from "vite";
5
5
  //#region src/pages/islands-plugin.ts
@@ -1,11 +1,11 @@
1
1
  import { a as join } from "./dist-DaKKDf8D.mjs";
2
2
  import { D as outro, E as note, T as log, a as cliTitle, w as intro } from "./agents-CtgBYqld.mjs";
3
3
  import { i as resolveProjectPaths } from "./project-paths-BQd7OmIo.mjs";
4
- import { c as loadVoidAuthConfig, r as scanRoutes, s as findVoidAuthConfig } from "./scan-DYXkrasO.mjs";
4
+ import { c as loadVoidAuthConfig, r as scanRoutes, s as findVoidAuthConfig } from "./scan-ChWt4pX1.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-BkTvs43g.mjs";
6
+ import { c as getDatabaseDialect, f as readConfig } from "./config-qHGgPuWT.mjs";
7
7
  import { i as inferProjectBindings, n as FRAMEWORK_SCAN_DIRS, r as detectFramework } from "./plugin-inference-DMeavIJ6.mjs";
8
- import { i as generateVoidAuthDrizzleSchema, n as generateQueueTypes, r as generateVoidAuthDatabaseModule, s as generateDbDts, t as generateRouteTypes } from "./route-types-COI2DsZv.mjs";
8
+ import { i as generateVoidAuthDrizzleSchema, n as generateQueueTypes, r as generateVoidAuthDatabaseModule, s as generateDbDts, t as generateRouteTypes } from "./route-types-z1jtHEi_.mjs";
9
9
  import { t as discoverSchema } from "./discover-CJHyvYfR.mjs";
10
10
  import { r as generateEnvTypes, t as findEnvFile } from "./env-types-QBj-ndax.mjs";
11
11
  import { existsSync, mkdirSync, rmSync } from "node:fs";
@@ -1,5 +1,5 @@
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-u7Rv9q_q.mjs";
2
+ import { b as readProvisionedBindingEntry, f as assertNoDuplicateMembers, w as writeProvisionedWranglerConfig } from "./deploy-jBJT1fUT.mjs";
3
3
  import { i as voidWarn, r as voidLog } from "./log-ChfPKsVd.mjs";
4
4
  import { o as resolveWranglerArgv, u as scanWranglerDotenvKeys } from "./preset-BjyR3lzz.mjs";
5
5
  import { createRequire } from "node:module";
@@ -283,7 +283,14 @@ function generateVoidAuthDatabaseModule(options) {
283
283
  `import { db as __voidDb } from "void/db";`,
284
284
  `import { schema as __voidAuthSchema } from ${schemaImport};`,
285
285
  "",
286
- `export const __voidAuthDatabase = drizzleAdapter(__voidDb, { provider: ${JSON.stringify(provider)}, schema: __voidAuthSchema });`,
286
+ "// Better Auth reads Drizzle metadata while creating the adapter.",
287
+ "// Defer that work until request handling, when runtime bindings are available.",
288
+ "let __voidAuthDatabaseInstance: ReturnType<typeof drizzleAdapter> | undefined;",
289
+ "",
290
+ "export const __voidAuthDatabase: ReturnType<typeof drizzleAdapter> = (options) => {",
291
+ ` __voidAuthDatabaseInstance ??= drizzleAdapter(__voidDb, { provider: ${JSON.stringify(provider)}, schema: __voidAuthSchema });`,
292
+ " return __voidAuthDatabaseInstance(options);",
293
+ "};",
287
294
  ""
288
295
  ].join("\n");
289
296
  }
@@ -2,7 +2,7 @@ import { a as join, i as isAbsolute, t as basename } from "./dist-DaKKDf8D.mjs";
2
2
  import { n as readProjectPaths } from "./project-paths-BQd7OmIo.mjs";
3
3
  import { n as globSync, t as glob } from "./dist-BuiRJkTd.mjs";
4
4
  import { c as isIdentifier, d as isObjectExpression, l as isLiteral, s as isCallExpression } from "./entry-D7yy4xVH.mjs";
5
- import { o as parseRouteFilename, s as parseWebSocketFilename, t as scanPages } from "./scan-DEwlM_Xy.mjs";
5
+ import { c as parseWebSocketFilename, o as isRouteEnabledForEnvironment, s as parseRouteFilename, t as scanPages } from "./scan-NU4xKGci.mjs";
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { parseSync } from "vite";
@@ -261,19 +261,19 @@ function buildWebSocketRoute(root, filePath, paths = readProjectPaths(root)) {
261
261
  hasAuthGate: info?.hasAuthGate ?? false
262
262
  };
263
263
  }
264
- function parseWebSocketFiles(root, files, paths = readProjectPaths(root)) {
265
- return files.filter((f) => isWebSocketRouteFile(f)).filter((f) => !f.split("/").some((segment) => segment.startsWith("_"))).map((filePath) => buildWebSocketRoute(root, filePath, paths)).sort((a, b) => {
264
+ function parseWebSocketFiles(root, files, paths = readProjectPaths(root), environment) {
265
+ return files.filter((f) => isWebSocketRouteFile(f)).filter((f) => !f.split("/").some((segment) => segment.startsWith("_"))).filter((filePath) => isRouteEnabledForEnvironment(parseWebSocketFilename(filePath), environment)).map((filePath) => buildWebSocketRoute(root, filePath, paths)).sort((a, b) => {
266
266
  if (a.catchAll !== b.catchAll) return a.catchAll ? 1 : -1;
267
267
  return a.pattern.localeCompare(b.pattern);
268
268
  });
269
269
  }
270
- async function scanWebSocketRoutes(root, paths = readProjectPaths(root)) {
270
+ async function scanWebSocketRoutes(root, paths = readProjectPaths(root), environment) {
271
271
  return parseWebSocketFiles(root, await glob(`**/*.{${HANDLER_EXTENSIONS.join(",")}}`, {
272
272
  cwd: paths.routesDir,
273
273
  absolute: false
274
- }).catch(() => []), paths);
274
+ }).catch(() => []), paths, environment);
275
275
  }
276
- function scanWebSocketRoutesSync(root, paths = readProjectPaths(root)) {
276
+ function scanWebSocketRoutesSync(root, paths = readProjectPaths(root), environment) {
277
277
  const pattern = `**/*.{${HANDLER_EXTENSIONS.join(",")}}`;
278
278
  let files;
279
279
  try {
@@ -284,7 +284,7 @@ function scanWebSocketRoutesSync(root, paths = readProjectPaths(root)) {
284
284
  } catch {
285
285
  files = [];
286
286
  }
287
- return parseWebSocketFiles(root, files, paths);
287
+ return parseWebSocketFiles(root, files, paths, environment);
288
288
  }
289
289
  function isWebSocketPatternFile(filePath) {
290
290
  return isWebSocketRouteFile(filePath);
@@ -329,7 +329,7 @@ async function scanRoutes(root, options) {
329
329
  cwd: paths.routesDir,
330
330
  absolute: false
331
331
  }),
332
- scanWebSocketRoutes(root, paths),
332
+ scanWebSocketRoutes(root, paths, options?.environment),
333
333
  glob(ROUTE_PATTERN, {
334
334
  cwd: paths.middlewareDir,
335
335
  absolute: false
@@ -343,9 +343,8 @@ async function scanRoutes(root, options) {
343
343
  ]);
344
344
  const routesDir = paths.routesDir;
345
345
  return {
346
- routes: routeFiles.filter((f) => !isWebSocketPatternFile(f)).filter((f) => !f.split("/").some((s) => s.startsWith("_"))).map((f) => {
347
- const route = parseRouteFilename(f);
348
- route.methods = detectHttpExports(join(routesDir, f));
346
+ routes: routeFiles.filter((f) => !isWebSocketPatternFile(f)).filter((f) => !f.split("/").some((s) => s.startsWith("_"))).map((file) => parseRouteFilename(file)).filter((route) => isRouteEnabledForEnvironment(route, options?.environment)).map((route) => {
347
+ route.methods = detectHttpExports(join(routesDir, route.filePath));
349
348
  return route;
350
349
  }).sort(compareRouteSpecificity),
351
350
  websockets,
@@ -367,7 +366,7 @@ function scanMiddlewareSync(root, paths = readProjectPaths(root)) {
367
366
  }
368
367
  return parseMiddlewareDefinitions(middlewareFiles);
369
368
  }
370
- function scanRoutePatternsSync(root, paths = readProjectPaths(root)) {
369
+ function scanRoutePatternsSync(root, paths = readProjectPaths(root), environment) {
371
370
  let routeFiles;
372
371
  try {
373
372
  routeFiles = globSync(ROUTE_PATTERN, {
@@ -377,7 +376,7 @@ function scanRoutePatternsSync(root, paths = readProjectPaths(root)) {
377
376
  } catch {
378
377
  routeFiles = [];
379
378
  }
380
- return routeFiles.filter((f) => !isWebSocketPatternFile(f)).filter((f) => !f.split("/").some((s) => s.startsWith("_"))).map((f) => parseRouteFilename(f)).sort(compareRouteSpecificity);
379
+ return routeFiles.filter((f) => !isWebSocketPatternFile(f)).filter((f) => !f.split("/").some((s) => s.startsWith("_"))).map((f) => parseRouteFilename(f)).filter((route) => isRouteEnabledForEnvironment(route, environment)).sort(compareRouteSpecificity);
381
380
  }
382
381
  //#endregion
383
382
  export { scanQueuesSync as a, loadVoidAuthConfig as c, scanWebSocketRoutesSync as i, scanRoutePatternsSync as n, scanJobsSync as o, scanRoutes as r, findVoidAuthConfig as s, scanMiddlewareSync as t };
@@ -74,6 +74,9 @@ function parseRouteFilename(relativePath) {
74
74
  function parseWebSocketFilename(relativePath) {
75
75
  return parsePath(relativePath, true);
76
76
  }
77
+ function isRouteEnabledForEnvironment(route, environment) {
78
+ return environment === void 0 || route.env === null || route.env === environment;
79
+ }
77
80
  //#endregion
78
81
  //#region src/pages/layout-resolution.ts
79
82
  function isNamedLayout(layout) {
@@ -348,4 +351,4 @@ function extractFrontmatterLayout(source) {
348
351
  return value;
349
352
  }
350
353
  //#endregion
351
- export { resolveNamedLayout as a, resolveLayoutIds as i, resolveEffectiveLayoutChain as n, parseRouteFilename as o, resolveLayoutChain as r, parseWebSocketFilename as s, scanPages as t };
354
+ export { resolveNamedLayout as a, parseWebSocketFilename as c, resolveLayoutIds as i, resolveEffectiveLayoutChain as n, isRouteEnabledForEnvironment as o, resolveLayoutChain as r, parseRouteFilename as s, scanPages as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "void",
3
- "version": "0.10.12",
3
+ "version": "0.10.13",
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.12"
359
+ "@void/md": "0.10.13"
360
360
  },
361
361
  "peerDependenciesMeta": {
362
362
  "@void/md": {
package/schema.json CHANGED
@@ -360,6 +360,19 @@
360
360
  "type": "object",
361
361
  "description": "Environment variables (plain text, not secrets)",
362
362
  "additionalProperties": { "type": "string" }
363
+ },
364
+ "limits": {
365
+ "type": "object",
366
+ "description": "Worker resource limit overrides.",
367
+ "properties": {
368
+ "cpu_ms": {
369
+ "type": "integer",
370
+ "minimum": 1,
371
+ "maximum": 300000,
372
+ "description": "Caps Workers CPU time per request, in milliseconds (max 300000 = 5 minutes). The platform plan may cap it lower."
373
+ }
374
+ },
375
+ "additionalProperties": false
363
376
  }
364
377
  },
365
378
  "additionalProperties": false
@@ -103,7 +103,7 @@ Apply these filename rules exactly when mapping old handlers to `routes/`:
103
103
  1. Extension and suffix parsing order
104
104
 
105
105
  - Strip extension (`.ts`, `.js`, `.mts`, `.mjs`).
106
- - Strip env suffix (`.dev`, `.prod`).
106
+ - Strip env suffix (`.dev`, `.prod`). The suffix restricts the route to that environment, so only use it for handlers that must not ship to the other one.
107
107
  - Strip HTTP method suffix (`.get`, `.post`, `.put`, `.delete`, `.patch`).
108
108
  - Strip trailing `index` segment.
109
109
  - Remove route group segments `(group-name)` from URL path.
@@ -62,10 +62,77 @@ If your CI pipeline runs typechecking or other static analysis before deploy, ru
62
62
  | `VOID_TOKEN` | Auth token (for CI, skips OAuth) |
63
63
  | `VOID_PROJECT` | Project slug override |
64
64
 
65
- ## GitHub Actions
65
+ ## GitHub
66
66
 
67
67
  You can deploy from a GitHub repo on every push to `main`. Running `void init --github` generates `.github/workflows/void-deploy.yml` in your project.
68
68
 
69
+ ### Void GitHub App (recommended)
70
+
71
+ No `.github/workflows/void-deploy.yml` or repository `VOID_TOKEN` is required.
72
+
73
+ 1. Initialize your Void project
74
+
75
+ ```bash
76
+ void init
77
+ # Or, for existing Void project:
78
+ void project link
79
+ ```
80
+
81
+ 2. Install the Void GitHub app
82
+
83
+ ```bash
84
+ void github install
85
+ ```
86
+
87
+ You should receive GitHub url asking you to install and authorize `Void Deploy`.
88
+
89
+ In the browser, select the GitHub account/organization and grant access to the repository.
90
+
91
+ :::details If someone already installed the App for your organization
92
+ Join that installation instead:
93
+
94
+ ```bash
95
+ void github join
96
+ ```
97
+
98
+ :::
99
+
100
+ 3. Link your repository to your project
101
+
102
+ ```bash
103
+ void github connect --executor container
104
+ ```
105
+
106
+ 4. Verify the connection
107
+
108
+ ```bash
109
+ void github status
110
+ ```
111
+
112
+ You should receive an output like:
113
+
114
+ ```bash
115
+ Repository <owner/repository>
116
+ Branch main
117
+ Build executor container
118
+ Deploy workflow .github/workflows/void-deploy.yml (unused for container builds)
119
+
120
+ ```
121
+
122
+ 5. Push to the configured branch to trigger a new deploy
123
+
124
+ ```bash
125
+ git push origin main
126
+ ```
127
+
128
+ 6. Follow the build
129
+
130
+ ```bash
131
+ void build logs --follow
132
+ ```
133
+
134
+ ### GitHub Actions
135
+
69
136
  The workflow authenticates with [GitHub OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) — there is **no long-lived `VOID_TOKEN` secret to store or rotate**. `void deploy` detects GitHub Actions and does the exchange itself: it requests a short-lived OIDC token from GitHub (audience `void`), exchanges it at `POST $VOID_API_URL/auth/github-oidc` for a short-lived project-scoped deploy token (about 15 minutes on the free tier; longer on higher plans, capped at ~60 minutes), and deploys — so the workflow is a single `void deploy` step. `permissions: id-token: write` is still required: it is what lets the job (and the CLI) mint the OIDC token.
70
137
 
71
138
  ```yaml
@@ -118,11 +118,24 @@ The SPA fallback is right for a single-page app, where deep links must boot the
118
118
 
119
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
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.
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.
122
+
123
+ TanStack Start, React Router, and vinext follow the rows above on a managed `void deploy` — that path resolves the asset config itself and applies it to the uploaded worker. Void writes no `assets` policy into their generated worker wrangler config:
124
+
125
+ | Framework | Generated worker config |
126
+ | -------------- | ---------------------------- |
127
+ | TanStack Start | `dist/server/wrangler.json` |
128
+ | React Router | `build/server/wrangler.json` |
129
+ | vinext (App) | `dist/server/wrangler.json` |
130
+ | vinext (Pages) | `dist/ssr/wrangler.json` |
131
+
132
+ So on a self-hosted `wrangler deploy` the setting takes effect only if that config declares a complete `assets` policy of its own — `binding`, `directory`, `not_found_handling`, and `run_worker_first`. Void leaves those fields alone, so a policy you write yourself is honored by the generated wrapper; with no policy at all, Cloudflare's default applies. `vite build` warns when `routing.notFound` is set so the choice is not silent.
122
133
 
123
134
  ### Generated config
124
135
 
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.
136
+ Void owns the generated asset routing policy during dev and build for Void apps. 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.
137
+
138
+ TanStack Start and React Router are the exception: Void generates no asset policy for them and leaves both fields to your own wrangler config. Writing `not_found_handling` alone would make the asset layer answer unmatched requests and the framework worker would never run, and completing the policy needs `assets.binding` and `assets.directory` that the framework owns, not Void.
126
139
 
127
140
  ## API routes and SSR pages
128
141
 
@@ -14,7 +14,27 @@ Create route handlers in `routes/**/*.ts`. Each file maps to a URL path based on
14
14
 
15
15
  Dynamic segments use brackets: `[id]` becomes a route parameter and `[...slug]` becomes a catch-all. Files or directories starting with `_` are ignored. Directories wrapped in parentheses like `(admin)` are route groups. They help organize files without changing the URL.
16
16
 
17
- You can add a `.dev` or `.prod` suffix before the extension (e.g. `debug.dev.ts`) to include a route only in that environment.
17
+ ### Environment-only routes
18
+
19
+ Add a `.dev` or `.prod` suffix before the extension to include a route in only one environment:
20
+
21
+ ```
22
+ routes/
23
+ api/health.ts → always
24
+ api/debug.dev.ts → development only
25
+ api/metrics.prod.ts → production only
26
+ ```
27
+
28
+ `dev` is the Vite dev server. `prod` is every build, `void deploy`, and `vite preview` — preview serves a production build, so it uses the production routes.
29
+
30
+ An excluded route is stripped from the worker bundle, from the generated types, and from the generated route and WebSocket config. It is not compiled and it is not deployed.
31
+
32
+ Two things do not follow the suffix:
33
+
34
+ - `void prepare` boots no Vite, so it has no environment to read. It generates types for every route in both environments.
35
+ - [Binding inference](../reference/resource-inference.md) scans your source for imports and does not read the suffix. A `void/storage` import inside `api/debug.dev.ts` still adds an R2 binding to your production config, and `void deploy` still provisions the bucket. Set the binding explicitly with [`inference.bindings`](../reference/config.md#inference-bindings) if you do not want it.
36
+
37
+ The suffix applies to files in `routes/` only. It has no effect in `pages/`, `middleware/`, `crons/`, or `queues/`.
18
38
 
19
39
  Each file exports named HTTP method constants to handle specific methods:
20
40
 
@@ -28,6 +28,9 @@ Filename rules match regular server routes:
28
28
  - `[id].ws.ts` becomes `:id`
29
29
  - `[...slug].ws.ts` becomes a catch-all
30
30
  - route groups like `(marketing)/chat.ws.ts` are ignored in the URL
31
+ - `chat.dev.ws.ts` / `chat.prod.ws.ts` restrict the route to one environment
32
+
33
+ The environment suffix goes **before** `.ws`. `chat.ws.dev.ts` is not recognised.
31
34
 
32
35
  ## `defineRoom()`
33
36
 
@@ -144,7 +144,7 @@ Build output uses `.mjs` / `.d.mts` extensions (`vp pack` / tsdown default).
144
144
  Applied in order to a path relative to `routes/`:
145
145
 
146
146
  1. Strip extension (`.ts`, `.js`, `.mts`, `.mjs`)
147
- 2. Strip env suffix (`.dev`, `.prod`)
147
+ 2. Strip env suffix (`.dev`, `.prod`) and record it on `RouteDefinition.env`
148
148
  3. Strip trailing `index`
149
149
  4. Remove route group dirs `(groupname)`
150
150
  5. Convert `[param]` → `:param`, `[...param]` → `:param{.+}`, `[...]` → `*`
@@ -152,6 +152,8 @@ Applied in order to a path relative to `routes/`:
152
152
 
153
153
  HTTP methods are determined by **named exports** in each file (detected by `detect-exports.ts` via `parseSync`), not by filename suffixes. Files/dirs starting with `_` are ignored by the scanner.
154
154
 
155
+ The recorded `env` is enforced by `isRouteEnabledForEnvironment()`. The scanners take an optional `environment` (`'dev' | 'prod'`); when it is omitted no filtering happens. `dev` is the Vite dev server, `prod` is every build, `void deploy`, and `vite preview`. A dev-only route is absent from the production bundle, types, and generated route/WebSocket config. Two paths do not follow it: `void prepare` boots no Vite, passes no `environment`, and so generates types for both environments; and `inferProjectBindings()` (`plugin-inference.ts`) globs source files without parsing route filenames, so a `void/storage` import in a `.dev.ts` route still emits and provisions an R2 binding in production. Over-emitting is the deliberate safe direction — under-emitting would drop a binding the running code needs — and the environment is not knowable at the `inferProjectBindings()` call site anyway (`index.ts:678`, versus `routeEnvironment` at `index.ts:1324`). The suffix is honoured in `routes/` only, not in `pages/`, `middleware/`, `crons/`, or `queues/`.
156
+
155
157
  ## Code Generation (`void gen`)
156
158
 
157
159
  Rails/Laravel-style scaffolding commands for common boilerplate:
@@ -103,7 +103,7 @@ Apply these filename rules exactly when mapping old handlers to `routes/`:
103
103
  1. Extension and suffix parsing order
104
104
 
105
105
  - Strip extension (`.ts`, `.js`, `.mts`, `.mjs`).
106
- - Strip env suffix (`.dev`, `.prod`).
106
+ - Strip env suffix (`.dev`, `.prod`). The suffix restricts the route to that environment, so only use it for handlers that must not ship to the other one.
107
107
  - Strip HTTP method suffix (`.get`, `.post`, `.put`, `.delete`, `.patch`).
108
108
  - Strip trailing `index` segment.
109
109
  - Remove route group segments `(group-name)` from URL path.