@gracile/engine 0.9.0-next.4 → 0.9.0-next.5

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 (36) hide show
  1. package/dist/plugin.d.ts.map +1 -1
  2. package/dist/plugin.js +37 -262
  3. package/dist/render/route-template-pipeline.d.ts +64 -0
  4. package/dist/render/route-template-pipeline.d.ts.map +1 -0
  5. package/dist/render/route-template-pipeline.js +144 -0
  6. package/dist/render/route-template.d.ts +1 -2
  7. package/dist/render/route-template.d.ts.map +1 -1
  8. package/dist/render/route-template.js +12 -92
  9. package/dist/routes/collect.d.ts +4 -0
  10. package/dist/routes/collect.d.ts.map +1 -1
  11. package/dist/routes/collect.js +2 -1
  12. package/dist/routes/match.d.ts +26 -0
  13. package/dist/routes/match.d.ts.map +1 -1
  14. package/dist/routes/match.js +4 -2
  15. package/dist/server/request-pipeline.d.ts +109 -0
  16. package/dist/server/request-pipeline.d.ts.map +1 -0
  17. package/dist/server/request-pipeline.js +198 -0
  18. package/dist/server/request.d.ts +3 -16
  19. package/dist/server/request.d.ts.map +1 -1
  20. package/dist/server/request.js +55 -169
  21. package/dist/test/init.d.ts +2 -0
  22. package/dist/test/init.d.ts.map +1 -0
  23. package/dist/test/init.js +7 -0
  24. package/dist/vite/plugin-client-build.d.ts +16 -0
  25. package/dist/vite/plugin-client-build.d.ts.map +1 -0
  26. package/dist/vite/plugin-client-build.js +49 -0
  27. package/dist/vite/plugin-serve.d.ts +18 -0
  28. package/dist/vite/plugin-serve.d.ts.map +1 -0
  29. package/dist/vite/plugin-serve.js +62 -0
  30. package/dist/vite/plugin-server-build.d.ts +33 -0
  31. package/dist/vite/plugin-server-build.d.ts.map +1 -0
  32. package/dist/vite/plugin-server-build.js +157 -0
  33. package/dist/vite/plugin-shared-state.d.ts +31 -0
  34. package/dist/vite/plugin-shared-state.d.ts.map +1 -0
  35. package/dist/vite/plugin-shared-state.js +22 -0
  36. package/package.json +2 -2
@@ -1,20 +1,12 @@
1
1
  import { Readable } from 'node:stream';
2
2
  import * as assert from '@gracile/internal-utils/assertions';
3
- import { html } from '@gracile/internal-utils/dummy-literals';
4
3
  import { render as renderLitSsr } from '@lit-labs/ssr';
5
4
  import { collectResult } from '@lit-labs/ssr/lib/render-result.js';
6
- import { LitElementRenderer } from '@lit-labs/ssr/lib/lit-element-renderer.js';
7
5
  import { GracileError, GracileErrorData, TemplateError, } from '../errors/errors.js';
8
- import { PAGE_ASSETS_MARKER, SSR_OUTLET_MARKER } from './markers.js';
9
- async function* concatStreams(...readables) {
10
- for (const readable of readables) {
11
- for await (const chunk of readable) {
12
- yield chunk;
13
- }
14
- }
15
- }
16
- export const REGEX_TAG_SCRIPT = /\s?<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script\s*>\s?/gi;
17
- export const REGEX_TAG_LINK = /\s?<link\b[^>]*?>\s?/gi;
6
+ import { SSR_OUTLET_MARKER } from './markers.js';
7
+ import { concatStreams, mergeRenderInfo, injectSiblingAssets, ensureDoctype, injectDevelopmentOverlay, injectServerAssets, } from './route-template-pipeline.js';
8
+ // Re-export for consumers that import these regexes from this module.
9
+ export { REGEX_TAG_SCRIPT, REGEX_TAG_LINK } from './route-template-pipeline.js';
18
10
  export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAssets, serverMode, docOnly, renderInfo, }) {
19
11
  const location = {
20
12
  file: routeInfos.foundRoute.filePath,
@@ -22,13 +14,7 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
22
14
  if (!routeInfos.routeModule.document && !routeInfos.routeModule.template)
23
15
  return { output: null, document: null };
24
16
  // MARK: Merged render info
25
- const mergedRenderInfo = {
26
- ...renderInfo,
27
- elementRenderers: [
28
- ...(renderInfo?.elementRenderers || []),
29
- LitElementRenderer,
30
- ],
31
- };
17
+ const mergedRenderInfo = mergeRenderInfo(renderInfo);
32
18
  // MARK: Context
33
19
  const context = {
34
20
  url: new URL(url),
@@ -74,76 +60,17 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
74
60
  location,
75
61
  }, { cause: String(error) });
76
62
  }
