@timber-js/app 0.2.0-alpha.97 → 0.2.0-alpha.98

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 (102) hide show
  1. package/dist/_chunks/{metadata-routes-DS3eKNmf.js → metadata-routes-BU684ls2.js} +1 -1
  2. package/dist/_chunks/{metadata-routes-DS3eKNmf.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  3. package/dist/_chunks/segment-classify-BjfuctV2.js +137 -0
  4. package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -0
  5. package/dist/_chunks/{interception-BbqMCVXa.js → walkers-VOXgavMF.js} +61 -85
  6. package/dist/_chunks/walkers-VOXgavMF.js.map +1 -0
  7. package/dist/adapters/nitro.d.ts.map +1 -1
  8. package/dist/adapters/nitro.js +55 -5
  9. package/dist/adapters/nitro.js.map +1 -1
  10. package/dist/client/index.js +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +189 -62
  13. package/dist/index.js.map +1 -1
  14. package/dist/plugins/build-report.d.ts +6 -4
  15. package/dist/plugins/build-report.d.ts.map +1 -1
  16. package/dist/plugins/dev-404-page.d.ts +8 -18
  17. package/dist/plugins/dev-404-page.d.ts.map +1 -1
  18. package/dist/routing/index.d.ts +5 -3
  19. package/dist/routing/index.d.ts.map +1 -1
  20. package/dist/routing/index.js +3 -3
  21. package/dist/routing/scanner.d.ts +1 -10
  22. package/dist/routing/scanner.d.ts.map +1 -1
  23. package/dist/routing/segment-classify.d.ts +37 -8
  24. package/dist/routing/segment-classify.d.ts.map +1 -1
  25. package/dist/routing/types.d.ts +63 -23
  26. package/dist/routing/types.d.ts.map +1 -1
  27. package/dist/routing/walkers.d.ts +51 -0
  28. package/dist/routing/walkers.d.ts.map +1 -0
  29. package/dist/server/action-handler.d.ts.map +1 -1
  30. package/dist/server/dev-holding-server.d.ts +4 -2
  31. package/dist/server/dev-holding-server.d.ts.map +1 -1
  32. package/dist/server/html-injector-core.d.ts +212 -0
  33. package/dist/server/html-injector-core.d.ts.map +1 -0
  34. package/dist/server/html-injectors.d.ts +59 -59
  35. package/dist/server/html-injectors.d.ts.map +1 -1
  36. package/dist/server/internal.js +710 -563
  37. package/dist/server/internal.js.map +1 -1
  38. package/dist/server/node-stream-transforms.d.ts +46 -49
  39. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  40. package/dist/server/pipeline-helpers.d.ts +88 -0
  41. package/dist/server/pipeline-helpers.d.ts.map +1 -0
  42. package/dist/server/pipeline-phases.d.ts +97 -0
  43. package/dist/server/pipeline-phases.d.ts.map +1 -0
  44. package/dist/server/pipeline.d.ts +53 -32
  45. package/dist/server/pipeline.d.ts.map +1 -1
  46. package/dist/server/port-resolution.d.ts +117 -0
  47. package/dist/server/port-resolution.d.ts.map +1 -0
  48. package/dist/server/route-matcher.d.ts +20 -47
  49. package/dist/server/route-matcher.d.ts.map +1 -1
  50. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  51. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +74 -0
  52. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
  53. package/dist/server/status-code-resolver.d.ts +16 -11
  54. package/dist/server/status-code-resolver.d.ts.map +1 -1
  55. package/dist/server/tree-builder.d.ts.map +1 -1
  56. package/dist/utils/directive-parser.d.ts +0 -45
  57. package/dist/utils/directive-parser.d.ts.map +1 -1
  58. package/package.json +7 -6
  59. package/src/adapters/nitro.ts +55 -5
  60. package/src/cli.ts +0 -0
  61. package/src/index.ts +84 -31
  62. package/src/plugins/build-report.ts +13 -22
  63. package/src/plugins/dev-404-page.ts +15 -41
  64. package/src/plugins/routing.ts +14 -12
  65. package/src/routing/codegen.ts +1 -1
  66. package/src/routing/convention-lint.ts +4 -4
  67. package/src/routing/index.ts +5 -3
  68. package/src/routing/interception.ts +1 -1
  69. package/src/routing/scanner.ts +17 -93
  70. package/src/routing/segment-classify.ts +107 -8
  71. package/src/routing/status-file-lint.ts +3 -3
  72. package/src/routing/types.ts +63 -23
  73. package/src/routing/walkers.ts +90 -0
  74. package/src/server/action-handler.ts +6 -0
  75. package/src/server/deny-renderer.ts +5 -5
  76. package/src/server/dev-holding-server.ts +4 -2
  77. package/src/server/fallback-error.ts +1 -1
  78. package/src/server/html-injector-core.ts +403 -0
  79. package/src/server/html-injectors.ts +158 -297
  80. package/src/server/node-stream-transforms.ts +108 -248
  81. package/src/server/pipeline-helpers.ts +180 -0
  82. package/src/server/pipeline-phases.ts +591 -0
  83. package/src/server/pipeline.ts +76 -539
  84. package/src/server/port-resolution.ts +215 -0
  85. package/src/server/route-element-builder.ts +1 -1
  86. package/src/server/route-matcher.ts +28 -60
  87. package/src/server/rsc-entry/api-handler.ts +2 -2
  88. package/src/server/rsc-entry/error-renderer.ts +1 -1
  89. package/src/server/rsc-entry/index.ts +52 -98
  90. package/src/server/rsc-entry/wrap-action-dispatch.ts +156 -0
  91. package/src/server/sitemap-generator.ts +1 -1
  92. package/src/server/slot-resolver.ts +1 -1
  93. package/src/server/status-code-resolver.ts +112 -128
  94. package/src/server/tree-builder.ts +6 -4
  95. package/src/utils/directive-parser.ts +0 -392
  96. package/LICENSE +0 -8
  97. package/dist/_chunks/interception-BbqMCVXa.js.map +0 -1
  98. package/dist/_chunks/segment-classify-BDNn6EzD.js +0 -65
  99. package/dist/_chunks/segment-classify-BDNn6EzD.js.map +0 -1
  100. package/dist/server/manifest-status-resolver.d.ts +0 -58
  101. package/dist/server/manifest-status-resolver.d.ts.map +0 -1
  102. package/src/server/manifest-status-resolver.ts +0 -215
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM, r as __exportAll, t as __commonJSMin } from "./_chunks/chunk-BYIpzuS7.js";
2
2
  import { n as setViteServer } from "./_chunks/dev-warnings-DpGRGoDi.js";
