@timber-js/app 0.2.0-alpha.111 → 0.2.0-alpha.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { n as setViteServer } from "./_chunks/warnings-B30wpfaS.js";
2
2
  import { h as swallow } from "./_chunks/logger-DDmK5skc.js";
3
3
  import { t as scanRoutes } from "./_chunks/scanner-DmfoB7iK.js";
4
4
  import { n as collectInterceptionRewrites, r as generateRouteMap, t as collectLeafRoutes } from "./_chunks/walkers-DGTmJT0f.js";
5
- import { n as findSchemaFile, r as parseExistingSchemaKeys, t as collectDynamicSegments } from "./_chunks/cli-schema-sync-BvvuK8Rp.js";
5
+ import { a as DEFAULT_BUILD_DIR, c as mergeFileConfig, d as resolveClientJavascript, f as createNoopTimer, l as resolveAppDir, n as findSchemaFile, o as createPluginContext, r as parseExistingSchemaKeys, s as loadTimberConfigFile, t as collectDynamicSegments, u as resolveBuildDir } from "./_chunks/cli-schema-sync-DFZmnmqG.js";
6
6
  import { t as formatSize } from "./_chunks/format-CR6v1pWE.js";
7
7
  import { dirname, extname, join, normalize, resolve } from "node:path";
8
8
  import { createRequire } from "node:module";
@@ -14,7 +14,6 @@ import { Readable } from "node:stream";
14
14
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
15
15
  import { createHash, randomUUID } from "node:crypto";
16
16
  import { parseAst } from "vite";
17
- import { performance as performance$1 } from "node:perf_hooks";
18
17
  import http from "node:http";
19
18
  //#region \0rolldown/runtime.js
20
19
  var __defProp = Object.defineProperty;
@@ -1980,6 +1979,52 @@ function timberMdx(ctx) {
1980
1979
  };
1981
1980
  }
1982
1981
  //#endregion
1982
+ //#region src/routing/segment-codegen.ts
1983
+ /**
1984
+ * _segment type generation and directory-to-route mapping.
1985
+ *
1986
+ * Generates per-route _segment.d.ts files in .timber/types/ that mirror
1987
+ * the app/ directory structure. TypeScript's rootDirs feature makes these
1988
+ * importable as './_segment' from route files.
1989
+ *
1990
+ * Design doc: design/41-global-params.md §The _segment Module
1991
+ */
1992
+ /**
1993
+ * Build a map of absolute filesystem directory → route pattern.
1994
+ * Used by resolveId to map a _segment import's importer to the correct route.
1995
+ *
1996
+ * Walks every segment (not just leaves) so layouts and middleware
1997
+ * at any depth can import _segment.
1998
+ */
1999
+ function buildDirToRouteMap(tree, appDir) {
2000
+ const map = /* @__PURE__ */ new Map();
2001
+ function walk(node, fsPath) {
2002
+ const routePattern = node.urlPath || "/";
2003
+ map.set(fsPath, routePattern);
2004
+ for (const child of node.children) walk(child, join(fsPath, child.segmentName));
2005
+ for (const [slotName, slotNode] of Object.entries(node.slots)) walk(slotNode, join(fsPath, slotName));
2006
+ }
2007
+ walk(tree.root, appDir);
2008
+ return map;
2009
+ }
2010
+ /**
2011
+ * Generate per-route _segment.d.ts files in .timber/types/ mirroring
2012
+ * the app/ directory structure. These files are importable via TypeScript's
2013
+ * rootDirs feature.
2014
+ */
2015
+ async function writeSegmentTypes(routeTree, appDir, root) {
2016
+ const typesBase = join(root, ".timber", "types", appDir.slice(root.length + 1));
2017
+ const map = buildDirToRouteMap(routeTree, appDir);
2018
+ const writes = [];
2019
+ for (const [fsDir, routePattern] of map) {
2020
+ const segmentDir = join(typesBase, fsDir.slice(appDir.length));
2021
+ const segmentPath = join(segmentDir, "_segment.d.ts");
2022
+ const content = `export const route: '${routePattern}';\n`;
2023
+ writes.push(mkdir(segmentDir, { recursive: true }).then(() => writeFile(segmentPath, content, "utf-8")));
2024
+ }
2025
+ await Promise.all(writes);
2026
+ }
2027
+ //#endregion
1983
2028
  //#region src/routing/convention-lint.ts