77
- // MARK: Sibling assets
78
- // NOTE: If the user doesn't use `pageAssetCustomLocation`,
79
- // we put this as a fallback.
80
- baseDocumentRendered = baseDocumentRendered
81
- .replace('</head>', `\n${PAGE_ASSETS_MARKER}</head>`)
82
- .replace(PAGE_ASSETS_MARKER, routeInfos.foundRoute.pageAssets.length > 0
83
- ? html `<!-- PAGE ASSETS -->` +
84
- `${routeInfos.foundRoute.pageAssets
85
- .map((path) => {
86
- //
87
- if (/\.(js|ts|jsx|tsx)$/.test(path)) {
88
- // prettier-ignore
89
- return html ` <script type="module" src="/${path}"></script>`;
90
- }
91
- if (/\.(css|scss|sass|less|styl|stylus)$/.test(path)) {
92
- // prettier-ignore
93
- return html ` <link rel="stylesheet" href="/${path}" />`;
94
- }
95
- // NOTE: Never called (filtered upstream in `collectRoutes`)
96
- return null;
97
- })
98
- .join('\n')}` +
99
- `<!-- /PAGE ASSETS -->\n `
100
- : '');
101
- // MARK: Add doctype if missing.
102
- if (baseDocumentRendered
103
- .trimStart()
104
- .toLocaleLowerCase()
105
- .startsWith('<!doctype') === false)
106
- baseDocumentRendered = `<!doctype html>\n${baseDocumentRendered}`;
107
- // MARK: Dev. overlay.
108
- // TODO: Need more testing and refinement (refreshes kills its usefulness).
109
- const overlay = () => html `
110
- <script type="module">
111
- if (import.meta.hot) {
112
- import.meta.hot.on('gracile:ssr-error', (error) => {
113
- console.error(error.message);
114
- });
115
- import.meta.hot.on('error', (payload) => {
116
- console.error(payload.err.message);
117
- });
118
- }
119
- </script>
120
- `;
63
+ // MARK: Post-process document HTML (pure transforms)
64
+ baseDocumentRendered = injectSiblingAssets(baseDocumentRendered, routeInfos.foundRoute.pageAssets);
65
+ baseDocumentRendered = ensureDoctype(baseDocumentRendered);
121
66
  if (mode === 'dev')
122
- baseDocumentRendered = baseDocumentRendered.replace('<head>', `<head>\n${overlay()}`);
123
- // MARK: Inject assets for server output runtime only.
67
+ baseDocumentRendered = injectDevelopmentOverlay(baseDocumentRendered);
124
68
  const routeAssetsString = routeAssets?.get?.(routeInfos.foundRoute.pattern.pathname);
125
69
  if (routeAssetsString)
126
- baseDocumentRendered = baseDocumentRendered
127
- .replaceAll(REGEX_TAG_SCRIPT, (s) => {
128
- if (s.includes(`type="module"`))
129
- return '';
130
- return s;
131
- })
132
- .replaceAll(REGEX_TAG_LINK, (s) => {
133
- if (s.includes(`rel="stylesheet"`))
134
- return '';
135
- return s;
136
- })
137
- .replace('</head>', `${routeAssetsString}\n</head>`);
138
- // MARK: Base document
70
+ baseDocumentRendered = injectServerAssets(baseDocumentRendered, routeAssetsString);
71
+ // MARK: Base document (Vite HTML transform in dev)
139
72
  const baseDocumentHtml = vite && mode === 'dev'