3
- import { i as scanRoutes, n as generateRouteMap, t as collectInterceptionRewrites } from "./_chunks/interception-BbqMCVXa.js";
3
+ import { i as scanRoutes, n as collectInterceptionRewrites, r as generateRouteMap, t as collectLeafRoutes } from "./_chunks/walkers-VOXgavMF.js";
4
4
  import { t as formatSize } from "./_chunks/format-CYBGxKtc.js";
5
5
  import { dirname, extname, join, normalize, resolve } from "node:path";
6
6
  import { createRequire } from "node:module";
@@ -12392,10 +12392,6 @@ var require_acorn = /* @__PURE__ */ __commonJSMin(((exports, module) => {
12392
12392
  * avoiding false positives from regex matching inside string literals,
12393
12393
  * comments, or template expressions.
12394
12394
  *
12395
- * The function-body directive detection (findFunctionsWithDirective) is a
12396
- * general-purpose utility kept for future use. Custom directives like
12397
- * 'use cache' are not currently implemented — see design/06-caching.md.
12398
- *
12399
12395
  * @module
12400
12396
  */
12401
12397
  var import_acorn_jsx = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -12850,10 +12846,10 @@ function lintStatusFileDirectives(tree) {
12850
12846
  }
12851
12847
  function walkNode(node, warnings) {
12852
12848
  if (node.error) checkFile(node.error.filePath, node.error.extension, "error", warnings);
12853
- if (node.statusFiles) for (const [code, file] of node.statusFiles) checkFile(file.filePath, file.extension, code, warnings);
12854
- if (node.legacyStatusFiles) for (const [name, file] of node.legacyStatusFiles) checkFile(file.filePath, file.extension, name, warnings);
12849
+ if (node.statusFiles) for (const [code, file] of Object.entries(node.statusFiles)) checkFile(file.filePath, file.extension, code, warnings);
12850
+ if (node.legacyStatusFiles) for (const [name, file] of Object.entries(node.legacyStatusFiles)) checkFile(file.filePath, file.extension, name, warnings);
12855
12851
  for (const child of node.children) walkNode(child, warnings);
12856
- for (const [, slotNode] of node.slots) walkNode(slotNode, warnings);
12852
+ for (const slotNode of Object.values(node.slots)) walkNode(slotNode, warnings);
12857
12853
  }
12858
12854
  function checkFile(filePath, extension, fileType, warnings) {
12859
12855
  if (!CLIENT_REQUIRED_EXTENSIONS.has(extension)) return;
@@ -12935,7 +12931,7 @@ function checkEmptyApp(root, appDir, warnings) {
12935
12931
  function hasAnyRoutable(node) {
12936
12932
  if (node.page || node.route) return true;
12937
12933
  for (const child of node.children) if (hasAnyRoutable(child)) return true;
12938
- for (const [, slot] of node.slots) if (hasAnyRoutable(slot)) return true;
12934
+ for (const slot of Object.values(node.slots)) if (hasAnyRoutable(slot)) return true;
12939
12935
  return false;
12940
12936
  }
12941
12937
  /**
@@ -12946,7 +12942,7 @@ function hasAnyRoutable(node) {
12946
12942
  function hasAnyPage(node) {
12947
12943
  if (node.page) return true;
12948
12944
  for (const child of node.children) if (hasAnyPage(child)) return true;
12949
- for (const [, slot] of node.slots) if (hasAnyPage(slot)) return true;
12945
+ for (const slot of Object.values(node.slots)) if (hasAnyPage(slot)) return true;
12950
12946
  return false;
12951
12947
  }
12952
12948
  /** HTTP methods that route.ts can export. */
@@ -12995,7 +12991,7 @@ function checkRouteExports(node, warnings) {
12995
12991
  } catch {}
12996
12992
  }
12997
12993
  for (const child of node.children) checkRouteExports(child, warnings);
12998
- for (const [, slot] of node.slots) checkRouteExports(slot, warnings);
12994
+ for (const slot of Object.values(node.slots)) checkRouteExports(slot, warnings);
12999
12995
  }
13000
12996
  /**
13001
12997
  * Warn when the root segment has no layout.tsx.
@@ -13036,7 +13032,7 @@ function checkDefaultExports(node, warnings) {
13036
13032
  if (node.page && isScriptExtension(node.page.extension)) checkFileDefaultExport(node.page.filePath, "page", warnings);
13037
13033
  if (node.layout && isScriptExtension(node.layout.extension)) checkFileDefaultExport(node.layout.filePath, "layout", warnings);
13038
13034
  for (const child of node.children) checkDefaultExports(child, warnings);
13039
- for (const [, slot] of node.slots) checkDefaultExports(slot, warnings);
13035
+ for (const slot of Object.values(node.slots)) checkDefaultExports(slot, warnings);
13040
13036
  }
13041
13037
  function isScriptExtension(ext) {
13042
13038
  return ext === "tsx" || ext === "ts" || ext === "jsx" || ext === "js";
@@ -13169,7 +13165,7 @@ function timberRouting(ctx) {
13169
13165
  segmentType: "static",
13170
13166
  urlPath: "/",
13171
13167
  children: [],
13172
- slots: /* @__PURE__ */ new Map()
13168
+ slots: {}
13173
13169
  } };
13174
13170
  return;
13175
13171
  }
