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

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 (42) hide show
  1. package/dist/dev/development.d.ts.map +1 -1
  2. package/dist/dev/development.js +1 -1
  3. package/dist/plugin.d.ts.map +1 -1
  4. package/dist/plugin.js +37 -262
  5. package/dist/render/route-template-pipeline.d.ts +64 -0
  6. package/dist/render/route-template-pipeline.d.ts.map +1 -0
  7. package/dist/render/route-template-pipeline.js +144 -0
  8. package/dist/render/route-template.d.ts +1 -2
  9. package/dist/render/route-template.d.ts.map +1 -1
  10. package/dist/render/route-template.js +17 -93
  11. package/dist/routes/collect.d.ts +5 -1
  12. package/dist/routes/collect.d.ts.map +1 -1
  13. package/dist/routes/collect.js +8 -6
  14. package/dist/routes/match.d.ts +31 -1
  15. package/dist/routes/match.d.ts.map +1 -1
  16. package/dist/routes/match.js +22 -4
  17. package/dist/routes/render.d.ts.map +1 -1
  18. package/dist/routes/render.js +9 -2
  19. package/dist/server/request-pipeline.d.ts +109 -0
  20. package/dist/server/request-pipeline.d.ts.map +1 -0
  21. package/dist/server/request-pipeline.js +198 -0
  22. package/dist/server/request.d.ts +3 -16
  23. package/dist/server/request.d.ts.map +1 -1
  24. package/dist/server/request.js +74 -171
  25. package/dist/test/init.d.ts +2 -0
  26. package/dist/test/init.d.ts.map +1 -0
  27. package/dist/test/init.js +7 -0
  28. package/dist/user-config.d.ts +13 -0
  29. package/dist/user-config.d.ts.map +1 -1
  30. package/dist/vite/plugin-client-build.d.ts +16 -0
  31. package/dist/vite/plugin-client-build.d.ts.map +1 -0
  32. package/dist/vite/plugin-client-build.js +49 -0
  33. package/dist/vite/plugin-serve.d.ts +18 -0
  34. package/dist/vite/plugin-serve.d.ts.map +1 -0
  35. package/dist/vite/plugin-serve.js +62 -0
  36. package/dist/vite/plugin-server-build.d.ts +33 -0
  37. package/dist/vite/plugin-server-build.d.ts.map +1 -0
  38. package/dist/vite/plugin-server-build.js +157 -0
  39. package/dist/vite/plugin-shared-state.d.ts +31 -0
  40. package/dist/vite/plugin-shared-state.d.ts.map +1 -0
  41. package/dist/vite/plugin-shared-state.js +22 -0
  42. package/package.json +4 -4
@@ -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),
@@ -46,6 +32,9 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
46
32
  });
47
33
  const fragmentRender = renderLitSsr(fragmentOutput, mergedRenderInfo);
48
34
  const output = Readable.from(fragmentRender);
35
+ // TODO: Disabled for now. Causes issue in static renders.
36
+ // Needs investigations.
37
+ // const output = new RenderResultReadable(fragmentRender);
49
38
  return { output, document: null };
50
39
  }
51
40
  // MARK: Document
@@ -74,76 +63,17 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
74
63
  location,
75
64
  }, { cause: String(error) });
76
65
  }
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
- `;
66
+ // MARK: Post-process document HTML (pure transforms)
67
+ baseDocumentRendered = injectSiblingAssets(baseDocumentRendered, routeInfos.foundRoute.pageAssets);
68
+ baseDocumentRendered = ensureDoctype(baseDocumentRendered);
121
69
  if (mode === 'dev')
122
- baseDocumentRendered = baseDocumentRendered.replace('<head>', `<head>\n${overlay()}`);
123
- // MARK: Inject assets for server output runtime only.
70
+ baseDocumentRendered = injectDevelopmentOverlay(baseDocumentRendered);
124
71
  const routeAssetsString = routeAssets?.get?.(routeInfos.foundRoute.pattern.pathname);
125
72
  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
73
+ baseDocumentRendered = injectServerAssets(baseDocumentRendered, routeAssetsString);
74
+ // MARK: Base document (Vite HTML transform in dev)
139
75
  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)
76
+ ? await vite.transformIndexHtml(routeInfos.pathname, baseDocumentRendered)
147
77
  : baseDocumentRendered;
148
78
  if (docOnly)
149
79
  return { document: baseDocumentHtml, output: null };
@@ -158,16 +88,10 @@ export async function renderRouteTemplate({ url, vite, mode, routeInfos, routeAs
158
88
  (serverMode &&
159
89
  (mode !== 'build' || routeInfos.routeModule.prerender === true)))) {
160
90
  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
91
  if (assert.isLitTemplate(routeOutput) === false)
169
92
  throw new Error(`Wrong template result for page template ${routeInfos.foundRoute.filePath}.`);
170
- const renderStream = Readable.from(renderLitSsr(routeOutput, mergedRenderInfo));
93
+ const renderStream =
94
+ /* TODO: Use `new RenderResultReadable` */ Readable.from(renderLitSsr(routeOutput, mergedRenderInfo));
171
95
  const output = Readable.from(concatStreams(baseDocumentRenderStreamPre, renderStream, baseDocumentRenderStreamPost));
172
96
  return { output, document: baseDocumentHtml };
173
97
  }
@@ -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;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,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"}
@@ -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
- // NOTE: The polyfill type lacks `hasRegExpGroups` from the global URLPattern.
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
- function extractRoutePatterns(routeFilePath) {
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 trailingSlash = pathRelativeNormalized.length > 0 ? '/' : '';
45
- const normalizedUrlPattern = `/${pathRelativeNormalized.join('/')}${trailingSlash}`;
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,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
- }): Promise<RouteInfos | null>;
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;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,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"}
@@ -1,8 +1,23 @@
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, trailingSlash = 'ignore') {
3
4
  let match;
4
5
  let foundRoute;
5
- const pathname = new URL(url).pathname;
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
- async function extractStaticPaths(options) {
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;AAcD,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;;;GAmKA"}
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;AAwBD,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"}
@@ -8,11 +8,18 @@ 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
+ // NOTE: Since using Lit's `RenderResultReadable` instead of pure
12
+ // Node Readable, the chunk can be a string or a Buffer.
13
+ // If it's a string, convert it to Buffer first.
11
14
  if (typeof chunk === 'string') {
12
15
  chunks.push(Buffer.from(chunk));
13
16
  }
14
17
  else
15
- throw new TypeError('Wrong buffer');
18
+ throw new TypeError('Wrong buffer type from stream. Should be a `string` only.');
19
+ // NOTE: Disabled for now. Causes issues with `RenderResultReadable`.
20
+ /* else {
21
+ chunks.push(chunk);
22
+ } */
16
23
  }
17
24
  return Buffer.concat(chunks).toString('utf8');
18
25
  }
@@ -20,7 +27,7 @@ export async function renderRoutes({ routes, vite, serverMode, root = process.cw
20
27
  const logger = getLogger();
21
28
  logger.info(c.green('Rendering routes…'), { timestamp: true });
22
29
  // MARK: Collect
23
- await collectRoutes(routes, root, gracileConfig.routes?.exclude);
30
+ await collectRoutes(routes, root, gracileConfig.routes?.exclude, gracileConfig.trailingSlash);
24
31
  const renderedRoutes = [];
25
32
  // MARK: Iterate modules
26
33
  await Promise.all([...routes].map(async ([patternString, route]) => {
@@ -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"}