@gracile/engine 0.9.0 → 0.9.1-next.0

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 (62) hide show
  1. package/dist/dev/development.d.ts.map +1 -1
  2. package/dist/dev/development.js +1 -1
  3. package/dist/dev/ssr-ce-tracker.d.ts +27 -0
  4. package/dist/dev/ssr-ce-tracker.d.ts.map +1 -0
  5. package/dist/dev/ssr-ce-tracker.js +113 -0
  6. package/dist/plugin.d.ts.map +1 -1
  7. package/dist/plugin.js +51 -264
  8. package/dist/render/route-template-pipeline.d.ts +64 -0
  9. package/dist/render/route-template-pipeline.d.ts.map +1 -0
  10. package/dist/render/route-template-pipeline.js +144 -0
  11. package/dist/render/route-template.d.ts +1 -2
  12. package/dist/render/route-template.d.ts.map +1 -1
  13. package/dist/render/route-template.js +37 -92
  14. package/dist/routes/collect.d.ts +5 -1
  15. package/dist/routes/collect.d.ts.map +1 -1
  16. package/dist/routes/collect.js +8 -6
  17. package/dist/routes/load-module.d.ts.map +1 -1
  18. package/dist/routes/load-module.js +5 -2
  19. package/dist/routes/match.d.ts +31 -1
  20. package/dist/routes/match.d.ts.map +1 -1
  21. package/dist/routes/match.js +22 -4
  22. package/dist/routes/render.d.ts.map +1 -1
  23. package/dist/routes/render.js +11 -3
  24. package/dist/server/request-pipeline.d.ts +109 -0
  25. package/dist/server/request-pipeline.d.ts.map +1 -0
  26. package/dist/server/request-pipeline.js +198 -0
  27. package/dist/server/request.d.ts +3 -16
  28. package/dist/server/request.d.ts.map +1 -1
  29. package/dist/server/request.js +74 -171
  30. package/dist/test/init.d.ts +2 -0
  31. package/dist/test/init.d.ts.map +1 -0
  32. package/dist/test/init.js +7 -0
  33. package/dist/user-config.d.ts +13 -0
  34. package/dist/user-config.d.ts.map +1 -1
  35. package/dist/vite/build-routes.d.ts +1 -2
  36. package/dist/vite/build-routes.d.ts.map +1 -1
  37. package/dist/vite/build-routes.js +0 -98
  38. package/dist/vite/plugin-build-environment.d.ts +21 -0
  39. package/dist/vite/plugin-build-environment.d.ts.map +1 -0
  40. package/dist/vite/plugin-build-environment.js +83 -0
  41. package/dist/vite/plugin-ce-tracker.d.ts +19 -0
  42. package/dist/vite/plugin-ce-tracker.d.ts.map +1 -0
  43. package/dist/vite/plugin-ce-tracker.js +87 -0
  44. package/dist/vite/plugin-client-build.d.ts +20 -0
  45. package/dist/vite/plugin-client-build.d.ts.map +1 -0
  46. package/dist/vite/plugin-client-build.js +55 -0
  47. package/dist/vite/plugin-html-routes-build.d.ts +22 -0
  48. package/dist/vite/plugin-html-routes-build.d.ts.map +1 -0
  49. package/dist/vite/plugin-html-routes-build.js +140 -0
  50. package/dist/vite/plugin-serve.d.ts +18 -0
  51. package/dist/vite/plugin-serve.d.ts.map +1 -0
  52. package/dist/vite/plugin-serve.js +61 -0
  53. package/dist/vite/plugin-server-build.d.ts +43 -0
  54. package/dist/vite/plugin-server-build.d.ts.map +1 -0
  55. package/dist/vite/plugin-server-build.js +108 -0
  56. package/dist/vite/plugin-shared-state.d.ts +33 -0
  57. package/dist/vite/plugin-shared-state.d.ts.map +1 -0
  58. package/dist/vite/plugin-shared-state.js +23 -0
  59. package/dist/vite/virtual-routes.d.ts +8 -5
  60. package/dist/vite/virtual-routes.d.ts.map +1 -1
  61. package/dist/vite/virtual-routes.js +32 -31
  62. package/package.json +5 -5
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Pure, testable pipeline steps extracted from the request handler.
3
+ *
4
+ * Each function here is a focused stage of the Gracile request lifecycle.
5
+ * They are composed by `createGracileHandler` in `./request.ts`.
6
+ *
7
+ * @internal
8
+ */
9
+ import { Readable } from 'node:stream';
10
+ import type { Logger, ViteDevServer } from 'vite';
11
+ import { renderRouteTemplate } from '../render/route-template.js';
12
+ import type { RouteInfos } from '../routes/match.js';
13
+ import type { GracileConfig } from '../user-config.js';
14
+ export type StandardResponse = {
15
+ response: Response;
16
+ body?: never;
17
+ init?: never;
18
+ };
19
+ export type ResponseWithNodeReadable = {
20
+ response?: never;
21
+ body: Readable;
22
+ init: ResponseInit;
23
+ };
24
+ export type HandlerResult = StandardResponse | ResponseWithNodeReadable | null;
25
+ export declare const CONTENT_TYPE_HTML: {
26
+ readonly 'Content-Type': "text/html";
27
+ };
28
+ export declare const PREMISE_REGEXES: {
29
+ readonly properties: RegExp;
30
+ readonly document: RegExp;
31
+ };
32
+ /** Describes which premises endpoint was requested, if any. */
33
+ export interface PremisesDescriptor {
34
+ propertiesOnly: boolean;
35
+ documentOnly: boolean;
36
+ }
37
+ /**
38
+ * Strip the `/__…` suffix so hidden-sibling URLs (premises) resolve to
39
+ * the parent route.
40
+ *
41
+ * @example
42
+ * rewriteHiddenRoutes('http://localhost/blog/__index.props.json')
43
+ * // → 'http://localhost/blog/'
44
+ */
45
+ export declare function rewriteHiddenRoutes(url: string): string;
46
+ /**
47
+ * Determine if the incoming request targets a premises endpoint
48
+ * (`__index.props.json` or `__index.doc.html`) and whether the config
49
+ * allows it.
50
+ *
51
+ * @returns A descriptor when premises are enabled, or `null` when not.
52
+ * @throws When a premise URL is hit but premises are not enabled.
53
+ */
54
+ export declare function resolvePremises(requestedUrl: string, gracileConfig: GracileConfig): PremisesDescriptor | null;
55
+ export interface ExecuteHandlerOptions {
56
+ routeInfos: RouteInfos;
57
+ method: string;
58
+ request: Request;
59
+ fullUrl: string;
60
+ locals: unknown;
61
+ responseInit: ResponseInit;
62
+ premises: PremisesDescriptor | null;
63
+ routeTemplateOptions: Parameters<typeof renderRouteTemplate>[0];
64
+ }
65
+ /**
66
+ * Result from `executeHandler`:
67
+ *
68
+ * - `{ type: 'response', value }` — return this response directly
69
+ * - `{ type: 'output', value }` — pass to response building
70
+ * - `{ type: 'fallthrough' }` — no handler, proceed to template-only render
71
+ */
72
+ export type ExecuteHandlerResult = {
73
+ type: 'response';
74
+ value: StandardResponse;
75
+ } | {
76
+ type: 'output';
77
+ value: Readable | Response | null;
78
+ } | {
79
+ type: 'fallthrough';
80
+ };
81
+ /**
82
+ * Dispatch the user's route handler (top-level function or method-map).
83
+ *
84
+ * This determines whether to call the handler, which method to use,
85
+ * and whether to short-circuit with premises or a 405.
86
+ */
87
+ export declare function executeHandler(options: ExecuteHandlerOptions): Promise<ExecuteHandlerResult>;
88
+ /**
89
+ * Render a page that has no handler — just a document/template.
90
+ * Handles premises short-circuits.
91
+ */
92
+ export declare function renderWithoutHandler(options: {
93
+ premises: PremisesDescriptor | null;
94
+ routeTemplateOptions: Parameters<typeof renderRouteTemplate>[0];
95
+ }): Promise<StandardResponse | Readable | null>;
96
+ export declare function isRedirect(response: Response): {
97
+ location: string;
98
+ } | null;
99
+ /**
100
+ * Convert a handler/render output (Response or Readable stream) into
101
+ * the final `HandlerResult` shape expected by adapters.
102
+ */
103
+ export declare function buildResponse(options: {
104
+ output: Readable | Response | null;
105
+ responseInit: ResponseInit;
106
+ vite: ViteDevServer | undefined;
107
+ logger: Logger;
108
+ }): HandlerResult;
109
+ //# sourceMappingURL=request-pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-pipeline.d.ts","sourceRoot":"","sources":["../../src/server/request-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvC,OAAO,KAAK,EAAc,MAAM,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,MAAM,MAAM,gBAAgB,GAAG;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,IAAI,CAAC,EAAE,KAAK,CAAC;CACb,CAAC;AACF,MAAM,MAAM,wBAAwB,GAAG;IACtC,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,YAAY,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG,wBAAwB,GAAG,IAAI,CAAC;AAE/E,eAAO,MAAM,iBAAiB;;CAA2C,CAAC;AAE1E,eAAO,MAAM,eAAe;;;CAGlB,CAAC;AAEX,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IAClC,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;CACtB;AAID;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAID;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC9B,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,aAAa,GAC1B,kBAAkB,GAAG,IAAI,CAY3B;AAaD,MAAM,WAAW,qBAAqB;IACrC,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,oBAAoB,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;CAChE;AAED;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAC7B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AAE3B;;;;;GAKG;AACH,wBAAsB,cAAc,CACnC,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,oBAAoB,CAAC,CAyF/B;AAID;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IACnD,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,oBAAoB,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;CAChE,GAAG,OAAO,CAAC,gBAAgB,GAAG,QAAQ,GAAG,IAAI,CAAC,CAoB9C;AAID,wBAAgB,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAM1E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACtC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;IACnC,YAAY,EAAE,YAAY,CAAC;IAC3B,IAAI,EAAE,aAAa,GAAG,SAAS,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;CACf,GAAG,aAAa,CAqDhB"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Pure, testable pipeline steps extracted from the request handler.
3
+ *
4
+ * Each function here is a focused stage of the Gracile request lifecycle.
5
+ * They are composed by `createGracileHandler` in `./request.ts`.
6
+ *
7
+ * @internal
8
+ */
9
+ import { Readable } from 'node:stream';
10
+ import * as assert from '@gracile/internal-utils/assertions';
11
+ import { renderRouteTemplate } from '../render/route-template.js';
12
+ export const CONTENT_TYPE_HTML = { 'Content-Type': 'text/html' };
13
+ export const PREMISE_REGEXES = {
14
+ properties: /\/__(.*?)\.props\.json$/,
15
+ document: /\/__(.*?)\.doc\.html$/,
16
+ };
17
+ // ── 1. Rewrite hidden route siblings ─────────────────────────────────
18
+ /**
19
+ * Strip the `/__…` suffix so hidden-sibling URLs (premises) resolve to
20
+ * the parent route.
21
+ *
22
+ * @example
23
+ * rewriteHiddenRoutes('http://localhost/blog/__index.props.json')
24
+ * // → 'http://localhost/blog/'
25
+ */
26
+ export function rewriteHiddenRoutes(url) {
27
+ return url.replace(/\/__(.*)$/, '/');
28
+ }
29
+ // ── 2. Resolve premises ──────────────────────────────────────────────
30
+ /**
31
+ * Determine if the incoming request targets a premises endpoint
32
+ * (`__index.props.json` or `__index.doc.html`) and whether the config
33
+ * allows it.
34
+ *
35
+ * @returns A descriptor when premises are enabled, or `null` when not.
36
+ * @throws When a premise URL is hit but premises are not enabled.
37
+ */
38
+ export function resolvePremises(requestedUrl, gracileConfig) {
39
+ const propertiesOnly = PREMISE_REGEXES.properties.test(requestedUrl);
40
+ const documentOnly = PREMISE_REGEXES.document.test(requestedUrl);
41
+ const allowPremises = Boolean(gracileConfig.pages?.premises?.expose);
42
+ if (allowPremises === false && (propertiesOnly || documentOnly))
43
+ throw new Error('Accessed a page premise but they are not activated. You must enable `pages.premises.expose`.');
44
+ if (allowPremises)
45
+ return { propertiesOnly, documentOnly };
46
+ return null;
47
+ }
48
+ /**
49
+ * Dispatch the user's route handler (top-level function or method-map).
50
+ *
51
+ * This determines whether to call the handler, which method to use,
52
+ * and whether to short-circuit with premises or a 405.
53
+ */
54
+ export async function executeHandler(options) {
55
+ const { routeInfos, method, request, fullUrl, locals, responseInit, premises, routeTemplateOptions, } = options;
56
+ const handler = routeInfos.routeModule.handler;
57
+ const shouldDispatch = ('handler' in routeInfos.routeModule && handler !== undefined) ||
58
+ // NOTE: When a handler exists but has no GET key, and the request
59
+ // method is not GET, we still dispatch to let it handle the method.
60
+ (handler && 'GET' in handler === false && method !== 'GET');
61
+ if (!shouldDispatch)
62
+ return { type: 'fallthrough' };
63
+ // Build the frozen context passed to the user handler.
64
+ let providedLocals = {};
65
+ if (locals && assert.isUnknownObject(locals))
66
+ providedLocals = locals;
67
+ const routeContext = Object.freeze({
68
+ request,
69
+ url: new URL(fullUrl),
70
+ responseInit,
71
+ params: routeInfos.params,
72
+ locals: providedLocals,
73
+ });
74
+ const hasTopLevelHandler = typeof handler === 'function';
75
+ if (!hasTopLevelHandler && !(method in handler)) {
76
+ const statusText = `This route doesn't handle the \`${method}\` method!`;
77
+ return {
78
+ type: 'response',
79
+ value: {
80
+ response: new Response(statusText, { status: 405, statusText }),
81
+ },
82
+ };
83
+ }
84
+ const handlerWithMethod = hasTopLevelHandler
85
+ ? handler
86
+ : handler[method];
87
+ if (typeof handlerWithMethod !== 'function')
88
+ throw new TypeError('Handler must be a function.');
89
+ const handlerOutput = await Promise.resolve(handlerWithMethod(routeContext));
90
+ // User returned a raw Response — pass it through.
91
+ if (assert.isResponseOrPatchedResponse(handlerOutput))
92
+ return { type: 'output', value: handlerOutput };
93
+ // User returned data — merge into routeInfos.props for template rendering.
94
+ routeTemplateOptions.routeInfos.props = hasTopLevelHandler
95
+ ? handlerOutput
96
+ : { [method]: handlerOutput };
97
+ // Short-circuit for premises.
98
+ if (premises?.documentOnly) {
99
+ const { document } = await renderRouteTemplate(routeTemplateOptions);
100
+ return {
101
+ type: 'response',
102
+ value: {
103
+ response: new Response(document, {
104
+ headers: { ...CONTENT_TYPE_HTML },
105
+ }),
106
+ },
107
+ };
108
+ }
109
+ if (premises?.propertiesOnly)
110
+ return {
111
+ type: 'response',
112
+ value: {
113
+ response: Response.json(routeTemplateOptions.routeInfos.props),
114
+ },
115
+ };
116
+ const output = await renderRouteTemplate(routeTemplateOptions).then((r) => r.output);
117
+ return { type: 'output', value: output };
118
+ }
119
+ // ── 4. Render without handler (template-only) ────────────────────────
120
+ /**
121
+ * Render a page that has no handler — just a document/template.
122
+ * Handles premises short-circuits.
123
+ */
124
+ export async function renderWithoutHandler(options) {
125
+ const { premises, routeTemplateOptions } = options;
126
+ if (premises?.documentOnly) {
127
+ const { document } = await renderRouteTemplate(routeTemplateOptions);
128
+ return {
129
+ response: new Response(document, {
130
+ headers: { ...CONTENT_TYPE_HTML },
131
+ }),
132
+ };
133
+ }
134
+ if (premises?.propertiesOnly)
135
+ return {
136
+ response: Response.json(routeTemplateOptions.routeInfos.props || {}),
137
+ };
138
+ const output = await renderRouteTemplate(routeTemplateOptions).then((r) => r.output);
139
+ return output;
140
+ }
141
+ // ── 5. Build the final response from output ──────────────────────────
142
+ export function isRedirect(response) {
143
+ const location = response.headers.get('location');
144
+ if (response.status >= 300 && response.status <= 303 && location) {
145
+ return { location };
146
+ }
147
+ return null;
148
+ }
149
+ /**
150
+ * Convert a handler/render output (Response or Readable stream) into
151
+ * the final `HandlerResult` shape expected by adapters.
152
+ */
153
+ export function buildResponse(options) {
154
+ const { output, responseInit, vite, logger } = options;
155
+ // Direct Response pass-through (e.g. from handler returning Response)
156
+ if (assert.isResponseOrPatchedResponse(output)) {
157
+ const redirect = isRedirect(output);
158
+ if (redirect?.location)
159
+ return {
160
+ response: Response.redirect(redirect.location, output.status),
161
+ };
162
+ return { response: output };
163
+ }
164
+ // Readable stream — the SSR page render
165
+ if (output instanceof Readable) {
166
+ responseInit.headers = {
167
+ ...responseInit.headers,
168
+ ...CONTENT_TYPE_HTML,
169
+ };
170
+ return {
171
+ body: output.on('error', (error) => {
172
+ const errorMessage = `[SSR Error] There was an error while rendering a template chunk on server-side.\n` +
173
+ `It was omitted from the resulting HTML.\n`;
174
+ if (vite) {
175
+ logger.error(errorMessage + error.stack);
176
+ const payload = {
177
+ type: 'error',
178
+ err: {
179
+ name: 'StreamingError',
180
+ message: errorMessage,
181
+ stack: error.stack ?? 'No stack trace available',
182
+ hint: 'This is often caused by a wrong template location dynamic interpolation.',
183
+ cause: error,
184
+ },
185
+ };
186
+ setTimeout(() => {
187
+ vite.hot.send(payload);
188
+ }, 200);
189
+ }
190
+ else {
191
+ logger.error(errorMessage);
192
+ }
193
+ }),
194
+ init: responseInit,
195
+ };
196
+ }
197
+ return null;
198
+ }
@@ -1,27 +1,14 @@
1
- import { Readable } from 'node:stream';
2
1
  import type { Logger, ViteDevServer } from 'vite';