140
- ? await vite.transformIndexHtml(
141
- // HACK: Sometimes, we need to invalidate for server asset url
142
- // imports to work. So we keep this hack around just in case.
143
- // Maybe it's linked to the way hashed assets are invalidating
144
- // the html proxy module…
145
- // `${routeInfos.pathname}?r=${Math.random()}`,
146
- routeInfos.pathname, baseDocumentRendered)
73
+ ? await vite.transformIndexHtml(routeInfos.pathname, baseDocumentRendered)
147
74
  : baseDocumentRendered;
148
75
  if (docOnly)
149
76
  return { document: baseDocumentHtml, output: null };
@@ -158,13 +85,6 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
158
85
  (serverMode &&
159
86
  (mode !== 'build' || routeInfos.routeModule.prerender === true)))) {
160
87
  const routeOutput = await Promise.resolve(routeInfos.routeModule.template(context));
161
- // NOTE: Explicitely unset template (maybe a bad idea as a feature. We'll see)
162
- // if (routeOutput === null || routeOutput === undefined) {
163
- // const output = Readable.from(
164
- // concatStreams(baseDocRenderStreamPre, baseDocRenderStreamPost),
165
- // );
166
- // return { output, document: null };
167
- // }
168
88
  if (assert.isLitTemplate(routeOutput) === false)
169
89
  throw new Error(`Wrong template result for page template ${routeInfos.foundRoute.filePath}.`);
170
90
  const renderStream = Readable.from(renderLitSsr(routeOutput, mergedRenderInfo));
@@ -1,4 +1,8 @@
1
1
  import type * as R from './route.js';
2
+ /** @internal Exported for unit testing. */
3
+ export declare function extractRoutePatterns(routeFilePath: string): Pick<R.Route, 'pattern' | 'hasParams'> & {
4
+ patternString: string;
5
+ };
2
6
  export declare const WATCHED_FILES_REGEX: RegExp;
3
7
  export declare function collectRoutes(routes: R.RoutesManifest, root: string, excludePatterns?: string[]): Promise<void>;
4
8
  //# sourceMappingURL=collect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/routes/collect.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,KAAK,CAAC,MAAM,YAAY,CAAC;AA0DrC,eAAO,MAAM,mBAAmB,QAC4C,CAAC;AAE7E,wBAAsB,aAAa,CAClC,MAAM,EAAE,CAAC,CAAC,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,eAAe,GAAE,MAAM,EAAO,GAC5B,OAAO,CAAC,IAAI,CAAC,CAiGf"}
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/routes/collect.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,KAAK,CAAC,MAAM,YAAY,CAAC;AAIrC,2CAA2C;AAC3C,wBAAgB,oBAAoB,CACnC,aAAa,EAAE,MAAM,GACnB,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,GAAG,WAAW,CAAC,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,CAkDpE;AAED,eAAO,MAAM,mBAAmB,QAC4C,CAAC;AAE7E,wBAAsB,aAAa,CAClC,MAAM,EAAE,CAAC,CAAC,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,eAAe,GAAE,MAAM,EAAO,GAC5B,OAAO,CAAC,IAAI,CAAC,CAiGf"}
@@ -12,7 +12,8 @@ import { emptyRoutes } from '../logging/messages.js';
12
12
  import { prepareSortableRoutes, routeComparator } from './comparator.js';
13
13
  import { REGEXES } from './load-module.js';
14
14
  const logger = getLogger();
15
- function extractRoutePatterns(routeFilePath) {
15
+ /** @internal Exported for unit testing. */
16
+ export function extractRoutePatterns(routeFilePath) {
16
17
  const routePathname = routeFilePath.replace(/\.(js|ts|jsx|tsx|html)$/, '');
17
18
  let pathParts = routePathname.split(paths.isWindows() ? paths.WINDOWS_PATH_SEPARATOR : '/');
18
19
  const last = pathParts.at(-1);
@@ -1,6 +1,32 @@
1
1
  import type { ViteDevServer } from 'vite';
2
2
  import type * as R from './route.js';
3
3
  type Parameters_ = Record<string, string | undefined>;
4
+ type MatchedRoute = {
5
+ match: URLPatternResult | undefined;
6
+ foundRoute: R.Route;
7
+ params: Parameters_;
8
+ pathname: string;
9
+ };
10
+ /** @internal Exported for unit testing. */
11
+ export declare function matchRouteFromUrl(url: string, routes: R.RoutesManifest): MatchedRoute | null;
12
+ type ExtractedStaticPaths = {
13
+ staticPaths: R.StaticPathOptionsGeneric[];
14
+ props: unknown;
15
+ } | null;
16
+ /**
17
+ * @param options
18
+ * @param options.routeModule
19
+ * @param options.foundRoute
20
+ * @param options.params
21
+ * @param options.pathname
22
+ */
23
+ /** @internal Exported for unit testing. */
24
+ export declare function extractStaticPaths(options: {
25
+ routeModule: R.RouteModule;
26
+ foundRoute: R.Route;
27
+ params: Parameters_;
28
+ pathname: string;
29
+ }): Promise<ExtractedStaticPaths>;
4
30
  export type RouteInfos = {
5
31
  params: Parameters_;
6
32
  props: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/routes/match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,KAAK,CAAC,MAAM,YAAY,CAAC;AAErC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAoFtD,MAAM,MAAM,UAAU,GAAG;IACxB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACrC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAsB,QAAQ,CAAC,OAAO,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC,cAAc,CAAC;IACzB,YAAY,CAAC,EAAE,CAAC,CAAC,aAAa,GAAG,SAAS,CAAC;CAC3C,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA6B7B"}
1
+ {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/routes/match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,KAAK,CAAC,MAAM,YAAY,CAAC;AAErC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEtD,KAAK,YAAY,GAAG;IACnB,KAAK,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACpC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,2CAA2C;AAC3C,wBAAgB,iBAAiB,CAChC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,CAAC,CAAC,cAAc,GACtB,YAAY,GAAG,IAAI,CAuBrB;AAED,KAAK,oBAAoB,GAAG;IAC3B,WAAW,EAAE,CAAC,CAAC,wBAAwB,EAAE,CAAC;IAC1C,KAAK,EAAE,OAAO,CAAC;CACf,GAAG,IAAI,CAAC;AACT;;;;;;GAMG;AACH,2CAA2C;AAC3C,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IACjD,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC;IAC3B,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA6BhC;AAED,MAAM,MAAM,UAAU,GAAG;IACxB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACrC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAsB,QAAQ,CAAC,OAAO,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC,cAAc,CAAC;IACzB,YAAY,CAAC,EAAE,CAAC,CAAC,aAAa,GAAG,SAAS,CAAC;CAC3C,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA6B7B"}
@@ -1,5 +1,6 @@
1
1
  import { loadForeignRouteObject } from './load-module.js';
2
- function matchRouteFromUrl(url, routes) {
2
+ /** @internal Exported for unit testing. */
3
+ export function matchRouteFromUrl(url, routes) {
3
4
  let match;
4
5
  let foundRoute;
5
6
  const pathname = new URL(url).pathname;
@@ -24,7 +25,8 @@ function matchRouteFromUrl(url, routes) {
24
25
  * @param options.params
25
26
  * @param options.pathname
26
27
  */
27
- async function extractStaticPaths(options) {
28
+ /** @internal Exported for unit testing. */
29
+ export async function extractStaticPaths(options) {
28
30
  if (!options.foundRoute.hasParams)
29
31
  return null;
30
32
  if (!options.routeModule.staticPaths)
@@ -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,kBAoHA;AAyBD,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,wBAAwB,EAC7B,UAAU,GACV,MAAM,uBAAuB,CAAC"}