@getstrata/core 2.2.13 → 2.2.14
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/CHANGELOG.md +6 -0
- package/dist/core/http/index.d.ts +2 -2
- package/dist/core/http/route.d.ts +64 -0
- package/dist/core/view/index.d.ts +1 -1
- package/dist/core/view/viewRenderer.d.ts +7 -1
- package/dist/entries/http/corsMiddleware.js +10 -1
- package/dist/entries/http/route.js +45 -1
- package/dist/framework/public-api.d.ts +3 -3
- package/dist/index.js +73 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.14
|
|
4
|
+
|
|
5
|
+
Complete the scoped HTML rendering starter adoption: generated apps inject ViewRenderer through createViewProvider instead of copying renderPage; welcome pages opt into HTMX fragments with full-page fallbacks, auth retains existing contracts, and CORS preserves representation Vary headers. Includes generator, runtime auth and cache-header regressions.
|
|
6
|
+
|
|
7
|
+
Add optional typed resourceRoutes and strict mergeRouteMaps composition, preserving native Bun routing and app-owned middleware. Add explicit ViewRenderer.namespace prefixes with unchanged layout/Response/HTMX contracts; generated auth controllers use the namespace. Includes native HTTP, compile-time, rendering isolation and generator regressions.
|
|
8
|
+
|
|
3
9
|
## 2.2.13
|
|
4
10
|
|
|
5
11
|
- Improve view rendering and better htmx support
|
|
@@ -14,8 +14,8 @@ export { parseMultipartUpload, sanitizeUploadFileName, validateUploadFile, } fro
|
|
|
14
14
|
export { createRequireAuthMiddleware } from "./requireAuthMiddleware";
|
|
15
15
|
export { serializeDate, toPaginatedResourceCollection, toResourceCollection } from "./resources";
|
|
16
16
|
export { createdResponse, errorResponse, jsonResponse, noContentResponse, withErrorHandling, } from "./response";
|
|
17
|
-
export type { RouteRequest } from "./route";
|
|
18
|
-
export { getRouteParams } from "./route";
|
|
17
|
+
export type { ResourceActions, ResourceMemberHandler, ResourceRouteMap, RouteRequest, } from "./route";
|
|
18
|
+
export { getRouteParams, mergeRouteMaps, resourceRoutes } from "./route";
|
|
19
19
|
export { withMiddleware } from "./routeMiddleware";
|
|
20
20
|
export { bindRouteModel } from "./routeModelBinding";
|
|
21
21
|
export { securedBindRouteModel, securedBindRouteModelByKey, } from "./securedRouteModelBinding";
|
|
@@ -1,3 +1,67 @@
|
|
|
1
|
+
import type { RouteHandler } from "./middleware";
|
|
2
|
+
type ResourceMemberHandler = RouteHandler<RouteRequest<{
|
|
3
|
+
id: string;
|
|
4
|
+
}>>;
|
|
5
|
+
interface ResourceActions {
|
|
6
|
+
index?: RouteHandler;
|
|
7
|
+
create?: RouteHandler;
|
|
8
|
+
store?: RouteHandler;
|
|
9
|
+
show?: ResourceMemberHandler;
|
|
10
|
+
edit?: ResourceMemberHandler;
|
|
11
|
+
update?: ResourceMemberHandler;
|
|
12
|
+
destroy?: ResourceMemberHandler;
|
|
13
|
+
}
|
|
14
|
+
type ResourceRouteMap<P extends string, A extends ResourceActions> = (A extends {
|
|
15
|
+
index: infer H;
|
|
16
|
+
} ? {
|
|
17
|
+
[K in P]: {
|
|
18
|
+
GET: H;
|
|
19
|
+
};
|
|
20
|
+
} : Record<never, never>) & (A extends {
|
|
21
|
+
store: infer H;
|
|
22
|
+
} ? {
|
|
23
|
+
[K in P]: {
|
|
24
|
+
POST: H;
|
|
25
|
+
};
|
|
26
|
+
} : Record<never, never>) & (A extends {
|
|
27
|
+
create: infer H;
|
|
28
|
+
} ? {
|
|
29
|
+
[K in `${P}/create`]: {
|
|
30
|
+
GET: H;
|
|
31
|
+
};
|
|
32
|
+
} : Record<never, never>) & (A extends {
|
|
33
|
+
show: infer H;
|
|
34
|
+
} ? {
|
|
35
|
+
[K in `${P}/:id`]: {
|
|
36
|
+
GET: H;
|
|
37
|
+
};
|
|
38
|
+
} : Record<never, never>) & (A extends {
|
|
39
|
+
edit: infer H;
|
|
40
|
+
} ? {
|
|
41
|
+
[K in `${P}/:id/edit`]: {
|
|
42
|
+
GET: H;
|
|
43
|
+
};
|
|
44
|
+
} : Record<never, never>) & (A extends {
|
|
45
|
+
update: infer H;
|
|
46
|
+
} ? {
|
|
47
|
+
[K in `${P}/:id`]: {
|
|
48
|
+
PUT: H;
|
|
49
|
+
PATCH: H;
|
|
50
|
+
};
|
|
51
|
+
} : Record<never, never>) & (A extends {
|
|
52
|
+
destroy: infer H;
|
|
53
|
+
} ? {
|
|
54
|
+
[K in `${P}/:id`]: {
|
|
55
|
+
DELETE: H;
|
|
56
|
+
};
|
|
57
|
+
} : Record<never, never>);
|
|
58
|
+
/** Expand only explicitly supplied actions into native Bun routes. */
|
|
59
|
+
declare function resourceRoutes<const P extends string, const A extends ResourceActions>(path: P, actions: A & Record<Exclude<keyof A, keyof ResourceActions>, never>): ResourceRouteMap<P, A>;
|
|
60
|
+
type Intersection<U> = (U extends unknown ? (value: U) => void : never) extends (value: infer I) => void ? I : never;
|
|
61
|
+
/** Strict composition: duplicate paths fail rather than silently replacing handlers. */
|
|
62
|
+
declare function mergeRouteMaps<const T extends readonly Record<string, unknown>[]>(...maps: T): Intersection<T[number]>;
|
|
63
|
+
export type { ResourceActions, ResourceMemberHandler, ResourceRouteMap };
|
|
64
|
+
export { mergeRouteMaps, resourceRoutes };
|
|
1
65
|
type RouteRequest<TParams extends Record<string, string>> = Request & {
|
|
2
66
|
params: TParams;
|
|
3
67
|
};
|
|
@@ -2,7 +2,7 @@ export type { LayoutDataResolver, RenderOptions } from "./etaViewEngine";
|
|
|
2
2
|
export { DEFAULT_VIEWS_DIRECTORY, EtaViewEngine } from "./etaViewEngine";
|
|
3
3
|
export { htmlResponse, isHtmxRequest, redirectResponse, rssResponse, textResponse, xmlResponse, } from "./htmlResponse";
|
|
4
4
|
export type { ViewEngine } from "./viewEngine";
|
|
5
|
-
export type { HtmxViewResponseOptions, ViewResponseOptions } from "./viewRenderer";
|
|
5
|
+
export type { HtmxViewResponseOptions, NamespacedViewRenderer, ViewResponseOptions, } from "./viewRenderer";
|
|
6
6
|
export { CORE_VIEW_RENDERER_TOKEN, ViewRenderer } from "./viewRenderer";
|
|
7
7
|
export type { WebErrorViewInput, WebErrorViewOptions } from "./webErrorView";
|
|
8
8
|
export { configureWebErrorView, errorTemplateName, htmlErrorResponse, notFoundHtmlResponse, renderKernelErrorChrome, renderWebErrorHtml, } from "./webErrorView";
|
|
@@ -10,15 +10,21 @@ interface HtmxViewResponseOptions extends ViewResponseOptions {
|
|
|
10
10
|
/** Optional fragment template; otherwise render the page template without its layout. */
|
|
11
11
|
fragment?: string;
|
|
12
12
|
}
|
|
13
|
+
interface NamespacedViewRenderer {
|
|
14
|
+
render: ViewRenderer["render"];
|
|
15
|
+
renderHtmx: ViewRenderer["renderHtmx"];
|
|
16
|
+
}
|
|
13
17
|
/** App-owned renderer; request data stays in the engine's async resolver. */
|
|
14
18
|
declare class ViewRenderer {
|
|
15
19
|
private readonly engine;
|
|
16
20
|
private readonly defaultLayout?;
|
|
17
21
|
constructor(engine: ViewEngine, defaultLayout?: string | false | undefined);
|
|
22
|
+
/** Explicit template prefix; layout names and response contracts remain unchanged. */
|
|
23
|
+
namespace(prefix: string): NamespacedViewRenderer;
|
|
18
24
|
render(name: string, data?: Record<string, unknown>, options?: ViewResponseOptions): Promise<Response>;
|
|
19
25
|
/** Opt-in representation selection; auth/CSRF/tenancy remain the route's responsibility. */
|
|
20
26
|
renderHtmx(name: string, data?: Record<string, unknown>, options?: HtmxViewResponseOptions): Promise<Response>;
|
|
21
27
|
}
|
|
22
28
|
declare const CORE_VIEW_RENDERER_TOKEN: import("../contracts/container").ServiceToken<ViewRenderer>;
|
|
23
|
-
export type { HtmxViewResponseOptions, ViewResponseOptions };
|
|
29
|
+
export type { HtmxViewResponseOptions, NamespacedViewRenderer, ViewResponseOptions };
|
|
24
30
|
export { CORE_VIEW_RENDERER_TOKEN, ViewRenderer };
|
|
@@ -141,7 +141,16 @@ function createCorsMiddleware(options = {}) {
|
|
|
141
141
|
const response = await next();
|
|
142
142
|
const headers = new Headers(response.headers);
|
|
143
143
|
for (const [key, value] of buildCorsHeaders(request, captured)) {
|
|
144
|
-
|
|
144
|
+
if (key === "vary") {
|
|
145
|
+
const fields = (headers.get("Vary") ?? "").split(",").map((field) => field.trim()).filter(Boolean);
|
|
146
|
+
if (fields.includes("*"))
|
|
147
|
+
continue;
|
|
148
|
+
if (!fields.some((field) => field.toLowerCase() === "origin"))
|
|
149
|
+
fields.push(value);
|
|
150
|
+
headers.set("Vary", fields.join(", "));
|
|
151
|
+
} else {
|
|
152
|
+
headers.set(key, value);
|
|
153
|
+
}
|
|
145
154
|
}
|
|
146
155
|
return new Response(response.body, {
|
|
147
156
|
status: response.status,
|
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/http/route.ts
|
|
3
|
+
function resourceRoutes(path, actions) {
|
|
4
|
+
if (!/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(path) || path.split("/").some((part) => part === "." || part === "..")) {
|
|
5
|
+
throw new TypeError("Resource path must be a static absolute path without a trailing slash.");
|
|
6
|
+
}
|
|
7
|
+
const routes = {};
|
|
8
|
+
const definitions = {
|
|
9
|
+
index: [path, ["GET"]],
|
|
10
|
+
create: [`${path}/create`, ["GET"]],
|
|
11
|
+
store: [path, ["POST"]],
|
|
12
|
+
show: [`${path}/:id`, ["GET"]],
|
|
13
|
+
edit: [`${path}/:id/edit`, ["GET"]],
|
|
14
|
+
update: [`${path}/:id`, ["PUT", "PATCH"]],
|
|
15
|
+
destroy: [`${path}/:id`, ["DELETE"]]
|
|
16
|
+
};
|
|
17
|
+
for (const [action, handler] of Object.entries(actions)) {
|
|
18
|
+
if (!Object.hasOwn(definitions, action) || typeof handler !== "function") {
|
|
19
|
+
throw new TypeError(`Invalid resource action: ${action}`);
|
|
20
|
+
}
|
|
21
|
+
const [route, methods] = definitions[action];
|
|
22
|
+
const methodMap = routes[route] ?? {};
|
|
23
|
+
routes[route] = methodMap;
|
|
24
|
+
for (const method of methods)
|
|
25
|
+
methodMap[method] = handler;
|
|
26
|
+
}
|
|
27
|
+
return routes;
|
|
28
|
+
}
|
|
29
|
+
function mergeRouteMaps(...maps) {
|
|
30
|
+
const routes = {};
|
|
31
|
+
for (const map of maps) {
|
|
32
|
+
for (const [path, handler] of Object.entries(map)) {
|
|
33
|
+
if (Object.hasOwn(routes, path))
|
|
34
|
+
throw new Error(`Duplicate route path: ${path}`);
|
|
35
|
+
Object.defineProperty(routes, path, {
|
|
36
|
+
value: handler,
|
|
37
|
+
enumerable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
configurable: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return routes;
|
|
44
|
+
}
|
|
3
45
|
function getRouteParams(request) {
|
|
4
46
|
return request.params;
|
|
5
47
|
}
|
|
6
48
|
export {
|
|
7
|
-
getRouteParams
|
|
49
|
+
getRouteParams,
|
|
50
|
+
mergeRouteMaps,
|
|
51
|
+
resourceRoutes
|
|
8
52
|
};
|
|
@@ -107,8 +107,8 @@ export { createRequireGlobalAdminMiddleware } from "../core/http/requireGlobalAd
|
|
|
107
107
|
export { createRequireWebAuthMiddleware } from "../core/http/requireWebAuthMiddleware.ts";
|
|
108
108
|
export { JsonResource, ResourceCollection, serializeDate, toPaginatedResourceCollection, toResourceCollection, whenLoaded, } from "../core/http/resources.ts";
|
|
109
109
|
export { createdResponse, errorResponse, jsonResponse, noContentResponse, withErrorHandling, } from "../core/http/response.ts";
|
|
110
|
-
export type { RouteRequest } from "../core/http/route.ts";
|
|
111
|
-
export { getRouteParams } from "../core/http/route.ts";
|
|
110
|
+
export type { ResourceActions, ResourceMemberHandler, ResourceRouteMap, RouteRequest, } from "../core/http/route.ts";
|
|
111
|
+
export { getRouteParams, mergeRouteMaps, resourceRoutes } from "../core/http/route.ts";
|
|
112
112
|
export { loginRedirectLocation, safeInternalRedirectPath, sanitizeInternalPath, } from "../core/http/safeInternalPath.ts";
|
|
113
113
|
export { createScimThrottleMiddleware } from "../core/http/scimThrottleMiddleware.ts";
|
|
114
114
|
export { createSecurityHeadersMiddleware } from "../core/http/securityHeadersMiddleware.ts";
|
|
@@ -171,5 +171,5 @@ export type { ValidationRule, ValidationSchema } from "../core/validation/rules.
|
|
|
171
171
|
export { emailRule, maxLength, minLength, required, stringRule, validateObject, } from "../core/validation/rules.ts";
|
|
172
172
|
export { CORE_VIEW_RENDERER_TOKEN, configureWebErrorView, configureWebLayoutData, createWebLayoutDataResolver, DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, errorTemplateName, htmlErrorResponse, htmlResponse, isHtmxRequest, notFoundHtmlResponse, redirectResponse, renderKernelErrorChrome, renderWebErrorHtml, resolveWebLayoutData, rssResponse, textResponse, ViewRenderer, xmlResponse, } from "../core/view/index.ts";
|
|
173
173
|
export type { ViewEngine } from "../core/view/viewEngine.ts";
|
|
174
|
-
export type { HtmxViewResponseOptions, ViewResponseOptions } from "../core/view/viewRenderer.ts";
|
|
174
|
+
export type { HtmxViewResponseOptions, NamespacedViewRenderer, ViewResponseOptions, } from "../core/view/viewRenderer.ts";
|
|
175
175
|
export type { WebLayoutAuthUser, WebLayoutData, WebLayoutDataOptions, WebLayoutUserKey, } from "../core/view/webLayoutData.ts";
|
package/dist/index.js
CHANGED
|
@@ -11352,7 +11352,16 @@ function createCorsMiddleware(options = {}) {
|
|
|
11352
11352
|
const response = await next();
|
|
11353
11353
|
const headers = new Headers(response.headers);
|
|
11354
11354
|
for (const [key, value] of buildCorsHeaders(request, captured)) {
|
|
11355
|
-
|
|
11355
|
+
if (key === "vary") {
|
|
11356
|
+
const fields = (headers.get("Vary") ?? "").split(",").map((field) => field.trim()).filter(Boolean);
|
|
11357
|
+
if (fields.includes("*"))
|
|
11358
|
+
continue;
|
|
11359
|
+
if (!fields.some((field) => field.toLowerCase() === "origin"))
|
|
11360
|
+
fields.push(value);
|
|
11361
|
+
headers.set("Vary", fields.join(", "));
|
|
11362
|
+
} else {
|
|
11363
|
+
headers.set(key, value);
|
|
11364
|
+
}
|
|
11356
11365
|
}
|
|
11357
11366
|
return new Response(response.body, {
|
|
11358
11367
|
status: response.status,
|
|
@@ -12390,6 +12399,48 @@ function withErrorHandling(handler) {
|
|
|
12390
12399
|
};
|
|
12391
12400
|
}
|
|
12392
12401
|
// ../../src/core/http/route.ts
|
|
12402
|
+
function resourceRoutes(path, actions) {
|
|
12403
|
+
if (!/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(path) || path.split("/").some((part) => part === "." || part === "..")) {
|
|
12404
|
+
throw new TypeError("Resource path must be a static absolute path without a trailing slash.");
|
|
12405
|
+
}
|
|
12406
|
+
const routes = {};
|
|
12407
|
+
const definitions = {
|
|
12408
|
+
index: [path, ["GET"]],
|
|
12409
|
+
create: [`${path}/create`, ["GET"]],
|
|
12410
|
+
store: [path, ["POST"]],
|
|
12411
|
+
show: [`${path}/:id`, ["GET"]],
|
|
12412
|
+
edit: [`${path}/:id/edit`, ["GET"]],
|
|
12413
|
+
update: [`${path}/:id`, ["PUT", "PATCH"]],
|
|
12414
|
+
destroy: [`${path}/:id`, ["DELETE"]]
|
|
12415
|
+
};
|
|
12416
|
+
for (const [action, handler] of Object.entries(actions)) {
|
|
12417
|
+
if (!Object.hasOwn(definitions, action) || typeof handler !== "function") {
|
|
12418
|
+
throw new TypeError(`Invalid resource action: ${action}`);
|
|
12419
|
+
}
|
|
12420
|
+
const [route, methods] = definitions[action];
|
|
12421
|
+
const methodMap = routes[route] ?? {};
|
|
12422
|
+
routes[route] = methodMap;
|
|
12423
|
+
for (const method of methods)
|
|
12424
|
+
methodMap[method] = handler;
|
|
12425
|
+
}
|
|
12426
|
+
return routes;
|
|
12427
|
+
}
|
|
12428
|
+
function mergeRouteMaps(...maps) {
|
|
12429
|
+
const routes = {};
|
|
12430
|
+
for (const map of maps) {
|
|
12431
|
+
for (const [path, handler] of Object.entries(map)) {
|
|
12432
|
+
if (Object.hasOwn(routes, path))
|
|
12433
|
+
throw new Error(`Duplicate route path: ${path}`);
|
|
12434
|
+
Object.defineProperty(routes, path, {
|
|
12435
|
+
value: handler,
|
|
12436
|
+
enumerable: true,
|
|
12437
|
+
writable: true,
|
|
12438
|
+
configurable: true
|
|
12439
|
+
});
|
|
12440
|
+
}
|
|
12441
|
+
}
|
|
12442
|
+
return routes;
|
|
12443
|
+
}
|
|
12393
12444
|
function getRouteParams(request) {
|
|
12394
12445
|
return request.params;
|
|
12395
12446
|
}
|
|
@@ -16119,6 +16170,11 @@ class EtaViewEngine {
|
|
|
16119
16170
|
}
|
|
16120
16171
|
}
|
|
16121
16172
|
// ../../src/core/view/viewRenderer.ts
|
|
16173
|
+
function validateRelativeViewName(name) {
|
|
16174
|
+
if (!/^[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(name) || name.split("/").some((part) => part === "." || part === "..")) {
|
|
16175
|
+
throw new TypeError("View namespace and names must be relative template paths without dot segments.");
|
|
16176
|
+
}
|
|
16177
|
+
}
|
|
16122
16178
|
var HTMX_VARY_HEADERS = ["HX-Request", "HX-Boosted", "HX-History-Restore-Request"];
|
|
16123
16179
|
function varyByHtmx(headers) {
|
|
16124
16180
|
const existing = (headers.get("Vary") ?? "").split(",").map((field) => field.trim());
|
|
@@ -16139,6 +16195,20 @@ class ViewRenderer {
|
|
|
16139
16195
|
this.engine = engine;
|
|
16140
16196
|
this.defaultLayout = defaultLayout;
|
|
16141
16197
|
}
|
|
16198
|
+
namespace(prefix) {
|
|
16199
|
+
validateRelativeViewName(prefix);
|
|
16200
|
+
const qualify = (name) => {
|
|
16201
|
+
validateRelativeViewName(name);
|
|
16202
|
+
return `${prefix}/${name}`;
|
|
16203
|
+
};
|
|
16204
|
+
return {
|
|
16205
|
+
render: async (name, data, options) => this.render(qualify(name), data, options),
|
|
16206
|
+
renderHtmx: async (name, data, options = {}) => this.renderHtmx(qualify(name), data, {
|
|
16207
|
+
...options,
|
|
16208
|
+
...options.fragment === undefined ? {} : { fragment: qualify(options.fragment) }
|
|
16209
|
+
})
|
|
16210
|
+
};
|
|
16211
|
+
}
|
|
16142
16212
|
async render(name, data = {}, options = {}) {
|
|
16143
16213
|
const html = await this.engine.render(name, data, {
|
|
16144
16214
|
request: options.request,
|
|
@@ -16534,6 +16604,7 @@ export {
|
|
|
16534
16604
|
maxLength,
|
|
16535
16605
|
membershipContext,
|
|
16536
16606
|
membershipRepository,
|
|
16607
|
+
mergeRouteMaps,
|
|
16537
16608
|
migrateDatabase,
|
|
16538
16609
|
minLength,
|
|
16539
16610
|
modelEventName,
|
|
@@ -16636,6 +16707,7 @@ export {
|
|
|
16636
16707
|
resolveUserId,
|
|
16637
16708
|
resolveUserTenantId,
|
|
16638
16709
|
resolveWebLayoutData,
|
|
16710
|
+
resourceRoutes,
|
|
16639
16711
|
rollbackDatabase,
|
|
16640
16712
|
rssResponse,
|
|
16641
16713
|
runDueScheduledTasks,
|