3
2
  import type * as R from '../routes/route.js';
4
3
  import type { GracileConfig } from '../user-config.js';
5
- type StandardResponse = {
6
- response: Response;
7
- body?: never;
8
- init?: never;
9
- };
10
- type ResponseWithNodeReadable = {
11
- response?: never;
12
- body: Readable;
13
- init: ResponseInit;
14
- };
4
+ import { type HandlerResult } from './request-pipeline.js';
15
5
  export interface AdapterOptions {
16
6
  logger?: Logger;
17
7
  }
18
8
  /**
19
9
  * The underlying handler interface that you can use to build your own adapter.
20
10
  */
21
- export type GracileHandler = (request: Request, locals?: unknown) => Promise<StandardResponse | ResponseWithNodeReadable | null>;
22
- export declare function isRedirect(response: Response): {
23
- location: string;
24
- } | null;
11
+ export type GracileHandler = (request: Request, locals?: unknown) => Promise<HandlerResult>;
25
12
  export declare function createGracileHandler({ vite, routes, routeImports, routeAssets, root, serverMode, gracileConfig, }: {
26
13
  vite?: ViteDevServer | undefined;
27
14
  routes: R.RoutesManifest;
@@ -31,5 +18,5 @@ export declare function createGracileHandler({ vite, routes, routeImports, route
31
18
  serverMode?: boolean | undefined;
32
19
  gracileConfig: GracileConfig;
33
20
  }): GracileHandler;
34
- export {};
21
+ export { type StandardResponse, type ResponseWithNodeReadable, isRedirect, } from './request-pipeline.js';
35
22
  //# sourceMappingURL=request.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/server/request.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAOvC,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAQlD,OAAO,KAAK,KAAK,CAAC,MAAM,oBAAoB,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,KAAK,gBAAgB,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,IAAI,CAAC,EAAE,KAAK,CAAC;IAAC,IAAI,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAC3E,KAAK,wBAAwB,GAAG;IAC/B,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,YAAY,CAAC;CACnB,CAAC;AAEF,MAAM,WAAW,cAAc;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAC5B,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,OAAO,KACZ,OAAO,CAAC,gBAAgB,GAAG,wBAAwB,GAAG,IAAI,CAAC,CAAC;AASjE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO1E;AAED,wBAAgB,oBAAoB,CAAC,EACpC,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,WAAW,EACX,IAAI,EACJ,UAAU,EACV,aAAa,GACb,EAAE;IACF,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC,cAAc,CAAC;IACzB,YAAY,CAAC,EAAE,CAAC,CAAC,aAAa,GAAG,SAAS,CAAC;IAC3C,WAAW,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;CAC7B,kBAgRA"}
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/server/request.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAOlD,OAAO,KAAK,KAAK,CAAC,MAAM,oBAAoB,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EACN,KAAK,aAAa,EAOlB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,cAAc;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAC5B,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,OAAO,KACZ,OAAO,CAAC,aAAa,CAAC,CAAC;AAE5B,wBAAgB,oBAAoB,CAAC,EACpC,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,WAAW,EACX,IAAI,EACJ,UAAU,EACV,aAAa,GACb,EAAE;IACF,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC,cAAc,CAAC;IACzB,YAAY,CAAC,EAAE,CAAC,CAAC,aAAa,GAAG,SAAS,CAAC;IAC3C,WAAW,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;CAC7B,kBAuIA;AAyBD,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,wBAAwB,EAC7B,UAAU,GACV,MAAM,uBAAuB,CAAC"}
@@ -1,24 +1,11 @@
1
1
  import { Readable } from 'node:stream';
2
- import * as assert from '@gracile/internal-utils/assertions';
3
2
  import { getLogger } from '@gracile/internal-utils/logger/helpers';
4
3
  import c from 'picocolors';
5
- // import { GracileError } from '../errors/errors.js';
6
4
  import { builtIn404Page, builtInErrorPage } from '../errors/pages.js';
7
5
  import { renderRouteTemplate } from '../render/route-template.js';
8
6
  import { renderLitTemplate } from '../render/lit-ssr.js';
9
7
  import { getRoute } from '../routes/match.js';
10
- const CONTENT_TYPE_HTML = { 'Content-Type': 'text/html' };
11
- const PREMISE_REGEXES = {
12
- properties: /\/__(.*?)\.props\.json$/,
13
- document: /\/__(.*?)\.doc\.html$/,
14
- };
15
- export function isRedirect(response) {
16
- const location = response.headers.get('location');
17
- if (response.status >= 300 && response.status <= 303 && location) {
18
- return { location };
19
- }
20
- return null;
21
- }
8
+ import { CONTENT_TYPE_HTML, rewriteHiddenRoutes, resolvePremises, executeHandler, renderWithoutHandler, buildResponse, } from './request-pipeline.js';
22
9
  export function createGracileHandler({ vite, routes, routeImports, routeAssets, root, serverMode, gracileConfig, }) {
23
10
  const logger = getLogger();
24
11
  const middleware = async (request, locals) => {
@@ -28,33 +15,42 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
28
15
  emitViteBetterError =
29
16
  await import('../errors/create-vite-better-error.js').then(({ emitViteBetterError: error }) => error);
30
17
  try {
31
- // MARK: Rewrite hidden route siblings
32
- const fullUrl = requestedUrl.replace(/\/__(.*)$/, '/');
33
- // MARK: Setup premises
34
- const propertiesOnly = PREMISE_REGEXES.properties.test(requestedUrl);
35
- const documentOnly = PREMISE_REGEXES.document.test(requestedUrl);
36
- const allowPremises = Boolean(gracileConfig.pages?.premises?.expose);
37
- if (allowPremises === false && (propertiesOnly || documentOnly))
38
- throw new Error('Accessed a page premise but they are not activated. You must enable `pages.premises.expose`.');
39
- const premises = allowPremises ? { propertiesOnly, documentOnly } : null;
40
- // MARK: Get route infos
18
+ // MARK: 1. Rewrite hidden route siblings
19
+ const fullUrl = rewriteHiddenRoutes(requestedUrl);
20
+ // MARK: 2. Setup premises
21
+ const premises = resolvePremises(requestedUrl, gracileConfig);
22
+ // MARK: 3. Route resolution + 404 fallback
41
23
  const routeOptions = {
42
24
  url: fullUrl,
43
25
  vite,
44
26
  routes,
45
27
  routeImports,
28
+ trailingSlash: gracileConfig.trailingSlash ?? 'ignore',
46
29
  };
47
30
  const responseInit = {};
48
- let routeInfos = await getRoute(routeOptions);
49
- // MARK: 404
31
+ const routeResult = await getRoute(routeOptions);
32
+ // Trailing slash redirect (301 for GET, 308 for other methods)
33
+ if (routeResult && 'redirect' in routeResult) {
34
+ const redirectUrl = new URL(routeResult.redirect, fullUrl).href;
35
+ const status = method === 'GET' ? 301 : 308;
36
+ return { response: Response.redirect(redirectUrl, status) };
37
+ }
38
+ let routeInfos = routeResult;
50
39
  if (routeInfos === null) {
51
40
  responseInit.status = 404;
41
+ // Use 'ignore' for the internal 404 lookup to avoid redirect loops.
52
42
  const url = new URL('/404/', fullUrl).href;
53
- const options = { ...routeOptions, url };
54
- const notFound = await getRoute(options);
55
- routeInfos = notFound;
43
+ const options = {
44
+ ...routeOptions,
45
+ url,
46
+ trailingSlash: 'ignore',
47
+ };
48
+ const notFoundResult = await getRoute(options);
49
+ routeInfos =
50
+ notFoundResult && !('redirect' in notFoundResult)
51
+ ? notFoundResult
52
+ : null;
56
53
  }
57
- // MARK: fallback 404
58
54
  if (routeInfos === null) {
59
55
  const page = builtIn404Page(new URL(fullUrl).pathname, Boolean(vite));
60
56
  return {
@@ -66,7 +62,7 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
66
62
  const routeTemplateOptions = {
67
63
  url: fullUrl,
68
64
  vite,
69
- mode: 'dev', // vite && vite.config.mode === 'dev' ? 'dev' : 'build',
65
+ mode: 'dev',
70
66
  routeAssets,
71
67
  root,
72
68
  serverMode,
@@ -77,153 +73,38 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
77
73
  logger.info(`[${c.yellow(method)}] ${c.yellow(fullUrl)}`, {
78
74
  timestamp: true,
79
75
  });
76
+ // MARK: 4. Handler dispatch
80
77
  let output;
81
- let providedLocals = {};
82
- if (locals && assert.isUnknownObject(locals))
83
- providedLocals = locals;
84
- // MARK: Server handler
85
- const handler = routeInfos.routeModule.handler;
86
- if (('handler' in routeInfos.routeModule && handler !== undefined) ||
87
- // TODO: Explain this condition
88
- (handler && 'GET' in handler === false && method !== 'GET')) {
89
- const routeContext = Object.freeze({
90
- request,
91
- url: new URL(fullUrl),
92
- responseInit,
93
- params: routeInfos.params,
94
- locals: providedLocals,
95
- });
96
- // MARK: Run user middleware
97
- // NOTE: Experimental
98
- /// eslint-disable-next-line no-inner-declarations
99
- // async function useHandler() {}
100
- // if (vite) {
101
- // const middleware = await vite
102
- // .ssrLoadModule('/src/middleware.ts')
103
- // .catch(() => null)
104
- // .then((m) => m.default);
105
- // if (middleware)
106
- // await middleware(
107
- // routeContext,
108
- // async () => {
109
- // await useHandler();
110
- // },
111
- // );
112
- // else await useHandler();
113
- // } else {
114
- // await useHandler();
115
- // }
116
- //
117
- // MARK: Handler(s)
118
- const hasTopLevelHandler = typeof handler === 'function';
119
- if (hasTopLevelHandler || method in handler) {
120
- const handlerWithMethod = hasTopLevelHandler
121
- ? handler
122
- : handler[method];
123
- if (typeof handlerWithMethod !== 'function')
124
- throw new TypeError('Handler must be a function.');
125
- const handlerOutput = await Promise.resolve(handlerWithMethod(routeContext));
126
- if (assert.isResponseOrPatchedResponse(handlerOutput))
127
- output = handlerOutput;
128
- else {
129
- routeTemplateOptions.routeInfos.props = hasTopLevelHandler
130
- ? handlerOutput
131
- : { [method]: handlerOutput };
132
- if (premises?.documentOnly) {
133
- const { document } = await renderRouteTemplate(routeTemplateOptions);
134
- return {
135
- response: new Response(document, {
136
- headers: { ...CONTENT_TYPE_HTML },
137
- }),
138
- };
139
- }
140
- if (premises?.propertiesOnly)
141
- return {
142
- response: Response.json(routeTemplateOptions.routeInfos.props),
143
- };
144
- output = await renderRouteTemplate(routeTemplateOptions).then((r) => r.output);
145
- }
146
- // MARK: No GET, render page
147
- }
148
- else {
149
- const statusText = `This route doesn't handle the \`${method}\` method!`;
150
- return {
151
- response: new Response(statusText, { status: 405, statusText }),
152
- };
153
- }
78
+ const handlerResult = await executeHandler({
79
+ routeInfos,
80
+ method,
81
+ request,
82
+ fullUrl,
83
+ locals,
84
+ responseInit,
85
+ premises,
86
+ routeTemplateOptions,
87
+ });
88
+ if (handlerResult.type === 'response')
89
+ return handlerResult.value;
90
+ if (handlerResult.type === 'output') {
91
+ output = handlerResult.value;
154
92
  }
155
93
  else {
156
- if (premises?.documentOnly) {
157
- const { document } = await renderRouteTemplate(routeTemplateOptions);
158
- return {
159
- response: new Response(document, {
160
- headers: { ...CONTENT_TYPE_HTML },
161
- }),
162
- };
163
- }
164
- if (premises?.propertiesOnly)
165
- return {
166
- response: Response.json(routeTemplateOptions.routeInfos.props || {}),
167
- };
168
- output = await renderRouteTemplate(routeTemplateOptions).then((r) => r.output);
169
- }
170
- // MARK: Return response
171
- // NOTE: try directly with the requestPonyfill. This might not be necessary
172
- if (assert.isResponseOrPatchedResponse(output)) {
173
- const redirect = isRedirect(output);
174
- if (redirect?.location)
175
- return {
176
- response: Response.redirect(redirect.location, output.status),
177
- };
178
- return { response: output };
179
- // MARK: Stream page render
180
- }
181
- // MARK: Page stream error
182
- if (output instanceof Readable) {
183
- responseInit.headers = {
184
- ...responseInit.headers,
185
- ...CONTENT_TYPE_HTML,
186
- };
187
- return {
188
- body: output.on('error', (error) => {
189
- const errorMessage = `[SSR Error] There was an error while rendering a template chunk on server-side.\n` +
190
- `It was omitted from the resulting HTML.\n`;
191
- if (vite) {
192
- logger.error(errorMessage + error.stack);
193
- // emitViteBetterError(new GracileError(GracileErrorData.FailedToGlobalLogger), vite);
194
- const payload = {
195
- type: 'error',
196
- // FIXME: Use the emitViteBetterError instead (but flaky for now with streaming)
197
- // err: new GracileError({}),
198
- err: {
199
- name: 'StreamingError',
200
- message: errorMessage,
201
- stack: error.stack,
202
- hint: 'This is often caused by a wrong template location dynamic interpolation.',
203
- cause: error,
204
- // highlightedCode: error.message,
205
- },
206
- };
207
- //
208
- setTimeout(() => {
209
- // @ts-expect-error ...........
210
- vite.hot.send(payload);
211
- // NOTE: Arbitrary value. Lower seems to be too fast, higher is not guaranteed to work.
212
- }, 200);
213
- }
214
- else {
215
- logger.error(errorMessage);
216
- }
217
- }),
218
- init: responseInit,
219
- };
94
+ // MARK: 5. Template-only render (no handler)
95
+ const renderResult = await renderWithoutHandler({
96
+ premises,
97
+ routeTemplateOptions,
98
+ });
99
+ if (renderResult && 'response' in renderResult)
100
+ return renderResult;
101
+ output = renderResult;
220
102
  }
221
- return null;
103
+ // MARK: 6. Build final response
104
+ return buildResponse({ output, responseInit, vite, logger });
222
105
  // MARK: Errors
223
106
  }
224
107
  catch (error) {
225
- // const safeError = createSafeError(error);
226
- // TODO: User defined dev/runtime 500 error
227
108
  const ultimateErrorPage = vite && emitViteBetterError
228
109
  ? await emitViteBetterError({ vite, error: error })
229
110
  : await renderLitTemplate(builtInErrorPage(error.name));
@@ -238,3 +119,25 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
238
119
  };
239
120
  return middleware;
240
121
  }
122
+ // MARK: Run user middleware
123
+ // NOTE: Experimental
124
+ /// eslint-disable-next-line no-inner-declarations
125
+ // async function useHandler() {}
126
+ // if (vite) {
127
+ // const middleware = await vite
128
+ // .ssrLoadModule('/src/middleware.ts')
129
+ // .catch(() => null)
130
+ // .then((m) => m.default);
131
+ // if (middleware)
132
+ // await middleware(
133
+ // routeContext,
134
+ // async () => {
135
+ // await useHandler();
136
+ // },
137
+ // );
138
+ // else await useHandler();
139
+ // } else {
140
+ // await useHandler();
141
+ // }
142
+ //
143
+ export { isRedirect, } from './request-pipeline.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/test/init.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Side-effect import: initializes the Gracile logger globally.
3
+ * Import this at the top of any engine unit test that touches modules
4
+ * which call `getLogger()` at module scope.
5
+ */
6
+ import { createLogger } from '@gracile/internal-utils/logger/helpers';
7
+ createLogger();