@@ -13293,7 +13289,7 @@ function generateSearchParamsRegistryModule(tree) {
13293
13289
  });
13294
13290
  }
13295
13291
  for (const child of node.children) walk(child);
13296
- for (const [, slot] of node.slots) walk(slot);
13292
+ for (const slot of Object.values(node.slots)) walk(slot);
13297
13293
  }
13298
13294
  walk(tree.root);
13299
13295
  entries.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
@@ -13395,33 +13391,33 @@ function generateManifestModule(tree, viteRoot) {
13395
13391
  const v = addImport(node.params);
13396
13392
  parts.push(`${nextIndent}params: { load: ${v}, filePath: ${JSON.stringify(node.params.filePath)} },`);
13397
13393
  }
13398
- if (node.statusFiles && node.statusFiles.size > 0) {
13394
+ if (node.statusFiles && Object.keys(node.statusFiles).length > 0) {
13399
13395
  const statusEntries = [];
13400
- for (const [code, file] of node.statusFiles) {
13396
+ for (const [code, file] of Object.entries(node.statusFiles)) {
13401
13397
  const v = addImport(file);
13402
13398
  statusEntries.push(`${nextIndent} ${JSON.stringify(code)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`);
13403
13399
  }
13404
13400
  parts.push(`${nextIndent}statusFiles: {\n${statusEntries.join(",\n")}\n${nextIndent}},`);
13405
13401
  }
13406
- if (node.jsonStatusFiles && node.jsonStatusFiles.size > 0) {
13402
+ if (node.jsonStatusFiles && Object.keys(node.jsonStatusFiles).length > 0) {
13407
13403
  const jsonEntries = [];
13408
- for (const [code, file] of node.jsonStatusFiles) {
13404
+ for (const [code, file] of Object.entries(node.jsonStatusFiles)) {
13409
13405
  const v = addImport(file);
13410
13406
  jsonEntries.push(`${nextIndent} ${JSON.stringify(code)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`);
13411
13407
  }
13412
13408
  parts.push(`${nextIndent}jsonStatusFiles: {\n${jsonEntries.join(",\n")}\n${nextIndent}},`);
13413
13409
  }
13414
- if (node.legacyStatusFiles && node.legacyStatusFiles.size > 0) {
13410
+ if (node.legacyStatusFiles && Object.keys(node.legacyStatusFiles).length > 0) {
13415
13411
  const legacyEntries = [];
13416
- for (const [name, file] of node.legacyStatusFiles) {
13412
+ for (const [name, file] of Object.entries(node.legacyStatusFiles)) {
13417
13413
  const v = addImport(file);
13418
13414
  legacyEntries.push(`${nextIndent} ${JSON.stringify(name)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`);
13419
13415
  }
13420
13416
  parts.push(`${nextIndent}legacyStatusFiles: {\n${legacyEntries.join(",\n")}\n${nextIndent}},`);
13421
13417
  }
13422
- if (node.metadataRoutes && node.metadataRoutes.size > 0) {
13418
+ if (node.metadataRoutes && Object.keys(node.metadataRoutes).length > 0) {
13423
13419
  const metaEntries = [];
13424
- for (const [name, file] of node.metadataRoutes) {
13420
+ for (const [name, file] of Object.entries(node.metadataRoutes)) {
13425
13421
  const v = addImport(file);
13426
13422
  metaEntries.push(`${nextIndent} ${JSON.stringify(name)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`);
13427
13423
  }
@@ -13431,9 +13427,13 @@ function generateManifestModule(tree, viteRoot) {
13431
13427
  const childNodes = node.children.map((c) => serializeNode(c, nextIndent));
13432
13428
  parts.push(`${nextIndent}children: [\n${childNodes.join(",\n")}\n${nextIndent}],`);
13433
13429
  } else parts.push(`${nextIndent}children: [],`);
13434
- if (node.slots.size > 0) {
13430
+ const slotKeys = Object.keys(node.slots);
13431
+ if (slotKeys.length > 0) {
13435
13432
  const slotEntries = [];
13436
- for (const [slotName, slotNode] of node.slots) slotEntries.push(`${nextIndent} ${JSON.stringify(slotName)}: ${serializeNode(slotNode, nextIndent + " ")}`);
13433
+ for (const slotName of slotKeys) {
13434
+ const slotNode = node.slots[slotName];
13435
+ slotEntries.push(`${nextIndent} ${JSON.stringify(slotName)}: ${serializeNode(slotNode, nextIndent + " ")}`);
13436
+ }
13437
13437
  parts.push(`${nextIndent}slots: {\n${slotEntries.join(",\n")}\n${nextIndent}},`);
13438
13438
  } else parts.push(`${nextIndent}slots: {},`);
13439
13439
  return `${indent}{\n${parts.join("\n")}\n${indent}}`;
@@ -16211,32 +16211,22 @@ function green(text) {
16211
16211
  /**
16212
16212
  * Walk the route tree and collect all leaf routes (pages + API endpoints).
16213
16213
  *
16214
- * Parallel slots (`@artists`, `@shows`, etc.) are intentionally skipped —
16215
- * they render alongside the parent page at the same URL and are not
16216
- * separately URL-addressable. Their JS is captured in shared/layout chunks.
16214
+ * Wraps the shared `collectLeafRoutes` walker (TIM-848). Parallel slots
16215
+ * (`@artists`, `@shows`, etc.) are intentionally skipped they render
16216
+ * alongside the parent page at the same URL and are not separately
16217
+ * URL-addressable. Their JS is captured in shared/layout chunks.
16217
16218
  *
16218
16219
  * After collection, entries are deduplicated by URL path so that overlapping
16219
16220
  * route groups (e.g. `(browse)` and `(marketing)` both producing `/`) only
16220
- * appear once. The entry with the largest route-specific size wins.
16221
+ * appear once. The entry with the longest segment chain (most specific
16222
+ * match) wins.
16221
16223
  */
16222
16224
  function collectRoutes(tree) {
16223
- const routes = [];
16224
- function walk(node, chain) {
16225
- const currentChain = [...chain, node];
16226
- const path = node.urlPath || "/";
16227
- if (node.page) routes.push({
16228
- path,
16229
- segments: currentChain,
16230
- entryFilePath: node.page.filePath
16231
- });
16232
- if (node.route) routes.push({
16233
- path,
16234
- segments: currentChain,
16235
- entryFilePath: node.route.filePath
16236
- });
16237
- for (const child of node.children) walk(child, currentChain);
16238
- }
16239
- walk(tree.root, []);
16225
+ const routes = collectLeafRoutes(tree.root).map((leaf) => ({
16226
+ path: leaf.urlPath,
16227
+ segments: leaf.segments,
16228
+ entryFilePath: leaf.page?.filePath ?? leaf.route?.filePath ?? null
16229
+ }));
16240
16230
  const seen = /* @__PURE__ */ new Map();
16241
16231
  for (const route of routes) {
16242
16232
  const existing = seen.get(route.path);
@@ -16645,9 +16635,11 @@ var HOLDING_PAGE_HTML = [
16645
16635
  *
16646
16636
  * Usage (inside Vite plugin):
16647
16637
  * ```ts
16648
- * // In config() hook — earliest point where port is known
16638
+ * // In config() hook — earliest point where port is known.
16639
+ * // Use bindWithBump() to honor the default-3000 + auto-bump policy
16640
+ * // (see ../server/port-resolution.ts and TIM-842).
16649
16641
  * const holding = createHoldingServer();
16650
- * holding.listen(config.server?.port ?? 5173);
16642
+ * await bindWithBump((p) => holding.listen(p), { startPort: 3000, autoBump: true });
16651
16643
  *
16652
16644
  * // In last plugin's configureServer() — wrap listen for seamless handoff
16653
16645
  * const originalListen = server.listen.bind(server);
@@ -16701,6 +16693,122 @@ function createHoldingServer() {
16701
16693
  };
16702
16694
  }
16703
16695
  //#endregion
16696
+ //#region src/server/port-resolution.ts
16697
+ /**
16698
+ * Port resolution for the dev server, Vite preview, and the Node
16699
+ * production preview server.
16700
+ *
16701
+ * Behavior (see TIM-842):
16702
+ *
16703
+ * 1. Default port is **3000** for both dev and prod.
16704
+ * 2. If the user did NOT set an explicit port, auto-bump from 3000
16705
+ * until a free port is found (3000 → 3001 → 3002 → …).
16706
+ * 3. If the user DID set an explicit port (via `--port`, `PORT` env
16707
+ * var, or `vite.config.ts` `server.port`), use it as-is and let
16708
+ * the bind fail loudly on conflict (`strictPort: true`).
16709
+ *
16710
+ * The port-bump probe is performed by binding the **actual** server
16711
+ * (e.g. the dev holding server) — not a throwaway probe — so there is
16712
+ * no time-of-check / time-of-use race between probing and listening.
16713
+ *
16714
+ * Design doc: 21-dev-server.md §"Default Port and Auto-Bump".
16715
+ */
16716
+ /** Default port used by `timber dev` and `timber preview`. */
16717
+ var DEFAULT_PORT = 3e3;
16718
+ /**
16719
+ * Pure: compute the starting port from config / env / defaults.
16720
+ *
16721
+ * Performs no I/O — pair with {@link bindWithBump} to actually listen.
16722
+ */
16723
+ function resolveStartPort(input) {
16724
+ const defaultPort = input.defaultPort ?? 3e3;
16725
+ if (typeof input.configPort === "number" && Number.isFinite(input.configPort)) return {
16726
+ port: input.configPort,
16727
+ explicit: true
16728
+ };
16729
+ if (input.envPort != null && input.envPort !== "") {
16730
+ const parsed = Number(input.envPort);
16731
+ if (Number.isFinite(parsed) && parsed > 0) return {
16732
+ port: parsed,
16733
+ explicit: true
16734
+ };
16735
+ }
16736
+ return {
16737
+ port: defaultPort,
16738
+ explicit: false
16739
+ };
16740
+ }
16741
+ /**
16742
+ * Bind a server starting at `startPort`, optionally bumping the port
16743
+ * on `EADDRINUSE` until a free one is found.
16744
+ *
16745
+ * Use this with the actual server you intend to keep listening (e.g.
16746
+ * the dev holding server). Pairing the probe with the real listen
16747
+ * eliminates the TOCTOU race that a throwaway probe would introduce.
16748
+ */
16749
+ async function bindWithBump(listen, options) {
16750
+ const maxAttempts = options.autoBump ? options.maxAttempts ?? 100 : 1;
16751
+ let lastErr = null;
16752
+ for (let i = 0; i < maxAttempts; i++) {
16753
+ const port = options.startPort + i;
16754
+ try {
16755
+ return {
16756
+ port: await listen(port),
16757
+ bumped: i > 0
16758
+ };
16759
+ } catch (err) {
16760
+ lastErr = err;
16761
+ if (!isAddrInUse(err)) throw err;
16762
+ }
16763
+ }
16764
+ throw lastErr ?? /* @__PURE__ */ new Error(`Could not bind to a free port starting at ${options.startPort}`);
16765
+ }
16766
+ /** True if `err` is a Node `EADDRINUSE` error from `server.listen()`. */
16767
+ function isAddrInUse(err) {
16768
+ return typeof err === "object" && err !== null && err.code === "EADDRINUSE";
16769
+ }
16770
+ /**
16771
+ * Run the full dev-server port resolution + holding-server bind sequence.
16772
+ *
16773
+ * Resolves the port from config / env / default, attempts to bind the
16774
+ * holding server (auto-bumping when the port came from the default),
16775
+ * and logs the chosen URL. On a clean failure for an explicit port, it
16776
+ * warns and falls back to the requested port so Vite can surface the
16777
+ * conflict via `strictPort: true`.
16778
+ *
16779
+ * Extracted from `index.ts` so the rootSync `config()` hook stays
16780
+ * focused on plugin assembly.
16781
+ */
16782
+ async function startDevServerPort(input) {
16783
+ const log = input.log ?? ((msg) => console.log(msg));
16784
+ const warn = input.warn ?? ((msg) => console.warn(msg));
16785
+ const start = resolveStartPort({
16786
+ configPort: input.configPort,
16787
+ envPort: input.envPort,
16788
+ defaultPort: DEFAULT_PORT
16789
+ });
16790
+ try {
16791
+ const result = await bindWithBump(input.listen, {
16792
+ startPort: start.port,
16793
+ autoBump: !start.explicit
16794
+ });
16795
+ if (result.bumped) log(`\n \x1b[33m[timber]\x1b[0m Port ${start.port} in use, using ${result.port}\n`);
16796
+ log(`\n \x1b[2m\u{1FAB5} timber.js dev server starting at\x1b[0m \x1b[36mhttp://localhost:${result.port}\x1b[0m\n`);
16797
+ return {
16798
+ port: result.port,
16799
+ explicit: start.explicit,
16800
+ bound: true
16801
+ };
16802
+ } catch (err) {
16803
+ if (start.explicit && isAddrInUse(err)) warn(`\n \x1b[33m[timber]\x1b[0m Port ${start.port} is already in use. Set PORT (or remove the override) to pick another port.\n`);
16804
+ return {
16805
+ port: start.port,
16806
+ explicit: start.explicit,
16807
+ bound: false
16808
+ };
16809
+ }
16810
+ }
16811
+ //#endregion
16704
16812
  //#region src/plugin-context.ts
16705
16813
  /**
16706
16814
  * Plugin context — internal types and helpers for timber sub-plugins.
@@ -16942,7 +17050,7 @@ function timber(config) {
16942
17050
  const earlyFileConfig = loadTimberConfigFile(process.cwd());
16943
17051
  const rootSync = {
16944
17052
  name: "timber-root-sync",
16945
- config(userConfig, { command }) {
17053
+ async config(userConfig, { command, isPreview }) {
16946
17054
  const viteRoot = resolve(userConfig.root ?? process.cwd());
16947
17055
  ctx.timer.start("config-load");
16948
17056
  const fileConfig = loadTimberConfigFile(viteRoot);
@@ -16960,19 +17068,26 @@ function timber(config) {
16960
17068
  const hadCompilerInEarly = !config?.reactCompiler && earlyFileConfig?.reactCompiler;
16961
17069
  if (hasCompilerInReloaded && !hadCompilerInEarly) console.warn("[timber] reactCompiler is set in timber.config.ts but could not be registered because the config file is in a non-cwd root directory. Move reactCompiler to the inline timber() config in vite.config.ts, or run vite from the project root directory.");
16962
17070
  }
16963
- if (command === "serve") {
16964
- const port = userConfig.server?.port ?? 5173;
16965
- try {
16966
- ctx.holdingServer = createHoldingServer();
16967
- ctx.holdingServer.listen(port).then((boundPort) => {
16968
- const url = `http://localhost:${boundPort}`;
16969
- console.log(`\n \x1b[2m🪵 timber.js dev server starting at\x1b[0m \x1b[36m${url}\x1b[0m\n`);
16970
- }, () => {
16971
- ctx.holdingServer = null;
16972
- });
16973
- } catch {
16974
- ctx.holdingServer = null;
16975
- }
17071
+ let resolvedDevPort = null;
17072
+ let resolvedDevPortExplicit = false;
17073
+ if (command === "serve" && !isPreview) {
17074
+ ctx.holdingServer = createHoldingServer();
17075
+ const holdingRef = ctx.holdingServer;
17076
+ const result = await startDevServerPort({
17077
+ configPort: typeof userConfig.server?.port === "number" ? userConfig.server.port : void 0,
17078
+ envPort: process.env.PORT,
17079
+ listen: (p) => holdingRef.listen(p)
17080
+ });
17081
+ resolvedDevPort = result.port;
17082
+ resolvedDevPortExplicit = result.explicit;
17083
+ if (!result.bound) ctx.holdingServer = null;
17084
+ } else if (command === "serve" && isPreview) {
17085
+ const start = resolveStartPort({
17086
+ configPort: typeof userConfig.preview?.port === "number" ? userConfig.preview.port : typeof userConfig.server?.port === "number" ? userConfig.server.port : void 0,
17087
+ envPort: process.env.PORT
17088
+ });
17089
+ resolvedDevPort = start.port;
17090
+ resolvedDevPortExplicit = start.explicit;
16976
17091
  }
16977
17092
  const buildOutDir = timberBuildDir ? timberBuildDir : viteOutDir && viteOutDir !== "dist" ? viteOutDir : DEFAULT_BUILD_DIR;
16978
17093
  const envOutDirs = {};
@@ -16986,9 +17101,21 @@ function timber(config) {
16986
17101
  environments: envOutDirs,
16987
17102
  oxc: { jsx: { development: false } }
16988
17103
  };
17104
+ const serverConfig = {};
17105
+ const previewConfig = {};
17106
+ if (resolvedDevPort != null) {
17107
+ if (!isPreview) {
17108
+ serverConfig.port = resolvedDevPort;
17109
+ serverConfig.strictPort = true;
17110
+ }
17111
+ previewConfig.port = resolvedDevPort;
17112
+ previewConfig.strictPort = resolvedDevPortExplicit;
17113
+ }
16989
17114
  return {
16990
17115
  build: { outDir: buildOutDir },
16991
- environments: envOutDirs
17116
+ environments: envOutDirs,
17117
+ server: serverConfig,
17118
+ preview: previewConfig
16992
17119
  };
16993
17120
  },
16994
17121
  configResolved(resolved) {