@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.
- package/dist/dev/development.d.ts.map +1 -1
- package/dist/dev/development.js +1 -1
- package/dist/dev/ssr-ce-tracker.d.ts +27 -0
- package/dist/dev/ssr-ce-tracker.d.ts.map +1 -0
- package/dist/dev/ssr-ce-tracker.js +113 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +51 -264
- package/dist/render/route-template-pipeline.d.ts +64 -0
- package/dist/render/route-template-pipeline.d.ts.map +1 -0
- package/dist/render/route-template-pipeline.js +144 -0
- package/dist/render/route-template.d.ts +1 -2
- package/dist/render/route-template.d.ts.map +1 -1
- package/dist/render/route-template.js +37 -92
- package/dist/routes/collect.d.ts +5 -1
- package/dist/routes/collect.d.ts.map +1 -1
- package/dist/routes/collect.js +8 -6
- package/dist/routes/load-module.d.ts.map +1 -1
- package/dist/routes/load-module.js +5 -2
- package/dist/routes/match.d.ts +31 -1
- package/dist/routes/match.d.ts.map +1 -1
- package/dist/routes/match.js +22 -4
- package/dist/routes/render.d.ts.map +1 -1
- package/dist/routes/render.js +11 -3
- package/dist/server/request-pipeline.d.ts +109 -0
- package/dist/server/request-pipeline.d.ts.map +1 -0
- package/dist/server/request-pipeline.js +198 -0
- package/dist/server/request.d.ts +3 -16
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +74 -171
- package/dist/test/init.d.ts +2 -0
- package/dist/test/init.d.ts.map +1 -0
- package/dist/test/init.js +7 -0
- package/dist/user-config.d.ts +13 -0
- package/dist/user-config.d.ts.map +1 -1
- package/dist/vite/build-routes.d.ts +1 -2
- package/dist/vite/build-routes.d.ts.map +1 -1
- package/dist/vite/build-routes.js +0 -98
- package/dist/vite/plugin-build-environment.d.ts +21 -0
- package/dist/vite/plugin-build-environment.d.ts.map +1 -0
- package/dist/vite/plugin-build-environment.js +83 -0
- package/dist/vite/plugin-ce-tracker.d.ts +19 -0
- package/dist/vite/plugin-ce-tracker.d.ts.map +1 -0
- package/dist/vite/plugin-ce-tracker.js +87 -0
- package/dist/vite/plugin-client-build.d.ts +20 -0
- package/dist/vite/plugin-client-build.d.ts.map +1 -0
- package/dist/vite/plugin-client-build.js +55 -0
- package/dist/vite/plugin-html-routes-build.d.ts +22 -0
- package/dist/vite/plugin-html-routes-build.d.ts.map +1 -0
- package/dist/vite/plugin-html-routes-build.js +140 -0
- package/dist/vite/plugin-serve.d.ts +18 -0
- package/dist/vite/plugin-serve.d.ts.map +1 -0
- package/dist/vite/plugin-serve.js +61 -0
- package/dist/vite/plugin-server-build.d.ts +43 -0
- package/dist/vite/plugin-server-build.d.ts.map +1 -0
- package/dist/vite/plugin-server-build.js +108 -0
- package/dist/vite/plugin-shared-state.d.ts +33 -0
- package/dist/vite/plugin-shared-state.d.ts.map +1 -0
- package/dist/vite/plugin-shared-state.js +23 -0
- package/dist/vite/virtual-routes.d.ts +8 -5
- package/dist/vite/virtual-routes.d.ts.map +1 -1
- package/dist/vite/virtual-routes.js +32 -31
- package/package.json +5 -5
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, testable pipeline steps extracted from the route template renderer.
|
|
3
|
+
*
|
|
4
|
+
* Each function here is a focused stage of the document post-processing
|
|
5
|
+
* lifecycle. They are composed by `renderRouteTemplate` in
|
|
6
|
+
* `./route-template.ts`.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
import { Readable } from 'node:stream';
|
|
11
|
+
import { html } from '@gracile/internal-utils/dummy-literals';
|
|
12
|
+
import { LitElementRenderer } from '@lit-labs/ssr/lib/lit-element-renderer.js';
|
|
13
|
+
import { PAGE_ASSETS_MARKER } from './markers.js';
|
|
14
|
+
// ── Regexes (re-exported for testability) ────────────────────────────
|
|
15
|
+
export const REGEX_TAG_SCRIPT = /\s?<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script\s*>\s?/gi;
|
|
16
|
+
export const REGEX_TAG_LINK = /\s?<link\b[^>]*?>\s?/gi;
|
|
17
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Concatenate multiple `Readable` streams into a single async iterable.
|
|
20
|
+
*/
|
|
21
|
+
export async function* concatStreams(...readables) {
|
|
22
|
+
for (const readable of readables) {
|
|
23
|
+
for await (const chunk of readable) {
|
|
24
|
+
yield chunk;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// ── 1. Merge render info ─────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Merge user-provided `RenderInfo` with the default `LitElementRenderer`.
|
|
31
|
+
* Always appends `LitElementRenderer` to whatever the user supplied.
|
|
32
|
+
*/
|
|
33
|
+
export function mergeRenderInfo(renderInfo) {
|
|
34
|
+
return {
|
|
35
|
+
...renderInfo,
|
|
36
|
+
elementRenderers: [
|
|
37
|
+
...(renderInfo?.elementRenderers || []),
|
|
38
|
+
LitElementRenderer,
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// ── 2. Inject sibling page assets ───────────────────────────────────
|
|
43
|
+
/** Regex matching JS/TS asset extensions. */
|
|
44
|
+
const REGEX_SCRIPT_EXT = /\.(js|ts|jsx|tsx)$/;
|
|
45
|
+
/** Regex matching CSS-like asset extensions. */
|
|
46
|
+
const REGEX_STYLE_EXT = /\.(css|scss|sass|less|styl|stylus)$/;
|
|
47
|
+
/**
|
|
48
|
+
* Inject sibling page assets into the rendered document HTML.
|
|
49
|
+
*
|
|
50
|
+
* Inserts the `PAGE_ASSETS_MARKER` before `</head>`, then replaces it
|
|
51
|
+
* with concrete `<script>` / `<link>` tags for each asset path.
|
|
52
|
+
*
|
|
53
|
+
* @param documentHtml The rendered document string.
|
|
54
|
+
* @param pageAssets Array of asset file paths (e.g. `['src/pages/about.css']`).
|
|
55
|
+
* @returns The document with asset tags injected (or unchanged if no assets).
|
|
56
|
+
*/
|
|
57
|
+
export function injectSiblingAssets(documentHtml, pageAssets) {
|
|
58
|
+
return documentHtml
|
|
59
|
+
.replace('</head>', `\n${PAGE_ASSETS_MARKER}</head>`)
|
|
60
|
+
.replace(PAGE_ASSETS_MARKER, pageAssets.length > 0
|
|
61
|
+
? html `<!-- PAGE ASSETS -->` +
|
|
62
|
+
`${pageAssets
|
|
63
|
+
.map((path) => {
|
|
64
|
+
if (REGEX_SCRIPT_EXT.test(path)) {
|
|
65
|
+
// prettier-ignore
|
|
66
|
+
return html ` <script type="module" src="/${path}"></script>`;
|
|
67
|
+
}
|
|
68
|
+
if (REGEX_STYLE_EXT.test(path)) {
|
|
69
|
+
// prettier-ignore
|
|
70
|
+
return html ` <link rel="stylesheet" href="/${path}" />`;
|
|
71
|
+
}
|
|
72
|
+
// NOTE: Never called (filtered upstream in `collectRoutes`)
|
|
73
|
+
return null;
|
|
74
|
+
})
|
|
75
|
+
.join('\n')}` +
|
|
76
|
+
`<!-- /PAGE ASSETS -->\n `
|
|
77
|
+
: '');
|
|
78
|
+
}
|
|
79
|
+
// ── 3. Ensure doctype ────────────────────────────────────────────────
|
|
80
|
+
/**
|
|
81
|
+
* Prepend `<!doctype html>` if the document doesn't already start with one.
|
|
82
|
+
*
|
|
83
|
+
* @param documentHtml The rendered document string.
|
|
84
|
+
* @returns The document guaranteed to start with a doctype declaration.
|
|
85
|
+
*/
|
|
86
|
+
export function ensureDoctype(documentHtml) {
|
|
87
|
+
if (documentHtml.trimStart().toLocaleLowerCase().startsWith('<!doctype') ===
|
|
88
|
+
false)
|
|
89
|
+
return `<!doctype html>\n${documentHtml}`;
|
|
90
|
+
return documentHtml;
|
|
91
|
+
}
|
|
92
|
+
// ── 4. Inject dev overlay ────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* The HMR error overlay script injected in dev mode.
|
|
95
|
+
* Returns the raw HTML string for the `<script>` tag.
|
|
96
|
+
*/
|
|
97
|
+
export function developmentOverlaySnippet() {
|
|
98
|
+
return html `
|
|
99
|
+
<script type="module">
|
|
100
|
+
if (import.meta.hot) {
|
|
101
|
+
import.meta.hot.on('gracile:ssr-error', (error) => {
|
|
102
|
+
console.error(error.message);
|
|
103
|
+
});
|
|
104
|
+
import.meta.hot.on('error', (payload) => {
|
|
105
|
+
console.error(payload.err.message);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Inject the dev overlay script right after `<head>`.
|
|
113
|
+
* Only applied when `mode === 'dev'`.
|
|
114
|
+
*
|
|
115
|
+
* @param documentHtml The rendered document string.
|
|
116
|
+
* @returns The document with the overlay injected.
|
|
117
|
+
*/
|
|
118
|
+
export function injectDevelopmentOverlay(documentHtml) {
|
|
119
|
+
return documentHtml.replace('<head>', `<head>\n${developmentOverlaySnippet()}`);
|
|
120
|
+
}
|
|
121
|
+
// ── 5. Inject server runtime assets ─────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* For server-output builds: strip dev-time `<script type="module">` and
|
|
124
|
+
* `<link rel="stylesheet">` tags, then inject the production asset string
|
|
125
|
+
* before `</head>`.
|
|
126
|
+
*
|
|
127
|
+
* @param documentHtml The rendered document string.
|
|
128
|
+
* @param routeAssetsHtml The production asset tags to inject.
|
|
129
|
+
* @returns The document with dev assets stripped and production assets injected.
|
|
130
|
+
*/
|
|
131
|
+
export function injectServerAssets(documentHtml, routeAssetsHtml) {
|
|
132
|
+
return documentHtml
|
|
133
|
+
.replaceAll(REGEX_TAG_SCRIPT, (s) => {
|
|
134
|
+
if (s.includes(`type="module"`))
|
|
135
|
+
return '';
|
|
136
|
+
return s;
|
|
137
|
+
})
|
|
138
|
+
.replaceAll(REGEX_TAG_LINK, (s) => {
|
|
139
|
+
if (s.includes(`rel="stylesheet"`))
|
|
140
|
+
return '';
|
|
141
|
+
return s;
|
|
142
|
+
})
|
|
143
|
+
.replace('</head>', `${routeAssetsHtml}\n</head>`);
|
|
144
|
+
}
|
|
@@ -3,8 +3,7 @@ import { type RenderInfo } from '@lit-labs/ssr';
|
|
|
3
3
|
import type { ViteDevServer } from 'vite';
|
|
4
4
|
import type { RouteInfos } from '../routes/match.js';
|
|
5
5
|
import type * as R from '../routes/route.js';
|
|
6
|
-
export
|
|
7
|
-
export declare const REGEX_TAG_LINK: RegExp;
|
|
6
|
+
export { REGEX_TAG_SCRIPT, REGEX_TAG_LINK } from './route-template-pipeline.js';
|
|
8
7
|
export declare function renderRouteTemplate({ url, vite, mode, routeInfos, routeAssets, serverMode, docOnly, renderInfo, }: {
|
|
9
8
|
url: string;
|
|
10
9
|
vite?: ViteDevServer | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-template.d.ts","sourceRoot":"","sources":["../../src/render/route-template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"route-template.d.ts","sourceRoot":"","sources":["../../src/render/route-template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvC,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAOxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAO1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,KAAK,CAAC,MAAM,oBAAoB,CAAC;AAa7C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AA0BhF,wBAAsB,mBAAmB,CAAC,EACzC,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,UAAU,EACV,WAAW,EACX,UAAU,EACV,OAAO,EACP,UAAU,GACV,EAAE;IACF,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,CAAC,YAAY,GAAG,SAAS,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;CAC7C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,MAAM,CAAA;CAAE,CAAC,CAwJhE"}
|
|
@@ -1,20 +1,37 @@
|
|
|
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
|
-
|
|
5
|
+
// Lit's purpose-built Readable subclass for RenderResult. Handles nested
|
|
6
|
+
// iterables, thunks, and Promises from sub-templates with proper back-pressure,
|
|
7
|
+
// whereas generic `Readable.from()` only sees the top-level iterable.
|
|
8
|
+
// Inherits from the same Node `Readable` we already depend on.
|
|
9
|
+
import { RenderResultReadable } from '@lit-labs/ssr/lib/render-result-readable.js';
|
|
7
10
|
import { GracileError, GracileErrorData, TemplateError, } from '../errors/errors.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
import { SSR_OUTLET_MARKER } from './markers.js';
|
|
12
|
+
import { concatStreams, mergeRenderInfo, injectSiblingAssets, ensureDoctype, injectDevelopmentOverlay, injectServerAssets, } from './route-template-pipeline.js';
|
|
13
|
+
// Re-export for consumers that import these regexes from this module.
|
|
14
|
+
export { REGEX_TAG_SCRIPT, REGEX_TAG_LINK } from './route-template-pipeline.js';
|
|
15
|
+
/**
|
|
16
|
+
* Lit SSR's `RenderResultReadable._read()` is `async`, but Node.js
|
|
17
|
+
* calls `_read()` without awaiting its return value. If a thunk inside
|
|
18
|
+
* the render result throws (e.g. template expressions in invalid
|
|
19
|
+
* locations), the rejected Promise escapes and becomes an
|
|
20
|
+
* `unhandledRejection`.
|
|
21
|
+
*
|
|
22
|
+
* This subclass overrides `_read()` to catch that rejection and convert
|
|
23
|
+
* it into a stream `'error'` event via `this.destroy(err)`, which is
|
|
24
|
+
* the standard Node Readable contract.
|
|
25
|
+
*/
|
|
26
|
+
class SafeRenderResultReadable extends RenderResultReadable {
|
|
27
|
+
_read(size) {
|
|
28
|
+
const maybePromise = super._read(size);
|
|
29
|
+
void maybePromise?.catch?.((error) => {
|
|
30
|
+
this.destroy(error);
|
|
31
|
+
});
|
|
32
|
+
return maybePromise;
|
|
14
33
|
}
|
|
15
34
|
}
|
|
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;
|
|
18
35
|
export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAssets, serverMode, docOnly, renderInfo, }) {
|
|
19
36
|
const location = {
|
|
20
37
|
file: routeInfos.foundRoute.filePath,
|
|
@@ -22,13 +39,7 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
|
|
|
22
39
|
if (!routeInfos.routeModule.document && !routeInfos.routeModule.template)
|
|
23
40
|
return { output: null, document: null };
|
|
24
41
|
// MARK: Merged render info
|
|
25
|
-
const mergedRenderInfo =
|
|
26
|
-
...renderInfo,
|
|
27
|
-
elementRenderers: [
|
|
28
|
-
...(renderInfo?.elementRenderers || []),
|
|
29
|
-
LitElementRenderer,
|
|
30
|
-
],
|
|
31
|
-
};
|
|
42
|
+
const mergedRenderInfo = mergeRenderInfo(renderInfo);
|
|
32
43
|
// MARK: Context
|
|
33
44
|
const context = {
|
|
34
45
|
url: new URL(url),
|
|
@@ -45,7 +56,7 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
|
|
|
45
56
|
// location,
|
|
46
57
|
});
|
|
47
58
|
const fragmentRender = renderLitSsr(fragmentOutput, mergedRenderInfo);
|
|
48
|
-
const output =
|
|
59
|
+
const output = new SafeRenderResultReadable(fragmentRender);
|
|
49
60
|
return { output, document: null };
|
|
50
61
|
}
|
|
51
62
|
// MARK: Document
|
|
@@ -74,76 +85,17 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
|
|
|
74
85
|
location,
|
|
75
86
|
}, { cause: String(error) });
|
|
76
87
|
}
|
|
77
|
-
// MARK:
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
`;
|
|
88
|
+
// MARK: Post-process document HTML (pure transforms)
|
|
89
|
+
baseDocumentRendered = injectSiblingAssets(baseDocumentRendered, routeInfos.foundRoute.pageAssets);
|
|
90
|
+
baseDocumentRendered = ensureDoctype(baseDocumentRendered);
|
|
121
91
|
if (mode === 'dev')
|
|
122
|
-
baseDocumentRendered = baseDocumentRendered
|
|
123
|
-
// MARK: Inject assets for server output runtime only.
|
|
92
|
+
baseDocumentRendered = injectDevelopmentOverlay(baseDocumentRendered);
|
|
124
93
|
const routeAssetsString = routeAssets?.get?.(routeInfos.foundRoute.pattern.pathname);
|
|
125
94
|
if (routeAssetsString)
|
|
126
|
-
baseDocumentRendered = baseDocumentRendered
|
|
127
|
-
|
|
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
|
|
95
|
+
baseDocumentRendered = injectServerAssets(baseDocumentRendered, routeAssetsString);
|
|
96
|
+
// MARK: Base document (Vite HTML transform in dev)
|
|
139
97
|
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)
|
|
98
|
+
? await vite.transformIndexHtml(routeInfos.pathname, baseDocumentRendered)
|
|
147
99
|
: baseDocumentRendered;
|
|
148
100
|
if (docOnly)
|
|
149
101
|
return { document: baseDocumentHtml, output: null };
|
|
@@ -158,16 +110,9 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
|
|
|
158
110
|
(serverMode &&
|
|
159
111
|
(mode !== 'build' || routeInfos.routeModule.prerender === true)))) {
|
|
160
112
|
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
113
|
if (assert.isLitTemplate(routeOutput) === false)
|
|
169
114
|
throw new Error(`Wrong template result for page template ${routeInfos.foundRoute.filePath}.`);
|
|
170
|
-
const renderStream =
|
|
115
|
+
const renderStream = new SafeRenderResultReadable(renderLitSsr(routeOutput, mergedRenderInfo));
|
|
171
116
|
const output = Readable.from(concatStreams(baseDocumentRenderStreamPre, renderStream, baseDocumentRenderStreamPost));
|
|
172
117
|
return { output, document: baseDocumentHtml };
|
|
173
118
|
}
|
package/dist/routes/collect.d.ts
CHANGED
|
@@ -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, trailingSlash?: 'always' | 'never' | 'ignore'): Pick<R.Route, 'pattern' | 'hasParams'> & {
|
|
4
|
+
patternString: string;
|
|
5
|
+
};
|
|
2
6
|
export declare const WATCHED_FILES_REGEX: RegExp;
|
|
3
|
-
export declare function collectRoutes(routes: R.RoutesManifest, root: string, excludePatterns?: string[]): Promise<void>;
|
|
7
|
+
export declare function collectRoutes(routes: R.RoutesManifest, root: string, excludePatterns?: string[], trailingSlash?: 'always' | 'never' | 'ignore'): 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;
|
|
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,EACrB,aAAa,GAAE,QAAQ,GAAG,OAAO,GAAG,QAAmB,GACrD,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,GAAG,WAAW,CAAC,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,CAmDpE;AAED,eAAO,MAAM,mBAAmB,QAC4C,CAAC;AAE7E,wBAAsB,aAAa,CAClC,MAAM,EAAE,CAAC,CAAC,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,eAAe,GAAE,MAAM,EAAO,EAC9B,aAAa,GAAE,QAAQ,GAAG,OAAO,GAAG,QAAmB,GACrD,OAAO,CAAC,IAAI,CAAC,CAiGf"}
|
package/dist/routes/collect.js
CHANGED
|
@@ -5,14 +5,15 @@ import { fdir as Fdir } from 'fdir';
|
|
|
5
5
|
import c from 'picocolors';
|
|
6
6
|
// eslint-disable-next-line import-x/order
|
|
7
7
|
import { URLPattern as URLPatternPolyfill } from 'urlpattern-polyfill/urlpattern';
|
|
8
|
-
//
|
|
8
|
+
// HACK: The polyfill type lacks `hasRegExpGroups` from the global URLPattern.
|
|
9
9
|
const URLPattern = URLPatternPolyfill;
|
|
10
10
|
import { createFilter } from 'vite';
|
|
11
11
|
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
|
-
|
|
15
|
+
/** @internal Exported for unit testing. */
|
|
16
|
+
export function extractRoutePatterns(routeFilePath, trailingSlash = 'ignore') {
|
|
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);
|
|
@@ -41,8 +42,9 @@ function extractRoutePatterns(routeFilePath) {
|
|
|
41
42
|
}
|
|
42
43
|
return entry;
|
|
43
44
|
});
|
|
44
|
-
const
|
|
45
|
-
const
|
|
45
|
+
const isRoot = pathRelativeNormalized.length === 0;
|
|
46
|
+
const slash = isRoot || trailingSlash === 'never' ? '' : '/';
|
|
47
|
+
const normalizedUrlPattern = `/${pathRelativeNormalized.join('/')}${slash}`;
|
|
46
48
|
return {
|
|
47
49
|
patternString: normalizedUrlPattern,
|
|
48
50
|
pattern: new URLPattern(normalizedUrlPattern, 'http://gracile/'),
|
|
@@ -50,7 +52,7 @@ function extractRoutePatterns(routeFilePath) {
|
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
54
|
export const WATCHED_FILES_REGEX = /\/src\/routes\/(.*)\.(js|ts|jsx|tsx|html|css|scss|sass|less|styl|stylus)$/;
|
|
53
|
-
export async function collectRoutes(routes, root, excludePatterns = []) {
|
|
55
|
+
export async function collectRoutes(routes, root, excludePatterns = [], trailingSlash = 'ignore') {
|
|
54
56
|
routes.clear();
|
|
55
57
|
const routesFolder = 'src/routes';
|
|
56
58
|
const routesFolderAbsolute = join(root, routesFolder);
|
|
@@ -104,7 +106,7 @@ export async function collectRoutes(routes, root, excludePatterns = []) {
|
|
|
104
106
|
// MARK: Associate
|
|
105
107
|
for (const routePath of serverEntrypointsSorted) {
|
|
106
108
|
const filePath = join(routesFolder, routePath);
|
|
107
|
-
const routeWithPatterns = extractRoutePatterns(routePath);
|
|
109
|
+
const routeWithPatterns = extractRoutePatterns(routePath, trailingSlash);
|
|
108
110
|
routes.set(routeWithPatterns.patternString, {
|
|
109
111
|
filePath,
|
|
110
112
|
pattern: routeWithPatterns.pattern,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-module.d.ts","sourceRoot":"","sources":["../../src/routes/load-module.ts"],"names":[],"mappings":"AAKA,OAAO,
|
|
1
|
+
{"version":3,"file":"load-module.d.ts","sourceRoot":"","sources":["../../src/routes/load-module.ts"],"names":[],"mappings":"AAKA,OAAO,EAA4B,KAAK,aAAa,EAAE,MAAM,MAAM,CAAC;AAIpE,OAAO,KAAK,CAAC,MAAM,YAAY,CAAC;AAGhC,eAAO,MAAM,OAAO;;;;;;CASnB,CAAC;AAQF,wBAAsB,sBAAsB,CAAC,EAC5C,IAAI,EACJ,KAAK,EACL,YAAY,GACZ,EAAE;IACF,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC;IACf,YAAY,CAAC,EAAE,CAAC,CAAC,aAAa,GAAG,SAAS,CAAC;CAC3C,0BAgDA"}
|
|
@@ -2,7 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
import { collectErrorMetadata } from '@gracile-labs/better-errors/dev/utils';
|
|
4
4
|
import { enhanceViteSSRError } from '@gracile-labs/better-errors/dev/vite';
|
|
5
|
-
import {} from 'vite';
|
|
5
|
+
import { isRunnableDevEnvironment } from 'vite';
|
|
6
6
|
import { GracileError, GracileErrorData } from '../errors/errors.js';
|
|
7
7
|
import * as R from './route.js';
|
|
8
8
|
// const ROUTE_SPREAD = /^\.{3}.+$/;
|
|
@@ -24,7 +24,10 @@ export async function loadForeignRouteObject({ vite, route, routeImports, }) {
|
|
|
24
24
|
let unknownRouteModule;
|
|
25
25
|
if (vite) {
|
|
26
26
|
try {
|
|
27
|
-
|
|
27
|
+
const ssrEnvironment = vite.environments.ssr;
|
|
28
|
+
if (!isRunnableDevEnvironment(ssrEnvironment))
|
|
29
|
+
throw new Error('Not in a SSR path');
|
|
30
|
+
unknownRouteModule = await ssrEnvironment.runner.import(route.filePath);
|
|
28
31
|
}
|
|
29
32
|
catch (error) {
|
|
30
33
|
const error_ = error;
|
package/dist/routes/match.d.ts
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
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
|
+
export type TrailingSlashRedirect = {
|
|
11
|
+
redirect: string;
|
|
12
|
+
};
|
|
13
|
+
/** @internal Exported for unit testing. */
|
|
14
|
+
export declare function matchRouteFromUrl(url: string, routes: R.RoutesManifest, trailingSlash?: 'always' | 'never' | 'ignore'): MatchedRoute | TrailingSlashRedirect | null;
|
|
15
|
+
type ExtractedStaticPaths = {
|
|
16
|
+
staticPaths: R.StaticPathOptionsGeneric[];
|
|
17
|
+
props: unknown;
|
|
18
|
+
} | null;
|
|
19
|
+
/**
|
|
20
|
+
* @param options
|
|
21
|
+
* @param options.routeModule
|
|
22
|
+
* @param options.foundRoute
|
|
23
|
+
* @param options.params
|
|
24
|
+
* @param options.pathname
|
|
25
|
+
*/
|
|
26
|
+
/** @internal Exported for unit testing. */
|
|
27
|
+
export declare function extractStaticPaths(options: {
|
|
28
|
+
routeModule: R.RouteModule;
|
|
29
|
+
foundRoute: R.Route;
|
|
30
|
+
params: Parameters_;
|
|
31
|
+
pathname: string;
|
|
32
|
+
}): Promise<ExtractedStaticPaths>;
|
|
4
33
|
export type RouteInfos = {
|
|
5
34
|
params: Parameters_;
|
|
6
35
|
props: unknown;
|
|
@@ -13,6 +42,7 @@ export declare function getRoute(options: {
|
|
|
13
42
|
vite?: ViteDevServer | undefined;
|
|
14
43
|
routes: R.RoutesManifest;
|
|
15
44
|
routeImports?: R.RoutesImports | undefined;
|
|
16
|
-
|
|
45
|
+
trailingSlash?: 'always' | 'never' | 'ignore';
|
|
46
|
+
}): Promise<RouteInfos | TrailingSlashRedirect | null>;
|
|
17
47
|
export {};
|
|
18
48
|
//# sourceMappingURL=match.d.ts.map
|
|
@@ -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;
|
|
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,MAAM,MAAM,qBAAqB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,2CAA2C;AAC3C,wBAAgB,iBAAiB,CAChC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,CAAC,CAAC,cAAc,EACxB,aAAa,GAAE,QAAQ,GAAG,OAAO,GAAG,QAAmB,GACrD,YAAY,GAAG,qBAAqB,GAAG,IAAI,CAyC7C;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;IAC3C,aAAa,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;CAC9C,GAAG,OAAO,CAAC,UAAU,GAAG,qBAAqB,GAAG,IAAI,CAAC,CAkCrD"}
|
package/dist/routes/match.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { loadForeignRouteObject } from './load-module.js';
|
|
2
|
-
|
|
2
|
+
/** @internal Exported for unit testing. */
|
|
3
|
+
export function matchRouteFromUrl(url, routes, trailingSlash = 'ignore') {
|
|
3
4
|
let match;
|
|
4
5
|
let foundRoute;
|
|
5
|
-
const
|
|
6
|
+
const rawPathname = new URL(url).pathname;
|
|
7
|
+
// Handle redirect cases for 'always' and 'never' before matching.
|
|
8
|
+
// Root '/' is exempt — it always keeps its slash.
|
|
9
|
+
if (rawPathname !== '/') {
|
|
10
|
+
if (trailingSlash === 'always' && !rawPathname.endsWith('/'))
|
|
11
|
+
return { redirect: rawPathname + '/' };
|
|
12
|
+
if (trailingSlash === 'never' && rawPathname.endsWith('/'))
|
|
13
|
+
return { redirect: rawPathname.slice(0, -1) };
|
|
14
|
+
}
|
|
15
|
+
// For 'ignore', normalize to trailing-slash so it matches stored patterns.
|
|
16
|
+
const pathname = trailingSlash === 'ignore' &&
|
|
17
|
+
rawPathname !== '/' &&
|
|
18
|
+
!rawPathname.endsWith('/')
|
|
19
|
+
? rawPathname + '/'
|
|
20
|
+
: rawPathname;
|
|
6
21
|
for (const [, route] of routes) {
|
|
7
22
|
if (match)
|
|
8
23
|
break;
|
|
@@ -24,7 +39,8 @@ function matchRouteFromUrl(url, routes) {
|
|
|
24
39
|
* @param options.params
|
|
25
40
|
* @param options.pathname
|
|
26
41
|
*/
|
|
27
|
-
|
|
42
|
+
/** @internal Exported for unit testing. */
|
|
43
|
+
export async function extractStaticPaths(options) {
|
|
28
44
|
if (!options.foundRoute.hasParams)
|
|
29
45
|
return null;
|
|
30
46
|
if (!options.routeModule.staticPaths)
|
|
@@ -47,8 +63,10 @@ async function extractStaticPaths(options) {
|
|
|
47
63
|
return { staticPaths, props: properties };
|
|
48
64
|
}
|
|
49
65
|
export async function getRoute(options) {
|
|
50
|
-
const matchedRoute = matchRouteFromUrl(options.url, options.routes);
|
|
66
|
+
const matchedRoute = matchRouteFromUrl(options.url, options.routes, options.trailingSlash);
|
|
51
67
|
if (!matchedRoute)
|
|
68
|
+
return null;
|
|
69
|
+
if ('redirect' in matchedRoute)
|
|
52
70
|
return matchedRoute;
|
|
53
71
|
const { foundRoute, pathname, params } = matchedRoute;
|
|
54
72
|
const routeModule = await loadForeignRouteObject({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/routes/render.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,uBAAuB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAEpB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,MAAM,EAAE;QACP,KAAK,EAAE,OAAO,CAAC;QACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,CAAC;IAEF,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/routes/render.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,uBAAuB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAEpB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,MAAM,EAAE;QACP,KAAK,EAAE,OAAO,CAAC;QACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,CAAC;IAEF,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9B;AAsBD,wBAAsB,YAAY,CAAC,EAClC,MAAM,EACN,IAAI,EACJ,UAAU,EACV,IAAoB,EACpB,aAAa,GACb,EAAE;IACF,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,aAAa,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,aAAa,CAAC;CAC7B;;;GAwKA"}
|
package/dist/routes/render.js
CHANGED
|
@@ -8,11 +8,19 @@ import { loadForeignRouteObject } from './load-module.js';
|
|
|
8
8
|
async function streamToString(stream) {
|
|
9
9
|
const chunks = [];
|
|
10
10
|
for await (const chunk of stream) {
|
|
11
|
+
// `RenderResultReadable` always pushes strings (it resolves promises,
|
|
12
|
+
// thunks, and nested iterables internally). `Readable.from(string)`
|
|
13
|
+
// used for the document-only path also yields strings.
|
|
14
|
+
// Accept Buffer too for safety (plain `Readable` streams may emit them).
|
|
11
15
|
if (typeof chunk === 'string') {
|
|
12
16
|
chunks.push(Buffer.from(chunk));
|
|
13
17
|
}
|
|
14
|
-
else
|
|
15
|
-
|
|
18
|
+
else if (Buffer.isBuffer(chunk)) {
|
|
19
|
+
chunks.push(chunk);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
throw new TypeError(`Unexpected chunk type in stream: ${typeof chunk}`);
|
|
23
|
+
}
|
|
16
24
|
}
|
|
17
25
|
return Buffer.concat(chunks).toString('utf8');
|
|
18
26
|
}
|
|
@@ -20,7 +28,7 @@ export async function renderRoutes({ routes, vite, serverMode, root = process.cw
|
|
|
20
28
|
const logger = getLogger();
|
|
21
29
|
logger.info(c.green('Rendering routes…'), { timestamp: true });
|
|
22
30
|
// MARK: Collect
|
|
23
|
-
await collectRoutes(routes, root, gracileConfig.routes?.exclude);
|
|
31
|
+
await collectRoutes(routes, root, gracileConfig.routes?.exclude, gracileConfig.trailingSlash);
|
|
24
32
|
const renderedRoutes = [];
|
|
25
33
|
// MARK: Iterate modules
|
|
26
34
|
await Promise.all([...routes].map(async ([patternString, route]) => {
|