@gracile/engine 0.8.2 → 0.9.0-next.10

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 (80) hide show
  1. package/dist/dev/development.d.ts.map +1 -1
  2. package/dist/dev/development.js +1 -1
  3. package/dist/dev/ssr-ce-tracker.d.ts +27 -0
  4. package/dist/dev/ssr-ce-tracker.d.ts.map +1 -0
  5. package/dist/dev/ssr-ce-tracker.js +113 -0
  6. package/dist/errors/create-vite-better-error.js +1 -1
  7. package/dist/plugin.d.ts.map +1 -1
  8. package/dist/plugin.js +56 -256
  9. package/dist/render/light-dom.d.ts +2 -0
  10. package/dist/render/light-dom.d.ts.map +1 -0
  11. package/dist/render/light-dom.js +31 -0
  12. package/dist/render/{utils.d.ts → lit-ssr.d.ts} +1 -1
  13. package/dist/render/lit-ssr.d.ts.map +1 -0
  14. package/dist/render/route-template-pipeline.d.ts +64 -0
  15. package/dist/render/route-template-pipeline.d.ts.map +1 -0
  16. package/dist/render/route-template-pipeline.js +144 -0
  17. package/dist/render/route-template.d.ts +4 -3
  18. package/dist/render/route-template.d.ts.map +1 -1
  19. package/dist/render/route-template.js +41 -87
  20. package/dist/routes/collect.d.ts +5 -1
  21. package/dist/routes/collect.d.ts.map +1 -1
  22. package/dist/routes/collect.js +11 -6
  23. package/dist/routes/load-module.d.ts.map +1 -1
  24. package/dist/routes/load-module.js +5 -2
  25. package/dist/routes/match.d.ts +31 -1
  26. package/dist/routes/match.d.ts.map +1 -1
  27. package/dist/routes/match.js +22 -4
  28. package/dist/routes/render.d.ts.map +1 -1
  29. package/dist/routes/render.js +12 -3
  30. package/dist/server/adapters/hono.d.ts +1 -1
  31. package/dist/server/adapters/hono.d.ts.map +1 -1
  32. package/dist/server/adapters/hono.js +1 -1
  33. package/dist/server/adapters/node.d.ts +1 -1
  34. package/dist/server/adapters/node.d.ts.map +1 -1
  35. package/dist/server/adapters/node.js +2 -2
  36. package/dist/server/request-pipeline.d.ts +109 -0
  37. package/dist/server/request-pipeline.d.ts.map +1 -0
  38. package/dist/server/request-pipeline.js +198 -0
  39. package/dist/server/request.d.ts +3 -16
  40. package/dist/server/request.d.ts.map +1 -1
  41. package/dist/server/request.js +78 -173
  42. package/dist/server/{utils.d.ts → utilities.d.ts} +1 -1
  43. package/dist/server/utilities.d.ts.map +1 -0
  44. package/dist/test/init.d.ts +2 -0
  45. package/dist/test/init.d.ts.map +1 -0
  46. package/dist/test/init.js +7 -0
  47. package/dist/user-config.d.ts +48 -0
  48. package/dist/user-config.d.ts.map +1 -1
  49. package/dist/vite/build-routes.d.ts +1 -2
  50. package/dist/vite/build-routes.d.ts.map +1 -1
  51. package/dist/vite/build-routes.js +0 -98
  52. package/dist/vite/plugin-build-environment.d.ts +21 -0
  53. package/dist/vite/plugin-build-environment.d.ts.map +1 -0
  54. package/dist/vite/plugin-build-environment.js +83 -0
  55. package/dist/vite/plugin-ce-tracker.d.ts +19 -0
  56. package/dist/vite/plugin-ce-tracker.d.ts.map +1 -0
  57. package/dist/vite/plugin-ce-tracker.js +87 -0
  58. package/dist/vite/plugin-client-build.d.ts +20 -0
  59. package/dist/vite/plugin-client-build.d.ts.map +1 -0
  60. package/dist/vite/plugin-client-build.js +55 -0
  61. package/dist/vite/plugin-html-routes-build.d.ts +22 -0
  62. package/dist/vite/plugin-html-routes-build.d.ts.map +1 -0
  63. package/dist/vite/plugin-html-routes-build.js +140 -0
  64. package/dist/vite/plugin-serve.d.ts +18 -0
  65. package/dist/vite/plugin-serve.d.ts.map +1 -0
  66. package/dist/vite/plugin-serve.js +61 -0
  67. package/dist/vite/plugin-server-build.d.ts +43 -0
  68. package/dist/vite/plugin-server-build.d.ts.map +1 -0
  69. package/dist/vite/plugin-server-build.js +108 -0
  70. package/dist/vite/plugin-shared-state.d.ts +33 -0
  71. package/dist/vite/plugin-shared-state.d.ts.map +1 -0
  72. package/dist/vite/plugin-shared-state.js +23 -0
  73. package/dist/vite/virtual-routes.d.ts +8 -5
  74. package/dist/vite/virtual-routes.d.ts.map +1 -1
  75. package/dist/vite/virtual-routes.js +32 -31
  76. package/package.json +11 -11
  77. package/dist/render/utils.d.ts.map +0 -1
  78. package/dist/server/utils.d.ts.map +0 -1
  79. /package/dist/render/{utils.js → lit-ssr.js} +0 -0
  80. /package/dist/server/{utils.js → utilities.js} +0 -0