1984
2029
  /**
1985
2030
  * Convention linter — validates common misconfigurations in the route tree.
@@ -2218,6 +2263,7 @@ var SEARCH_PARAMS_REGISTRY_ID = "virtual:timber-search-params-registry";
2218
2263
  var RESOLVED_SEARCH_PARAMS_REGISTRY_ID = `\0${SEARCH_PARAMS_REGISTRY_ID}`;
2219
2264
  var SCHEMA_ID = "virtual:timber-schema";
2220
2265
  var RESOLVED_SCHEMA_ID = `\0${SCHEMA_ID}`;
2266
+ var SEGMENT_PREFIX = "\0_segment:";
2221
2267
  /**
2222
2268
  * File convention names we track for changes that require manifest regeneration.
2223
2269
  */
@@ -2258,13 +2304,19 @@ function writeCodegen(ctx) {
2258
2304
  });
2259
2305
  const routesPath = join(ctx.root, CODEGEN_OUTPUT);
2260
2306
  const envPath = join(ctx.root, "timber-env.d.ts");
2261
- mkdir(timberDir, { recursive: true }).then(() => Promise.all([writeFile(routesPath, content, "utf-8"), writeFile(envPath, TIMBER_ENV_DTS, "utf-8")])).catch((err) => {
2307
+ mkdir(timberDir, { recursive: true }).then(() => Promise.all([
2308
+ writeFile(routesPath, content, "utf-8"),
2309
+ writeFile(envPath, TIMBER_ENV_DTS, "utf-8"),
2310
+ writeSegmentTypes(ctx.routeTree, ctx.appDir, ctx.root)
2311
+ ])).catch((err) => {
2262
2312
  console.warn("[timber] Failed to write codegen output:", err);
2263
2313
  });
2264
2314
  }
2265
2315
  function timberRouting(ctx) {
2266
2316
  /** Track warned files to avoid repeating warnings on rescan. */
2267
2317
  const warnedFiles = /* @__PURE__ */ new Set();
2318
+ /** Maps absolute filesystem directory → route pattern for _segment resolution. */
2319
+ let dirToRoute = /* @__PURE__ */ new Map();
2268
2320
  function rescan() {
2269
2321
  const appDirWarning = checkAppDirExists(ctx.appDir);
2270
2322
  if (appDirWarning) {
@@ -2282,6 +2334,7 @@ function timberRouting(ctx) {
2282
2334
  ctx.timer.start("route-scan");
2283
2335
  ctx.routeTree = scanRoutes(ctx.appDir, { pageExtensions: ctx.config.pageExtensions });
2284
2336
  ctx.timer.end("route-scan");
2337
+ dirToRoute = buildDirToRouteMap(ctx.routeTree, ctx.appDir);
2285
2338
  writeCodegen(ctx);
2286
2339
  warnUnregisteredSegments(ctx);
2287
2340
  const newConventionWarnings = lintConventions(ctx.routeTree, ctx.appDir).filter((w) => !warnedFiles.has(w.id));
@@ -2293,12 +2346,18 @@ function timberRouting(ctx) {
2293
2346
  }
2294
2347
  return {
2295
2348
  name: "timber-routing",
2296
- resolveId(id) {
2349
+ resolveId(id, importer) {
2297
2350
  const cleanId = id.startsWith("\0") ? id.slice(1) : id;
2298
2351
  if (cleanId === VIRTUAL_MODULE_ID$1) return RESOLVED_VIRTUAL_ID$1;
2299
2352
  if (cleanId.endsWith(`/${VIRTUAL_MODULE_ID$1}`)) return RESOLVED_VIRTUAL_ID$1;
2300
2353
  if (cleanId === SEARCH_PARAMS_REGISTRY_ID || cleanId.endsWith(`/${SEARCH_PARAMS_REGISTRY_ID}`)) return RESOLVED_SEARCH_PARAMS_REGISTRY_ID;
2301
2354
  if (cleanId === SCHEMA_ID || cleanId.endsWith(`/${SCHEMA_ID}`)) return RESOLVED_SCHEMA_ID;
2355
+ if (importer && (cleanId === "./_segment" || cleanId === "./_segment.js" || cleanId === "./_segment.ts")) {
2356
+ const cleanImporter = importer.startsWith("\0") ? importer.slice(1) : importer;
2357
+ const importerDir = cleanImporter.substring(0, cleanImporter.lastIndexOf("/"));
2358
+ const routePattern = dirToRoute.get(importerDir);
2359
+ if (routePattern !== void 0) return `${SEGMENT_PREFIX}${routePattern}`;
2360
+ }
2302
2361
  return null;
2303
2362
  },
2304
2363
  load(id) {
@@ -2311,6 +2370,10 @@ function timberRouting(ctx) {
2311
2370
  return generateSearchParamsRegistryModule(ctx.routeTree);
2312
2371
  }
2313
2372
  if (id === RESOLVED_SCHEMA_ID) return generateSchemaModule(ctx.appDir);
2373
+ if (id.startsWith(SEGMENT_PREFIX)) {
2374
+ const routePattern = id.slice(10);
2375
+ return `export const route = ${JSON.stringify(routePattern)};\n`;
2376
+ }
2314
2377
  return null;
2315
2378
  },
2316
2379
  buildStart() {
@@ -2392,6 +2455,7 @@ function invalidateManifest(server) {
2392
2455
  const mod = env.moduleGraph.getModuleById(virtualId);
2393
2456
  if (mod) env.moduleGraph.invalidateModule(mod);
2394
2457
  }
2458
+ for (const mod of env.moduleGraph.idToModuleMap.values()) if (mod.id?.startsWith(SEGMENT_PREFIX)) env.moduleGraph.invalidateModule(mod);
2395
2459
  }
2396
2460
  server.hot.send({ type: "full-reload" });
2397
2461
  }
@@ -6493,81 +6557,6 @@ function timberBuildReport(ctx) {
6493
6557
  };
6494
6558
  }
6495
6559
  //#endregion
6496
- //#region src/utils/startup-timer.ts
6497
- /**
6498
- * Startup timer — records named phases with their durations.
6499
- *
6500
- * Used by the plugin system to instrument cold start and report a
6501
- * timing breakdown in dev mode. Zero overhead in production (disabled).
6502
- *
6503
- * See design/18-build-system.md, TIM-155.
6504
- */
6505
- /**
6506
- * Create a startup timer that records phase durations.
6507
- */
6508
- function createStartupTimer() {
6509
- const pending = /* @__PURE__ */ new Map();
6510
- const phases = [];
6511
- return {
6512
- start(phase) {
6513
- pending.set(phase, performance$1.now());
6514
- },
6515
- end(phase) {
6516
- const startMs = pending.get(phase);
6517
- if (startMs === void 0) return 0;
6518
- pending.delete(phase);
6519
- const durationMs = performance$1.now() - startMs;
6520
- phases.push({
6521
- name: phase,
6522
- startMs,
6523
- durationMs
6524
- });
6525
- return durationMs;
6526
- },
6527
- getPhases() {
6528
- return [...phases].sort((a, b) => a.startMs - b.startMs);
6529
- },
6530
- totalMs() {
6531
- if (phases.length === 0) return 0;
6532
- const sorted = this.getPhases();
6533
- const first = sorted[0];
6534
- const last = sorted[sorted.length - 1];
6535
- return last.startMs + last.durationMs - first.startMs;
6536
- },
6537
- formatSummary() {
6538
- const sorted = this.getPhases();
6539
- if (sorted.length === 0) return "No phases recorded.";
6540
- const lines = sorted.map((p) => {
6541
- const ms = p.durationMs.toFixed(1);
6542
- return ` ${p.name.padEnd(30)} ${ms.padStart(8)}ms`;
6543
- });
6544
- const total = this.totalMs().toFixed(1);
6545
- lines.push(` ${"total".padEnd(30)} ${total.padStart(8)}ms`);
6546
- return ["[timber] startup timing:", ...lines].join("\n");
6547
- }
6548
- };
6549
- }
6550
- /**
6551
- * No-op timer for production builds — all methods are empty.
6552
- */
6553
- function createNoopTimer() {
6554
- return {
6555
- start() {},
6556
- end() {
6557
- return 0;
6558
- },
6559
- getPhases() {
6560
- return [];
6561
- },
6562
- totalMs() {
6563
- return 0;
6564
- },
6565
- formatSummary() {
6566
- return "";
6567
- }
6568
- };
6569
- }
6570
- //#endregion
6571
6560
  //#region src/server/action-encryption.ts
6572
6561
  /**
6573
6562
  * Regex for safe `defineEncryptionKey` expressions.
@@ -6902,150 +6891,6 @@ async function startDevServerPort(input) {
6902
6891
  }
6903
6892
  }
6904
6893
  //#endregion
6905
- //#region src/plugin-context.ts
6906
- /**
6907
- * Plugin context — internal types and helpers for timber sub-plugins.
6908
- *
6909
- * These are NOT part of the user-facing API. They are consumed by
6910
- * sub-plugins (routing, entries, shims, etc.) via relative imports.
6911
- * The root entry point (index.ts) does not re-export them.
6912
- *
6913
- * Design doc: 18-build-system.md §"Shared Plugin Context"
6914
- */
6915
- /**
6916
- * Resolve `clientJavascript` into a fully resolved config.
6917
- */
6918
- function resolveClientJavascript(config) {
6919
- if (config.clientJavascript !== void 0) {
6920
- if (typeof config.clientJavascript === "boolean") return {
6921
- disabled: !config.clientJavascript,
6922
- enableHMRInDev: !config.clientJavascript
6923
- };
6924
- return {
6925
- disabled: config.clientJavascript.disabled,
6926
- enableHMRInDev: config.clientJavascript.enableHMRInDev ?? config.clientJavascript.disabled
6927
- };
6928
- }
6929
- return {
6930
- disabled: false,
6931
- enableHMRInDev: false
6932
- };
6933
- }
6934
- /**
6935
- * Resolve the app directory. Checks (in order):
6936
- * 1. Explicit `configAppDir` from timber.config.ts
6937
- * 2. `<root>/app`
6938
- * 3. `<root>/src/app`
6939
- *
6940
- * Throws if none exist.
6941
- */
6942
- function resolveAppDir(root, configAppDir) {
6943
- if (configAppDir) {
6944
- const explicit = join(root, configAppDir);
6945
- if (!existsSync(explicit)) throw new Error(`[timber] Configured appDir "${configAppDir}" does not exist at ${explicit}`);
6946
- return explicit;
6947
- }
6948
- const rootApp = join(root, "app");
6949
- if (existsSync(rootApp)) return rootApp;
6950
- const srcApp = join(root, "src", "app");
6951
- if (existsSync(srcApp)) return srcApp;
6952
- throw new Error(`[timber] Could not find app directory. Expected "app/" or "src/app/" in ${root}. You can set appDir in timber.config.ts to specify a custom location.`);
6953
- }
6954
- function createPluginContext(config, root) {
6955
- const projectRoot = root ?? process.cwd();
6956
- const resolvedConfig = { ...config };
6957
- return {
6958
- config: resolvedConfig,
6959
- clientJavascript: resolveClientJavascript(resolvedConfig),
6960
- routeTree: null,
6961
- appDir: join(projectRoot, "app"),
6962
- root: projectRoot,
6963
- dev: false,
6964
- buildManifest: null,
6965
- deploymentId: null,
6966
- timer: createStartupTimer(),
6967
- holdingServer: null,
6968
- buildDir: resolveBuildDir(projectRoot, resolvedConfig.buildDir)
6969
- };
6970
- }
6971
- /** Default build output directory (relative to root). */
6972
- var DEFAULT_BUILD_DIR = join(".timber", "dist");
6973
- /**
6974
- * Resolve the build output directory.
6975
- *
6976
- * Priority:
6977
- * 1. Explicit `timberBuildDir` from timber.config.ts
6978
- * 2. Explicit `viteBuildOutDir` from Vite's build.outDir (if not the default 'dist')
6979
- * 3. Default: `.timber/dist`
6980
- *
6981
- * Returns an absolute path.
6982
- */
6983
- function resolveBuildDir(root, timberBuildDir, viteBuildOutDir) {
6984
- if (timberBuildDir) return join(root, timberBuildDir);
6985
- if (viteBuildOutDir && viteBuildOutDir !== "dist") return join(root, viteBuildOutDir);
6986
- return join(root, DEFAULT_BUILD_DIR);
6987
- }
6988
- /**
6989
- * Load timber.config.ts (or .js, .mjs) from the project root.
6990
- * Returns the config object or null if no config file is found.
6991
- *
6992
- * Uses require() which works for ESM modules on Node 22.12+.
6993
- * This keeps timber() synchronous — no async config loading needed.
6994
- */
6995
- function loadTimberConfigFile(root) {
6996
- const configNames = [
6997
- "timber.config.ts",
6998
- "timber.config.js",
6999
- "timber.config.mjs"
7000
- ];
7001
- const req = createRequire(join(root, "package.json"));
7002
- for (const name of configNames) {
7003
- const configPath = join(root, name);
7004
- if (existsSync(configPath)) {
7005
- const mod = req(configPath);
7006
- return mod.default ?? mod;
7007
- }
7008
- }
7009
- return null;
7010
- }
7011
- /**
7012
- * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)
7013
- * and warn the user. The `output` key is excluded because it defaults to 'server'
7014
- * in createPluginContext and would always appear as an inline key.
7015
- *
7016
- * Returns the list of conflicting key names (for testing).
7017
- */
7018
- function warnConfigConflicts(inline, fileConfig) {
7019
- const conflicts = [];
7020
- for (const key of Object.keys(fileConfig)) if (key in inline && inline[key] !== void 0) conflicts.push(key);
7021
- if (conflicts.length > 0) console.warn(`[timber] Config conflict: ${conflicts.map((k) => `"${k}"`).join(", ")} set in both vite.config.ts (inline) and timber.config.ts. Move all config to timber.config.ts to avoid confusion. The inline value from vite.config.ts will be used.`);
7022
- return conflicts;
7023
- }
7024
- /**
7025
- * Merge file-based config into ctx.config. Inline config (already in ctx.config)
7026
- * takes precedence — file config only fills in missing fields.
7027
- */
7028
- function mergeFileConfig(ctx, fileConfig) {
7029
- const inline = ctx.config;
7030
- warnConfigConflicts(inline, fileConfig);
7031
- ctx.config = {
7032
- ...fileConfig,
7033
- ...inline,
7034
- ...fileConfig.limits && inline.limits ? { limits: {
7035
- ...fileConfig.limits,
7036
- ...inline.limits
7037
- } } : {},
7038
- ...fileConfig.dev && inline.dev ? { dev: {
7039
- ...fileConfig.dev,
7040
- ...inline.dev
7041
- } } : {},
7042
- ...fileConfig.mdx && inline.mdx ? { mdx: {
7043
- ...fileConfig.mdx,
7044
- ...inline.mdx
7045
- } } : {}
7046
- };
7047
- }
7048
- //#endregion
7049
6894
  //#region src/index.ts
7050
6895
  /**
7051
6896
  * Type-safe helper for timber.config.ts files.