@stacksjs/router 0.75.9 → 0.75.11

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.d.ts CHANGED
@@ -6,6 +6,7 @@ export type { BootHook, StacksRouterConfig } from './stacks-router';
6
6
  export type { MiddlewareAliases, MiddlewareClasses, MiddlewareClassName, MiddlewareConfig, Request } from './middleware';
7
7
  // Export route loader
8
8
  export type { RootMountedAppRoute } from './route-loader';
9
+ export type { DefaultRouteBundleSelection } from './route-loader';
9
10
  // Export route registry types — owned here rather than in app/Routes.ts
10
11
  // so the path doesn't depend on a 5-level relative reach across the
11
12
  // framework defaults tree (stacksjs/stacks#1863, T-10).
@@ -33,7 +34,7 @@ export { assertRouteMiddlewareResolvable,
33
34
  export { cacheRequestQuery, clearCurrentRequest, getCurrentRequest, getTraceId, request, runWithRequest, setCurrentRequest, withTraceId } from './request-context';
34
35
  // Export Middleware class for defining route middleware
35
36
  export { defineMiddleware, Middleware } from './middleware';
36
- export { appRouteRegistry, DEFAULT_ROUTE_REGISTRY, listRootMountedAppRoutes, loadRoutes } from './route-loader';
37
+ export { appRouteRegistry, bundleMounts, DEFAULT_ROUTE_BUNDLE_FEATURES, DEFAULT_ROUTE_REGISTRY, listRootMountedAppRoutes, loadRoutes, mountedDefaultRouteBundles } from './route-loader';
37
38
  // Export error handler utilities
38
39
  export {
39
40
  clearTrackedQueries,
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import"./request-augmentation";export*from"@stacksjs/bun-router";export{assertRouteMiddlewareResolvable,configureViewDirectories,clearCsrfModuleCache,clearMiddlewareCache,createStacksRouter,disableViewRouting,enhanceRequest,findUnresolvableRouteMiddleware,installMiddlewareHotReload,loadMiddlewareHandlers,resetBootHooks,route,runBootHooks,serve,serverResponse,url,warnOnMultipleRouterInstances}from"./stacks-router";export{cacheRequestQuery,clearCurrentRequest,getCurrentRequest,getTraceId,request,runWithRequest,setCurrentRequest,withTraceId}from"./request-context";export{defineMiddleware,Middleware}from"./middleware";export{appRouteRegistry,DEFAULT_ROUTE_REGISTRY,listRootMountedAppRoutes,loadRoutes}from"./route-loader";export{clearTrackedQueries,createErrorResponse,createMiddlewareErrorResponse,createNotFoundResponse,createValidationErrorResponse,getQueryShapeCounts,trackQuery}from"./error-handler";export{listNamedRoutes,listRegisteredRoutes,routeParams}from"./stacks-router";export{isRouterAction,wrapAction}from"./stacks-router";export{createTypedRouter}from"./typed-router";export{clearRouteModelBindings,defineRouteModelBinding,resolveRouteModel,routeModelBindings,setRouteModelFallback}from"./route-model-binding";export{isApiRequest,JSON_CONTENT_TYPE}from"./api-shape";export{rateLimit,rateLimitStatus,clearRateLimit}from"./rate-limit";export{PathParamError,safePathParam,sanitizePathParam}from"./path-sanitize";export{checkApplicationHealth,runHealthProbes}from"./health";export{stream}from"./stacks-router";export{signedUrl,signUrl,verifySignedUrl,verifySignedUrlMiddleware}from"./signed-url";export{EncryptedSessionStore}from"./encrypted-session-store";export{createSessionStore,createStacksSessionStore,DatabaseSessionStore,FileSessionStore,MemorySessionStore,RedisSessionStore}from"./session-factory";
1
+ import"./request-augmentation";export*from"@stacksjs/bun-router";export{assertRouteMiddlewareResolvable,configureViewDirectories,clearCsrfModuleCache,clearMiddlewareCache,createStacksRouter,disableViewRouting,enhanceRequest,findUnresolvableRouteMiddleware,installMiddlewareHotReload,loadMiddlewareHandlers,resetBootHooks,route,runBootHooks,serve,serverResponse,url,warnOnMultipleRouterInstances}from"./stacks-router";export{cacheRequestQuery,clearCurrentRequest,getCurrentRequest,getTraceId,request,runWithRequest,setCurrentRequest,withTraceId}from"./request-context";export{defineMiddleware,Middleware}from"./middleware";export{appRouteRegistry,bundleMounts,DEFAULT_ROUTE_BUNDLE_FEATURES,DEFAULT_ROUTE_REGISTRY,listRootMountedAppRoutes,loadRoutes,mountedDefaultRouteBundles}from"./route-loader";export{clearTrackedQueries,createErrorResponse,createMiddlewareErrorResponse,createNotFoundResponse,createValidationErrorResponse,getQueryShapeCounts,trackQuery}from"./error-handler";export{listNamedRoutes,listRegisteredRoutes,routeParams}from"./stacks-router";export{isRouterAction,wrapAction}from"./stacks-router";export{createTypedRouter}from"./typed-router";export{clearRouteModelBindings,defineRouteModelBinding,resolveRouteModel,routeModelBindings,setRouteModelFallback}from"./route-model-binding";export{isApiRequest,JSON_CONTENT_TYPE}from"./api-shape";export{rateLimit,rateLimitStatus,clearRateLimit}from"./rate-limit";export{PathParamError,safePathParam,sanitizePathParam}from"./path-sanitize";export{checkApplicationHealth,runHealthProbes}from"./health";export{stream}from"./stacks-router";export{signedUrl,signUrl,verifySignedUrl,verifySignedUrlMiddleware}from"./signed-url";export{EncryptedSessionStore}from"./encrypted-session-store";export{createSessionStore,createStacksSessionStore,DatabaseSessionStore,FileSessionStore,MemorySessionStore,RedisSessionStore}from"./session-factory";
@@ -78,6 +78,33 @@ export declare function resolveDefaultRouteBundles(env?: NodeJS.ProcessEnv): Set
78
78
  * named, the flags gate exactly as they did before.
79
79
  */
80
80
  export declare function resolveDefaultRouteBundlesWithDiagnostics(env?: NodeJS.ProcessEnv): { bundles: Set<DefaultRouteBundle>, unknown: string[], explicit: boolean };
81
+ /**
82
+ * Whether one default bundle mounts, given the selection and its feature flag.
83
+ *
84
+ * An app that NAMED its bundles has already answered the question, so the
85
+ * feature flag does not get a second veto - otherwise `STACKS_DEFAULT_ROUTES=auth`
86
+ * would still be withheld from an app running with `dashboard` off, which is
87
+ * the exact case the variable exists for. When nothing was named, the flag
88
+ * gates as it always did.
89
+ *
90
+ * `defaults/bootstrap.ts` decides with this, and so does the views server
91
+ * when it works out which default pages have routes behind them. One rule in
92
+ * two processes; a copy in each is how they come to disagree.
93
+ */
94
+ export declare function bundleMounts(selection: DefaultRouteBundleSelection | undefined, bundle: string, featureEnabled: boolean): boolean;
95
+ /**
96
+ * The default bundles this app mounts, without mounting anything.
97
+ *
98
+ * For a process that needs the answer but does not own the routes: the views
99
+ * server serves `/login` only when the API process mounts `POST /login`
100
+ * (stacksjs/stacks#2237). `featureEnabled` is `feature` from
101
+ * `@stacksjs/config`, passed in so this stays free of config state; call it
102
+ * after `overridesReady`, for the reason {@link loadFrameworkRoutes} awaits it.
103
+ *
104
+ * `social` is not included. It is opt-in, decided by configured providers,
105
+ * and no default page depends on it alone.
106
+ */
107
+ export declare function mountedDefaultRouteBundles(featureEnabled: (name: string) => boolean, env?: NodeJS.ProcessEnv): Set<typeof DEFAULT_ROUTE_BUNDLES[number]>;
81
108
  /**
82
109
  * Load all routes from the registry
83
110
  */
@@ -120,9 +147,21 @@ export declare const DEFAULT_ROUTE_BUNDLES: readonly ['auth', 'dashboard', 'deli
120
147
  export declare const OPT_IN_ROUTE_BUNDLES: readonly ['social'];
121
148
  /** Where {@link loadFrameworkRoutes} leaves the selection for bootstrap.ts. */
122
149
  export declare const DEFAULT_ROUTE_BUNDLES_KEY: '__stacksDefaultRouteBundles';
150
+ /**
151
+ * The feature flag that gates each default bundle when an app has not named
152
+ * its bundles. `auth` rides on `dashboard` rather than `auth` for the reason
153
+ * `defaults/bootstrap.ts` gives: `config/auth.ts` ships enabled in every app,
154
+ * so gating on it would widen existing apps' public surface on upgrade.
155
+ */
156
+ export declare const DEFAULT_ROUTE_BUNDLE_FEATURES: Record<typeof DEFAULT_ROUTE_BUNDLES[number], string>;
123
157
  export declare interface RootMountedAppRoute {
124
158
  method: string
125
159
  path: string
126
160
  file: string
127
161
  }
162
+ /** What {@link loadFrameworkRoutes} publishes for bootstrap.ts. */
163
+ export declare interface DefaultRouteBundleSelection {
164
+ bundles: ReadonlySet<string>
165
+ explicit: boolean
166
+ }
128
167
  export type DefaultRouteBundle = typeof DEFAULT_ROUTE_BUNDLES[number] | typeof OPT_IN_ROUTE_BUNDLES[number];
@@ -1 +1 @@
1
- import{existsSync}from"node:fs";import{log}from"@stacksjs/logging/runtime";import{route}from"./stacks-router";let rootMountedAppRoutes;export function listRootMountedAppRoutes(){return rootMountedAppRoutes??=[]}export const DEFAULT_ROUTE_REGISTRY={api:"api"};export async function appRouteRegistry(){const{projectPath}=await import("@stacksjs/path"),file=projectPath("app/Routes.ts");if(!existsSync(file))return DEFAULT_ROUTE_REGISTRY;const registry=(await import(file)).default;if(!registry||typeof registry!=="object"||Object.keys(registry).length===0){log.debug("[route-loader] app/Routes.ts exports no registry; using the default");return DEFAULT_ROUTE_REGISTRY}return registry}export async function loadRoutes(registry){for(const[key,definition]of Object.entries(registry)){const config=normalizeDefinition(definition),prefix=config.prefix!==void 0?config.prefix?config.prefix.startsWith("/")?config.prefix:`/${config.prefix}`:void 0:key==="web"?void 0:`/${key}`,middleware=normalizeMiddleware(config.middleware);log.debug(`[route-loader] Loading: ${config.path} prefix=${prefix||"/"} middleware=[${middleware.join(", ")}]`);try{if(prefix||middleware.length>0)await route.group({prefix,middleware:middleware.length>0?middleware:void 0},async()=>{await importRouteFile(config.path)});else{const before=registeredRouteKeys();await importRouteFile(config.path);for(const entry of currentRouteTable()){const method=String(entry.method??"").toUpperCase(),path=String(entry.path??"");if(!method||!path||before.has(`${method} ${path}`))continue;(rootMountedAppRoutes??=[]).push({method,path,file:`routes/${config.path}.ts`})}}}catch(error){const message=error instanceof Error?error.message:String(error);console.error(`[Routes] Failed to load route file '${config.path}': ${message}`);throw Error(`Route loading failed for '${config.path}': ${message}`)}}await loadFrameworkRoutes()}export const DEFAULT_ROUTE_BUNDLES=["auth","dashboard","delivery","email","forms"],OPT_IN_ROUTE_BUNDLES=["social"],DEFAULT_ROUTE_BUNDLES_KEY="__stacksDefaultRouteBundles";export function resolveDefaultRouteBundles(env=process.env){return resolveDefaultRouteBundlesWithDiagnostics(env).bundles}export function resolveDefaultRouteBundlesWithDiagnostics(env=process.env){const all=()=>new Set(DEFAULT_ROUTE_BUNDLES),raw=env.STACKS_DEFAULT_ROUTES;if(raw!==void 0){const names=raw.split(",").map((name)=>name.trim().toLowerCase()).filter(Boolean);if(names.length===0||names.includes("none"))return{bundles:new Set,unknown:[],explicit:!0};if(names.includes("all"))return{bundles:all(),unknown:[],explicit:!0};const bundles=new Set,unknown=[];for(const name of names)if(DEFAULT_ROUTE_BUNDLES.includes(name)||OPT_IN_ROUTE_BUNDLES.includes(name))bundles.add(name);else unknown.push(name);return{bundles,unknown,explicit:!0}}if(env.STACKS_SKIP_DEFAULT_ROUTES==="1")return{bundles:new Set,unknown:[],explicit:!0};return{bundles:all(),unknown:[],explicit:!1}}async function loadFrameworkRoutes(){const{bundles,unknown,explicit}=resolveDefaultRouteBundlesWithDiagnostics();for(const name of unknown)log.warn(`[Routes] STACKS_DEFAULT_ROUTES lists unknown bundle "${name}" - known bundles are ${DEFAULT_ROUTE_BUNDLES.join(", ")}.`);if(bundles.size===0)return;globalThis[DEFAULT_ROUTE_BUNDLES_KEY]={bundles,explicit};try{try{const{overridesReady}=await import("@stacksjs/config");await overridesReady}catch{}const{frameworkPath}=await import("@stacksjs/path"),bootstrapPath=frameworkPath("defaults/bootstrap.ts");if(await Bun.file(bootstrapPath).exists())await import(bootstrapPath)}catch(error){const message=error instanceof Error?error.message:String(error);if(!message.includes("Cannot find module")&&!message.includes("MODULE_NOT_FOUND"))console.error(`[Routes] Failed to load framework bootstrap: ${message}`)}}function assertSafeRouteName(routeName){if(typeof routeName!=="string"||routeName.length===0)throw Error("[route-loader] Invalid route path: empty or non-string");if(routeName.includes("\x00"))throw Error("[route-loader] Invalid route path: null byte");let decoded;try{decoded=decodeURIComponent(routeName)}catch{throw Error("[route-loader] Invalid route path: malformed URL encoding")}const cleanPath=decoded.replace(/\.ts$/,"");if(cleanPath.startsWith("/")||/^[A-Za-z]:[\\/]/.test(cleanPath))throw Error(`[route-loader] Invalid route path: absolute paths not allowed (${cleanPath})`);if(cleanPath.split(/[/\\]/).some((s)=>s===".."))throw Error(`[route-loader] Invalid route path: '..' segment not allowed (${cleanPath})`);return cleanPath}async function importRouteFile(routeName){const cleanPath=assertSafeRouteName(routeName),{projectPath}=await import("@stacksjs/path");await import(projectPath(`routes/${cleanPath}`))}function normalizeDefinition(def){if(typeof def==="string")return{path:def};return def}function normalizeMiddleware(middleware){if(!middleware)return[];if(typeof middleware==="string")return[middleware];return middleware}function currentRouteTable(){return route.routes??[]}function registeredRouteKeys(){const keys=new Set;for(const entry of currentRouteTable())keys.add(`${String(entry.method??"").toUpperCase()} ${String(entry.path??"")}`);return keys}
1
+ import{existsSync}from"node:fs";import{log}from"@stacksjs/logging/runtime";import{route}from"./stacks-router";let rootMountedAppRoutes;export function listRootMountedAppRoutes(){return rootMountedAppRoutes??=[]}export const DEFAULT_ROUTE_REGISTRY={api:"api"};export async function appRouteRegistry(){const{projectPath}=await import("@stacksjs/path"),file=projectPath("app/Routes.ts");if(!existsSync(file))return DEFAULT_ROUTE_REGISTRY;const registry=(await import(file)).default;if(!registry||typeof registry!=="object"||Object.keys(registry).length===0){log.debug("[route-loader] app/Routes.ts exports no registry; using the default");return DEFAULT_ROUTE_REGISTRY}return registry}export async function loadRoutes(registry){for(const[key,definition]of Object.entries(registry)){const config=normalizeDefinition(definition),prefix=config.prefix!==void 0?config.prefix?config.prefix.startsWith("/")?config.prefix:`/${config.prefix}`:void 0:key==="web"?void 0:`/${key}`,middleware=normalizeMiddleware(config.middleware);log.debug(`[route-loader] Loading: ${config.path} prefix=${prefix||"/"} middleware=[${middleware.join(", ")}]`);try{if(prefix||middleware.length>0)await route.group({prefix,middleware:middleware.length>0?middleware:void 0},async()=>{await importRouteFile(config.path)});else{const before=registeredRouteKeys();await importRouteFile(config.path);for(const entry of currentRouteTable()){const method=String(entry.method??"").toUpperCase(),path=String(entry.path??"");if(!method||!path||before.has(`${method} ${path}`))continue;(rootMountedAppRoutes??=[]).push({method,path,file:`routes/${config.path}.ts`})}}}catch(error){const message=error instanceof Error?error.message:String(error);console.error(`[Routes] Failed to load route file '${config.path}': ${message}`);throw Error(`Route loading failed for '${config.path}': ${message}`)}}await loadFrameworkRoutes()}export const DEFAULT_ROUTE_BUNDLES=["auth","dashboard","delivery","email","forms"],OPT_IN_ROUTE_BUNDLES=["social"],DEFAULT_ROUTE_BUNDLES_KEY="__stacksDefaultRouteBundles";export function resolveDefaultRouteBundles(env=process.env){return resolveDefaultRouteBundlesWithDiagnostics(env).bundles}export function resolveDefaultRouteBundlesWithDiagnostics(env=process.env){const all=()=>new Set(DEFAULT_ROUTE_BUNDLES),raw=env.STACKS_DEFAULT_ROUTES;if(raw!==void 0){const names=raw.split(",").map((name)=>name.trim().toLowerCase()).filter(Boolean);if(names.length===0||names.includes("none"))return{bundles:new Set,unknown:[],explicit:!0};if(names.includes("all"))return{bundles:all(),unknown:[],explicit:!0};const bundles=new Set,unknown=[];for(const name of names)if(DEFAULT_ROUTE_BUNDLES.includes(name)||OPT_IN_ROUTE_BUNDLES.includes(name))bundles.add(name);else unknown.push(name);return{bundles,unknown,explicit:!0}}if(env.STACKS_SKIP_DEFAULT_ROUTES==="1")return{bundles:new Set,unknown:[],explicit:!0};return{bundles:all(),unknown:[],explicit:!1}}export const DEFAULT_ROUTE_BUNDLE_FEATURES={auth:"dashboard",dashboard:"dashboard",delivery:"commerce",email:"email",forms:"forms"};export function bundleMounts(selection,bundle,featureEnabled){if(selection&&!selection.bundles.has(bundle))return!1;return selection?.explicit?!0:featureEnabled}export function mountedDefaultRouteBundles(featureEnabled,env=process.env){const selection=resolveDefaultRouteBundlesWithDiagnostics(env),mounted=new Set;for(const bundle of DEFAULT_ROUTE_BUNDLES)if(bundleMounts(selection,bundle,featureEnabled(DEFAULT_ROUTE_BUNDLE_FEATURES[bundle])))mounted.add(bundle);return mounted}async function loadFrameworkRoutes(){const{bundles,unknown,explicit}=resolveDefaultRouteBundlesWithDiagnostics();for(const name of unknown)log.warn(`[Routes] STACKS_DEFAULT_ROUTES lists unknown bundle "${name}" - known bundles are ${DEFAULT_ROUTE_BUNDLES.join(", ")}.`);if(bundles.size===0)return;globalThis[DEFAULT_ROUTE_BUNDLES_KEY]={bundles,explicit};try{try{const{overridesReady}=await import("@stacksjs/config");await overridesReady}catch{}const{frameworkPath}=await import("@stacksjs/path"),bootstrapPath=frameworkPath("defaults/bootstrap.ts");if(await Bun.file(bootstrapPath).exists())await import(bootstrapPath)}catch(error){const message=error instanceof Error?error.message:String(error);if(!message.includes("Cannot find module")&&!message.includes("MODULE_NOT_FOUND"))console.error(`[Routes] Failed to load framework bootstrap: ${message}`)}}function assertSafeRouteName(routeName){if(typeof routeName!=="string"||routeName.length===0)throw Error("[route-loader] Invalid route path: empty or non-string");if(routeName.includes("\x00"))throw Error("[route-loader] Invalid route path: null byte");let decoded;try{decoded=decodeURIComponent(routeName)}catch{throw Error("[route-loader] Invalid route path: malformed URL encoding")}const cleanPath=decoded.replace(/\.ts$/,"");if(cleanPath.startsWith("/")||/^[A-Za-z]:[\\/]/.test(cleanPath))throw Error(`[route-loader] Invalid route path: absolute paths not allowed (${cleanPath})`);if(cleanPath.split(/[/\\]/).some((s)=>s===".."))throw Error(`[route-loader] Invalid route path: '..' segment not allowed (${cleanPath})`);return cleanPath}async function importRouteFile(routeName){const cleanPath=assertSafeRouteName(routeName),{projectPath}=await import("@stacksjs/path");await import(projectPath(`routes/${cleanPath}`))}function normalizeDefinition(def){if(typeof def==="string")return{path:def};return def}function normalizeMiddleware(middleware){if(!middleware)return[];if(typeof middleware==="string")return[middleware];return middleware}function currentRouteTable(){return route.routes??[]}function registeredRouteKeys(){const keys=new Set;for(const entry of currentRouteTable())keys.add(`${String(entry.method??"").toUpperCase()} ${String(entry.path??"")}`);return keys}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/router",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.9",
5
+ "version": "0.75.11",
6
6
  "description": "The Stacks framework router.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -67,27 +67,27 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@stacksjs/bun-router": "^0.1.18",
70
- "@stacksjs/cache": "0.75.9",
71
- "@stacksjs/collections": "0.75.9",
72
- "@stacksjs/config": "0.75.9",
73
- "@stacksjs/error-handling": "0.75.9",
74
- "@stacksjs/logging": "0.75.9",
75
- "@stacksjs/pagination": "0.75.9",
76
- "@stacksjs/path": "0.75.9",
77
- "@stacksjs/security": "0.75.9",
78
- "@stacksjs/storage": "0.75.9",
70
+ "@stacksjs/cache": "0.75.11",
71
+ "@stacksjs/collections": "0.75.11",
72
+ "@stacksjs/config": "0.75.11",
73
+ "@stacksjs/error-handling": "0.75.11",
74
+ "@stacksjs/logging": "0.75.11",
75
+ "@stacksjs/pagination": "0.75.11",
76
+ "@stacksjs/path": "0.75.11",
77
+ "@stacksjs/security": "0.75.11",
78
+ "@stacksjs/storage": "0.75.11",
79
79
  "ts-rate-limiter": "^0.4.10"
80
80
  },
81
81
  "devDependencies": {
82
- "@stacksjs/actions": "0.75.9",
83
- "@stacksjs/types": "0.75.9",
82
+ "@stacksjs/actions": "0.75.11",
83
+ "@stacksjs/types": "0.75.11",
84
84
  "better-dx": "^0.2.24"
85
85
  },
86
86
  "peerDependencies": {
87
- "@stacksjs/api": "0.75.9",
88
- "@stacksjs/auth": "0.75.9",
89
- "@stacksjs/database": "0.75.9",
90
- "@stacksjs/validation": "0.75.9"
87
+ "@stacksjs/api": "0.75.11",
88
+ "@stacksjs/auth": "0.75.11",
89
+ "@stacksjs/database": "0.75.11",
90
+ "@stacksjs/validation": "0.75.11"
91
91
  },
92
92
  "peerDependenciesMeta": {
93
93
  "@stacksjs/api": {