@@ -1,59 +1,56 @@
1
1
  import { Readable } from 'node:stream';
2
- import * as assert from '@gracile/internal-utils/assertions';
3
2
  import { getLogger } from '@gracile/internal-utils/logger/helpers';
4
3
  import c from 'picocolors';
5
- // import { GracileError } from '../errors/errors.js';
6
4
  import { builtIn404Page, builtInErrorPage } from '../errors/pages.js';
7
5
  import { renderRouteTemplate } from '../render/route-template.js';
8
- import { renderLitTemplate } from '../render/utils.js';
6
+ import { renderLitTemplate } from '../render/lit-ssr.js';
9
7
  import { getRoute } from '../routes/match.js';
10
- const CONTENT_TYPE_HTML = { 'Content-Type': 'text/html' };
11
- const PREMISE_REGEXES = {
12
- properties: /\/__(.*?)\.props\.json$/,
13
- document: /\/__(.*?)\.doc\.html$/,
14
- };
15
- export function isRedirect(response) {
16
- const location = response.headers.get('location');
17
- if (response.status >= 300 && response.status <= 303 && location) {
18
- return { location };
19
- }
20
- return null;
21
- }
8
+ import { CONTENT_TYPE_HTML, rewriteHiddenRoutes, resolvePremises, executeHandler, renderWithoutHandler, buildResponse, } from './request-pipeline.js';
22
9
  export function createGracileHandler({ vite, routes, routeImports, routeAssets, root, serverMode, gracileConfig, }) {
23
10
  const logger = getLogger();
24
11
  const middleware = async (request, locals) => {
25
12
  const { url: requestedUrl, method } = request;
26
13
  let emitViteBetterError = null;
27
14
  if (vite)
28
- emitViteBetterError = await import('../errors/create-vite-better-error.js').then(({ emitViteBetterError: error }) => error);
15
+ emitViteBetterError =
16
+ await import('../errors/create-vite-better-error.js').then(({ emitViteBetterError: error }) => error);
29
17
  try {
30
- // MARK: Rewrite hidden route siblings
31
- const fullUrl = requestedUrl.replace(/\/__(.*)$/, '/');
32
- // MARK: Setup premises
33
- const propertiesOnly = PREMISE_REGEXES.properties.test(requestedUrl);
34
- const documentOnly = PREMISE_REGEXES.document.test(requestedUrl);
35
- const allowPremises = Boolean(gracileConfig.pages?.premises?.expose);
36
- if (allowPremises === false && (propertiesOnly || documentOnly))
37
- throw new Error('Accessed a page premise but they are not activated. You must enable `pages.premises.expose`.');
38
- const premises = allowPremises ? { propertiesOnly, documentOnly } : null;
39
- // MARK: Get route infos
18
+ // MARK: 1. Rewrite hidden route siblings
19
+ const fullUrl = rewriteHiddenRoutes(requestedUrl);
20
+ // MARK: 2. Setup premises
21
+ const premises = resolvePremises(requestedUrl, gracileConfig);
22
+ // MARK: 3. Route resolution + 404 fallback
40
23
  const routeOptions = {
41
24
  url: fullUrl,
42
25
  vite,
43
26
  routes,
44
27
  routeImports,
28
+ trailingSlash: gracileConfig.trailingSlash ?? 'ignore',
45
29
  };
46
30
  const responseInit = {};
47
- let routeInfos = await getRoute(routeOptions);
48
- // MARK: 404
31
+ const routeResult = await getRoute(routeOptions);
32
+ // Trailing slash redirect (301 for GET, 308 for other methods)
33
+ if (routeResult && 'redirect' in routeResult) {
34
+ const redirectUrl = new URL(routeResult.redirect, fullUrl).href;
35
+ const status = method === 'GET' ? 301 : 308;
36
+ return { response: Response.redirect(redirectUrl, status) };
37
+ }
38
+ let routeInfos = routeResult;
49
39
  if (routeInfos === null) {
50
40
  responseInit.status = 404;
41
+ // Use 'ignore' for the internal 404 lookup to avoid redirect loops.
51
42
  const url = new URL('/404/', fullUrl).href;
52
- const options = { ...routeOptions, url };
53
- const notFound = await getRoute(options);
54
- routeInfos = notFound;
43
+ const options = {
44
+ ...routeOptions,
45
+ url,
46
+ trailingSlash: 'ignore',
47
+ };
48
+ const notFoundResult = await getRoute(options);
49
+ routeInfos =
50
+ notFoundResult && !('redirect' in notFoundResult)
51
+ ? notFoundResult
52
+ : null;
55
53
  }
56
- // MARK: fallback 404
57
54
  if (routeInfos === null) {
58
55
  const page = builtIn404Page(new URL(fullUrl).pathname, Boolean(vite));
59
56
  return {
@@ -65,163 +62,49 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
65
62
  const routeTemplateOptions = {
66
63
  url: fullUrl,
67
64
  vite,
68
- mode: 'dev', // vite && vite.config.mode === 'dev' ? 'dev' : 'build',
65
+ mode: 'dev',
69
66
  routeAssets,
70
67
  root,
71
68
  serverMode,
72
69
  routeInfos,
73
70
  docOnly: premises?.documentOnly,
71
+ renderInfo: gracileConfig.litSsr?.renderInfo,
74
72
  };
75
73
  logger.info(`[${c.yellow(method)}] ${c.yellow(fullUrl)}`, {
76
74
  timestamp: true,
77
75
  });
76
+ // MARK: 4. Handler dispatch
78
77
  let output;
79
- let providedLocals = {};
80
- if (locals && assert.isUnknownObject(locals))
81
- providedLocals = locals;
82
- // MARK: Server handler
83
- const handler = routeInfos.routeModule.handler;
84
- if (('handler' in routeInfos.routeModule && handler !== undefined) ||
85
- // TODO: Explain this condition
86
- (handler && 'GET' in handler === false && method !== 'GET')) {
87
- const routeContext = Object.freeze({
88
- request,
89
- url: new URL(fullUrl),
90
- responseInit,
91
- params: routeInfos.params,
92
- locals: providedLocals,
93
- });
94
- // MARK: Run user middleware
95
- // NOTE: Experimental
96
- /// eslint-disable-next-line no-inner-declarations
97
- // async function useHandler() {}
98
- // if (vite) {
99
- // const middleware = await vite
100
- // .ssrLoadModule('/src/middleware.ts')
101
- // .catch(() => null)
102
- // .then((m) => m.default);
103
- // if (middleware)
104
- // await middleware(
105
- // routeContext,
106
- // async () => {
107
- // await useHandler();
108
- // },
109
- // );
110
- // else await useHandler();
111
- // } else {
112
- // await useHandler();
113
- // }
114
- //
115
- // MARK: Handler(s)
116
- const hasTopLevelHandler = typeof handler === 'function';
117
- if (hasTopLevelHandler || method in handler) {
118
- const handlerWithMethod = hasTopLevelHandler
119
- ? handler
120
- : handler[method];
121
- if (typeof handlerWithMethod !== 'function')
122
- throw new TypeError('Handler must be a function.');
123
- const handlerOutput = await Promise.resolve(handlerWithMethod(routeContext));
124
- if (assert.isResponseOrPatchedResponse(handlerOutput))
125
- output = handlerOutput;
126
- else {
127
- routeTemplateOptions.routeInfos.props = hasTopLevelHandler
128
- ? handlerOutput
129
- : { [method]: handlerOutput };
130
- if (premises?.documentOnly) {
131
- const { document } = await renderRouteTemplate(routeTemplateOptions);
132
- return {
133
- response: new Response(document, {
134
- headers: { ...CONTENT_TYPE_HTML },
135
- }),
136
- };
137
- }
138
- if (premises?.propertiesOnly)
139
- return {
140
- response: Response.json(routeTemplateOptions.routeInfos.props),
141
- };
142
- output = await renderRouteTemplate(routeTemplateOptions).then((r) => r.output);
143
- }
144
- // MARK: No GET, render page
145
- }
146
- else {
147
- const statusText = `This route doesn't handle the \`${method}\` method!`;
148
- return {
149
- response: new Response(statusText, { status: 405, statusText }),
150
- };
151
- }
78
+ const handlerResult = await executeHandler({
79
+ routeInfos,
80
+ method,
81
+ request,
82
+ fullUrl,
83
+ locals,
84
+ responseInit,
85
+ premises,
86
+ routeTemplateOptions,
87
+ });
88
+ if (handlerResult.type === 'response')
89
+ return handlerResult.value;
90
+ if (handlerResult.type === 'output') {
91
+ output = handlerResult.value;
152
92
  }
153
93
  else {
154
- if (premises?.documentOnly) {
155
- const { document } = await renderRouteTemplate(routeTemplateOptions);
156
- return {
157
- response: new Response(document, {
158
- headers: { ...CONTENT_TYPE_HTML },
159
- }),
160
- };
161
- }
162
- if (premises?.propertiesOnly)
163
- return {
164
- response: Response.json(routeTemplateOptions.routeInfos.props || {}),
165
- };
166
- output = await renderRouteTemplate(routeTemplateOptions).then((r) => r.output);
167
- }
168
- // MARK: Return response
169
- // NOTE: try directly with the requestPonyfill. This might not be necessary
170
- if (assert.isResponseOrPatchedResponse(output)) {
171
- const redirect = isRedirect(output);
172
- if (redirect?.location)
173
- return {
174
- response: Response.redirect(redirect.location, output.status),
175
- };
176
- return { response: output };
177
- // MARK: Stream page render
178
- }
179
- // MARK: Page stream error
180
- if (output instanceof Readable) {
181
- responseInit.headers = {
182
- ...responseInit.headers,
183
- ...CONTENT_TYPE_HTML,
184
- };
185
- return {
186
- body: output.on('error', (error) => {
187
- const errorMessage = `[SSR Error] There was an error while rendering a template chunk on server-side.\n` +
188
- `It was omitted from the resulting HTML.\n`;
189
- if (vite) {
190
- logger.error(errorMessage + error.stack);
191
- // emitViteBetterError(new GracileError(GracileErrorData.FailedToGlobalLogger), vite);
192
- const payload = {
193
- type: 'error',
194
- // FIXME: Use the emitViteBetterError instead (but flaky for now with streaming)
195
- // err: new GracileError({}),
196
- err: {
197
- name: 'StreamingError',
198
- message: errorMessage,
199
- stack: error.stack,
200
- hint: 'This is often caused by a wrong template location dynamic interpolation.',
201
- cause: error,
202
- // highlightedCode: error.message,
203
- },
204
- };
205
- //
206
- setTimeout(() => {
207
- // @ts-expect-error ...........
208
- vite.hot.send(payload);
209
- // NOTE: Arbitrary value. Lower seems to be too fast, higher is not guaranteed to work.
210
- }, 200);
211
- }
212
- else {
213
- logger.error(errorMessage);
214
- }
215
- }),
216
- init: responseInit,
217
- };
94
+ // MARK: 5. Template-only render (no handler)
95
+ const renderResult = await renderWithoutHandler({
96
+ premises,
97
+ routeTemplateOptions,
98
+ });
99
+ if (renderResult && 'response' in renderResult)
100
+ return renderResult;
101
+ output = renderResult;
218
102
  }
219
- return null;
103
+ // MARK: 6. Build final response
104
+ return buildResponse({ output, responseInit, vite, logger });
220
105
  // MARK: Errors
221
106
  }
222
107
  catch (error) {
223
- // const safeError = createSafeError(error);
224
- // TODO: User defined dev/runtime 500 error
225
108
  const ultimateErrorPage = vite && emitViteBetterError
226
109
  ? await emitViteBetterError({ vite, error: error })
227
110
  : await renderLitTemplate(builtInErrorPage(error.name));
@@ -236,3 +119,25 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
236
119
  };
237
120
  return middleware;
238
121
  }
122
+ // MARK: Run user middleware
123
+ // NOTE: Experimental
124
+ /// eslint-disable-next-line no-inner-declarations
125
+ // async function useHandler() {}
126
+ // if (vite) {
127
+ // const middleware = await vite
128
+ // .ssrLoadModule('/src/middleware.ts')
129
+ // .catch(() => null)
130
+ // .then((m) => m.default);
131
+ // if (middleware)
132
+ // await middleware(
133
+ // routeContext,
134
+ // async () => {
135
+ // await useHandler();
136
+ // },
137
+ // );
138
+ // else await useHandler();
139
+ // } else {
140
+ // await useHandler();
141
+ // }
142
+ //
143
+ export { isRedirect, } from './request-pipeline.js';
@@ -18,4 +18,4 @@ import type { AddressInfo } from 'node:net';
18
18
  * ```
19
19
  */
20
20
  export declare function printUrls(server: string | AddressInfo | null): void;
21
- //# sourceMappingURL=utils.d.ts.map
21
+ //# sourceMappingURL=utilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../../src/server/utilities.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAY5C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,QA0C5D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/test/init.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Side-effect import: initializes the Gracile logger globally.
3
+ * Import this at the top of any engine unit test that touches modules
4
+ * which call `getLogger()` at module scope.
5
+ */
6
+ import { createLogger } from '@gracile/internal-utils/logger/helpers';
7
+ createLogger();
@@ -1,3 +1,4 @@
1
+ import type { RenderInfo } from '@lit-labs/ssr';
1
2
  import type { Connect } from 'vite';
2
3
  /**
3
4
  * @example
@@ -37,6 +38,19 @@ export interface GracileConfig {
37
38
  * @defaultValue 'static'
38
39
  */
39
40
  output?: 'static' | 'server';
41
+ /**
42
+ * Controls how trailing slashes are matched on incoming URLs.
43
+ *
44
+ * - `'ignore'` — Match regardless of whether a trailing `/` is present.
45
+ * `/about` and `/about/` both resolve to the same route. *(default)*
46
+ * - `'always'` — Only match URLs that include a trailing slash (e.g. `/about/`).
47
+ * Requests without one are redirected: `301` for GET, `308` for other methods.
48
+ * - `'never'` — Only match URLs that do not include a trailing slash (e.g. `/about`).
49
+ * Requests with one are redirected: `301` for GET, `308` for other methods.
50
+ *
51
+ * @defaultValue 'ignore'
52
+ */
53
+ trailingSlash?: 'always' | 'never' | 'ignore';
40
54
  /**
41
55
  * Settings for the development mode.
42
56
  */
@@ -93,6 +107,40 @@ export interface GracileConfig {
93
107
  exclude?: string[];
94
108
  };
95
109
  };
110
+ litSsr?: {
111
+ /**
112
+ * Lets you extend Gracile's SSR pipeline with
113
+ * custom Lit SSR `ElementRenderer` subclasses. This is the foundation for
114
+ * features like [Islands](/docs/add-ons/islands/), which register a renderer
115
+ * for the `<is-land>` custom element to server-render components from other
116
+ * UI frameworks.
117
+ *
118
+ * In most cases, you do **not** set this option manually — add-on plugins
119
+ * (like `gracileIslands()`) register their renderers automatically via the
120
+ * plugin context communication channel. However,
121
+ * you can use it directly for advanced use cases:
122
+ *
123
+ * ```ts
124
+ * import { gracile } from '@gracile/gracile/plugin';
125
+ * import { defineConfig } from 'vite';
126
+ *
127
+ * export default defineConfig({
128
+ * plugins: [
129
+ * gracile({
130
+ * litSsr: {
131
+ * renderInfo: {
132
+ * elementRenderers: [
133
+ * // Your custom ElementRenderer subclass
134
+ * ],
135
+ * },
136
+ * },
137
+ * }),
138
+ * ],
139
+ * });
140
+ * ```
141
+ */
142
+ renderInfo?: Partial<RenderInfo>;
143
+ };
96
144
  /**
97
145
  * Future, unstable features flags.
98
146
  */
@@ -1 +1 @@
1
- {"version":3,"file":"user-config.d.ts","sourceRoot":"","sources":["../src/user-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,aAAa;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAE7B;;OAEG;IACH,GAAG,CAAC,EAAE;QACL;;;;;WAKG;QACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE;YAAE,WAAW,EAAE,OAAO,CAAC,eAAe,CAAA;SAAE,KAAK,OAAO,CAAC;KACxE,CAAC;IAEF;;OAEG;IACH,MAAM,CAAC,EAAE;QACR;;WAEG;QACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IAEF;;OAEG;IACH,KAAK,CAAC,EAAE;QACP;;;;;;;;;;;;;WAaG;QACH,QAAQ,CAAC,EAAE;YACV;;eAEG;YACH,MAAM,CAAC,EAAE,OAAO,CAAC;YAGjB;;eAEG;YACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;YAEnB;;eAEG;YACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;SACnB,CAAC;KACF,CAAC;IAEF;;OAEG;IACH,YAAY,CAAC,EAAE;QACd;;;WAGG;QACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;KAOhC,CAAC;CACF"}
1
+ {"version":3,"file":"user-config.d.ts","sourceRoot":"","sources":["../src/user-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,aAAa;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAE7B;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IAE9C;;OAEG;IACH,GAAG,CAAC,EAAE;QACL;;;;;WAKG;QACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE;YAAE,WAAW,EAAE,OAAO,CAAC,eAAe,CAAA;SAAE,KAAK,OAAO,CAAC;KACxE,CAAC;IAEF;;OAEG;IACH,MAAM,CAAC,EAAE;QACR;;WAEG;QACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IAEF;;OAEG;IACH,KAAK,CAAC,EAAE;QACP;;;;;;;;;;;;;WAaG;QACH,QAAQ,CAAC,EAAE;YACV;;eAEG;YACH,MAAM,CAAC,EAAE,OAAO,CAAC;YAGjB;;eAEG;YACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;YAEnB;;eAEG;YACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;SACnB,CAAC;KACF,CAAC;IACF,MAAM,CAAC,EAAE;QACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WA8BG;QACH,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;KACjC,CAAC;IAEF;;OAEG;IACH,YAAY,CAAC,EAAE;QACd;;;WAGG;QACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;KAOhC,CAAC;CACF"}
@@ -1,4 +1,4 @@
1
- import { type Plugin, type ViteDevServer } from 'vite';
1
+ import { type ViteDevServer } from 'vite';
2
2
  import { type RenderedRouteDefinition } from '../routes/render.js';
3
3
  import type { RoutesManifest } from '../routes/route.js';
4
4
  import type { GracileConfig } from '../user-config.js';
@@ -12,6 +12,5 @@ export declare const buildRoutes: ({ routes, viteServerForBuild, root, gracileCo
12
12
  routes: RoutesManifest;
13
13
  renderedRoutes: RenderedRouteDefinition[];
14
14
  inputList: string[];
15
- plugin: Plugin[];
16
15
  }>;
17
16
  //# sourceMappingURL=build-routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-routes.d.ts","sourceRoot":"","sources":["../../src/vite/build-routes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,MAAM,CAAC;AAErE,OAAO,EAEN,KAAK,uBAAuB,EAC5B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AASvD,eAAO,MAAM,WAAW,qEAMrB;IACF,MAAM,EAAE,cAAc,CAAC;IACvB,kBAAkB,EAAE,aAAa,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB,KAAG,OAAO,CAAC;IACX,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,EAAE,uBAAuB,EAAE,CAAC;IAC1C,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CACjB,CA6JA,CAAC"}
1
+ {"version":3,"file":"build-routes.d.ts","sourceRoot":"","sources":["../../src/vite/build-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,MAAM,CAAC;AAExD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AASvD,eAAO,MAAM,WAAW,GAAU,kEAM/B;IACF,MAAM,EAAE,cAAc,CAAC;IACvB,kBAAkB,EAAE,aAAa,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB,KAAG,OAAO,CAAC;IACX,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,EAAE,uBAAuB,EAAE,CAAC;IAC1C,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB,CAkCA,CAAC"}
@@ -1,7 +1,5 @@
1
- import { basename, extname, join } from 'node:path';
2
1
  import { createFilter } from 'vite';
3
2
  import { renderRoutes, } from '../routes/render.js';
4
- import { REGEX_TAG_LINK, REGEX_TAG_SCRIPT } from '../render/route-template.js';
5
3
  function stripPremises(input) {
6
4
  return input
7
5
  .replace(/index\.html$/, '__index.doc.html')
@@ -9,7 +7,6 @@ function stripPremises(input) {
9
7
  .replace(/500\.html$/, '__500.doc.html');
10
8
  }
11
9
  export const buildRoutes = async ({ routes, viteServerForBuild, root, gracileConfig, serverMode = false, }) => {
12
- // TODO: extract upstream, return just the plugins
13
10
  const { renderedRoutes } = await renderRoutes({
14
11
  vite: viteServerForBuild,
15
12
  serverMode,
@@ -35,100 +32,5 @@ export const buildRoutes = async ({ routes, viteServerForBuild, root, gracileCon
35
32
  routes,
36
33
  renderedRoutes,
37
34
  inputList,
38
- plugin: [
39
- {
40
- name: 'gracile-html-routes',
41
- apply: 'build',
42
- enforce: 'pre',
43
- resolveId(id) {
44
- if (extname(id) === '.html' && inputList.includes(id))
45
- return join(root, id);
46
- return null;
47
- },
48
- load(id) {
49
- if (extname(id) === '.html') {
50
- if (['index.html', '404.html', '500.html'].includes(basename(id))) {
51
- const content = renderedRoutes.find((index) => index.absoluteId === id)?.html;
52
- if (content)
53
- return content;
54
- }
55
- if (gracileConfig.pages?.premises?.expose) {
56
- if (basename(id).endsWith('doc.html')) {
57
- const content = renderedRoutes.find((index) => stripPremises(index.absoluteId) === id)?.static.document;
58
- if (content)
59
- return content;
60
- }
61
- const content = renderedRoutes.find((index) => index.name === basename(id))?.html;
62
- if (content)
63
- return content;
64
- // return '';}
65
- }
66
- }
67
- return null;
68
- },
69
- },
70
- {
71
- name: 'gracile-collect-handler-assets',
72
- enforce: 'post',
73
- buildStart() {
74
- if (!gracileConfig.pages?.premises?.expose || !premisesFilter)
75
- return;
76
- for (const route of renderedRoutes) {
77
- if (premisesFilter(route.name) === false)
78
- continue;
79
- if (serverMode && route.savePrerender !== true)
80
- continue;
81
- const fileNameParts = route.name.split('/');
82
- const last = fileNameParts.pop();
83
- const properties = last?.replace(/(.*)\.html$/, (_, r) => `__${r}.props.json`);
84
- if (!properties)
85
- throw new Error('No props.');
86
- fileNameParts.push(properties);
87
- const fileName = fileNameParts.join('/');
88
- this.emitFile({
89
- fileName,
90
- type: 'asset',
91
- source: JSON.stringify(route.static.props ?? {}),
92
- });
93
- }
94
- },
95
- generateBundle(_, bundle) {
96
- if (serverMode === false)
97
- return;
98
- for (const fileKey in bundle) {
99
- const file = bundle[fileKey];
100
- if (fileKey.endsWith('.html') && file && 'source' in file) {
101
- const source = file.source.toString();
102
- const collectedAssets = [
103
- ...[...source.matchAll(REGEX_TAG_SCRIPT)]
104
- .map((v) => v[0])
105
- // NOTE: Too brittle
106
- .filter((v) => v.includes(`type="module"`)),
107
- ...[...source.matchAll(REGEX_TAG_LINK)]
108
- .map((v) => v[0])
109
- // NOTE: Too brittle
110
- .filter((v) => /rel="(stylesheet|modulepreload)"/.test(v)),
111
- ].join('\n');
112
- // NOTE: Not used (for now?)
113
- // file.source = alteredContent;
114
- const route = renderedRoutes.find((r) => {
115
- if (gracileConfig.pages?.premises?.expose)
116
- return (`/${r.name}` ===
117
- `/${fileKey}`.replace(/(.*?)\/__(.*?)\.doc\.html$/, (a, b, c) => `${b}/${c}.html`));
118
- return r.name === fileKey;
119
- });
120
- if (route)
121
- route.handlerAssets = collectedAssets;
122
- if (route?.savePrerender !== true) {
123
- delete bundle[fileKey];
124
- // NOTE: Not sure if it's useful
125
- if (route?.html)
126
- route.html = null;
127
- }
128
- }
129
- }
130
- },
131
- },
132
- ],
133
35
  };
134
36
  };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Vite plugin: build environment configuration.
3
+ *
4
+ * Owns all Environment API concerns for builds:
5
+ * - Top-level config: `builder.sharedConfigBuild`, `ssr.external` (server mode)
6
+ * - `configEnvironment('client')`: rollupOptions.input, outDir
7
+ * - `configEnvironment('ssr')`: outDir, SSR-specific build options, rollupOptions
8
+ * - `buildApp()`: orchestrates build order (client-only or client → SSR)
9
+ *
10
+ * `rollupOptions.input` must be set via `configEnvironment` (not the
11
+ * top-level `config` hook) because `builder.build(env)` uses
12
+ * environment-resolved config, not the top-level config.
13
+ *
14
+ * @internal
15
+ */
16
+ import type { PluginOption } from 'vite';
17
+ import type { PluginSharedState } from './plugin-shared-state.js';
18
+ export declare function gracileBuildEnvironmentPlugin({ state, }: {
19
+ state: PluginSharedState;
20
+ }): PluginOption;
21
+ //# sourceMappingURL=plugin-build-environment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-build-environment.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-build-environment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,MAAM,CAAC;AAEtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,wBAAgB,6BAA6B,CAAC,EAC7C,KAAK,GACL,EAAE;IACF,KAAK,EAAE,iBAAiB,CAAC;CACzB,GAAG,YAAY,CA4Ef"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Vite plugin: build environment configuration.
3
+ *
4
+ * Owns all Environment API concerns for builds:
5
+ * - Top-level config: `builder.sharedConfigBuild`, `ssr.external` (server mode)
6
+ * - `configEnvironment('client')`: rollupOptions.input, outDir
7
+ * - `configEnvironment('ssr')`: outDir, SSR-specific build options, rollupOptions
8
+ * - `buildApp()`: orchestrates build order (client-only or client → SSR)
9
+ *
10
+ * `rollupOptions.input` must be set via `configEnvironment` (not the
11
+ * top-level `config` hook) because `builder.build(env)` uses
12
+ * environment-resolved config, not the top-level config.
13
+ *
14
+ * @internal
15
+ */
16
+ import { join } from 'node:path';
17
+ export function gracileBuildEnvironmentPlugin({ state, }) {
18
+ const isServerMode = state.outputMode === 'server';
19
+ return {
20
+ name: 'vite-plugin-gracile-build-env',
21
+ apply: 'build',
22
+ config() {
23
+ return {
24
+ ...(isServerMode ? { ssr: { external: ['@gracile/gracile'] } } : {}),
25
+ builder: { sharedConfigBuild: true },
26
+ };
27
+ },
28
+ configEnvironment(name, config) {
29
+ if (name === 'client') {
30
+ if (!state.clientBuildInputList)
31
+ return null;
32
+ return {
33
+ build: {
34
+ rollupOptions: {
35
+ input: state.clientBuildInputList,
36
+ },
37
+ outDir: join(config.build?.outDir || 'dist', isServerMode ? 'client' : ''),
38
+ },
39
+ };
40
+ }
41
+ if (name === 'ssr' && isServerMode) {
42
+ return {
43
+ build: {
44
+ outDir: 'dist/server',
45
+ copyPublicDir: false,
46
+ ssrEmitAssets: true,
47
+ cssMinify: true,
48
+ cssCodeSplit: true,
49
+ rollupOptions: {
50
+ input: 'entrypoint.js',
51
+ output: {
52
+ entryFileNames: '[name].js',
53
+ assetFileNames: (chunkInfo) => {
54
+ if (chunkInfo.name) {
55
+ const fileName = state.clientAssets[chunkInfo.name];
56
+ if (fileName)
57
+ return fileName;
58
+ return `assets/${chunkInfo.name.replace(/\.(.*)$/, '')}-[hash].[ext]`;
59
+ }
60
+ return 'assets/[name]-[hash].[ext]';
61
+ },
62
+ chunkFileNames: 'chunk/[name].js',
63
+ },
64
+ },
65
+ },
66
+ };
67
+ }
68
+ return null;
69
+ },
70
+ async buildApp(builder) {
71
+ const client = builder.environments['client'];
72
+ if (!client)
73
+ throw new Error('Missing client build environment.');
74
+ await builder.build(client);
75
+ if (isServerMode) {
76
+ const ssr = builder.environments['ssr'];
77
+ if (!ssr)
78
+ throw new Error('Missing ssr build environment.');
79
+ await builder.build(ssr);
80
+ }
81
+ },
82
+ };
83
+ }