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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/plugin.d.ts.map +1 -1
  2. package/dist/plugin.js +37 -262
  3. package/dist/render/route-template-pipeline.d.ts +64 -0
  4. package/dist/render/route-template-pipeline.d.ts.map +1 -0
  5. package/dist/render/route-template-pipeline.js +144 -0
  6. package/dist/render/route-template.d.ts +1 -2
  7. package/dist/render/route-template.d.ts.map +1 -1
  8. package/dist/render/route-template.js +12 -92
  9. package/dist/routes/collect.d.ts +4 -0
  10. package/dist/routes/collect.d.ts.map +1 -1
  11. package/dist/routes/collect.js +2 -1
  12. package/dist/routes/match.d.ts +26 -0
  13. package/dist/routes/match.d.ts.map +1 -1
  14. package/dist/routes/match.js +4 -2
  15. package/dist/server/request-pipeline.d.ts +109 -0
  16. package/dist/server/request-pipeline.d.ts.map +1 -0
  17. package/dist/server/request-pipeline.js +198 -0
  18. package/dist/server/request.d.ts +3 -16
  19. package/dist/server/request.d.ts.map +1 -1
  20. package/dist/server/request.js +55 -169
  21. package/dist/test/init.d.ts +2 -0
  22. package/dist/test/init.d.ts.map +1 -0
  23. package/dist/test/init.js +7 -0
  24. package/dist/vite/plugin-client-build.d.ts +16 -0
  25. package/dist/vite/plugin-client-build.d.ts.map +1 -0
  26. package/dist/vite/plugin-client-build.js +49 -0
  27. package/dist/vite/plugin-serve.d.ts +18 -0
  28. package/dist/vite/plugin-serve.d.ts.map +1 -0
  29. package/dist/vite/plugin-serve.js +62 -0
  30. package/dist/vite/plugin-server-build.d.ts +33 -0
  31. package/dist/vite/plugin-server-build.d.ts.map +1 -0
  32. package/dist/vite/plugin-server-build.js +157 -0
  33. package/dist/vite/plugin-shared-state.d.ts +31 -0
  34. package/dist/vite/plugin-shared-state.d.ts.map +1 -0
  35. package/dist/vite/plugin-shared-state.js +22 -0
  36. package/package.json +2 -2
@@ -1,24 +1,11 @@
1
1
  import { Readable } from 'node:stream';
2
- import * as assert from '@gracile/internal-utils/assertions';
3
2
  import { getLogger } from '@gracile/internal-utils/logger/helpers';
4
3
  import c from 'picocolors';
5
- // import { GracileError } from '../errors/errors.js';
6
4
  import { builtIn404Page, builtInErrorPage } from '../errors/pages.js';
7
5
  import { renderRouteTemplate } from '../render/route-template.js';
8
6
  import { renderLitTemplate } from '../render/lit-ssr.js';
9
7
  import { getRoute } from '../routes/match.js';
10
- const CONTENT_TYPE_HTML = { 'Content-Type': 'text/html' };
11
- const PREMISE_REGEXES = {
12
- properties: /\/__(.*?)\.props\.json$/,
13
- document: /\/__(.*?)\.doc\.html$/,
14
- };
15
- export function isRedirect(response) {
16
- const location = response.headers.get('location');
17
- if (response.status >= 300 && response.status <= 303 && location) {
18
- return { location };
19
- }
20
- return null;
21
- }
8
+ import { CONTENT_TYPE_HTML, rewriteHiddenRoutes, resolvePremises, executeHandler, renderWithoutHandler, buildResponse, } from './request-pipeline.js';
22
9
  export function createGracileHandler({ vite, routes, routeImports, routeAssets, root, serverMode, gracileConfig, }) {
23
10
  const logger = getLogger();
24
11
  const middleware = async (request, locals) => {
@@ -28,16 +15,11 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
28
15
  emitViteBetterError =
29
16
  await import('../errors/create-vite-better-error.js').then(({ emitViteBetterError: error }) => error);
30
17
  try {
31
- // MARK: Rewrite hidden route siblings
32
- const fullUrl = requestedUrl.replace(/\/__(.*)$/, '/');
33
- // MARK: Setup premises
34
- const propertiesOnly = PREMISE_REGEXES.properties.test(requestedUrl);
35
- const documentOnly = PREMISE_REGEXES.document.test(requestedUrl);
36
- const allowPremises = Boolean(gracileConfig.pages?.premises?.expose);
37
- if (allowPremises === false && (propertiesOnly || documentOnly))
38
- throw new Error('Accessed a page premise but they are not activated. You must enable `pages.premises.expose`.');
39
- const premises = allowPremises ? { propertiesOnly, documentOnly } : null;
40
- // MARK: Get route infos
18
+ // MARK: 1. Rewrite hidden route siblings
19
+ const fullUrl = rewriteHiddenRoutes(requestedUrl);
20
+ // MARK: 2. Setup premises
21
+ const premises = resolvePremises(requestedUrl, gracileConfig);
22
+ // MARK: 3. Route resolution + 404 fallback
41
23
  const routeOptions = {
42
24
  url: fullUrl,
43
25
  vite,
@@ -46,15 +28,12 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
46
28
  };
47
29
  const responseInit = {};
48
30
  let routeInfos = await getRoute(routeOptions);
49
- // MARK: 404
50
31
  if (routeInfos === null) {
51
32
  responseInit.status = 404;
52
33
  const url = new URL('/404/', fullUrl).href;
53
34
  const options = { ...routeOptions, url };
54
- const notFound = await getRoute(options);
55
- routeInfos = notFound;
35
+ routeInfos = await getRoute(options);
56
36
  }
57
- // MARK: fallback 404
58
37
  if (routeInfos === null) {
59
38
  const page = builtIn404Page(new URL(fullUrl).pathname, Boolean(vite));
60
39
  return {
@@ -66,7 +45,7 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
66
45
  const routeTemplateOptions = {
67
46
  url: fullUrl,
68
47
  vite,
69
- mode: 'dev', // vite && vite.config.mode === 'dev' ? 'dev' : 'build',
48
+ mode: 'dev',
70
49
  routeAssets,
71
50
  root,
72
51
  serverMode,
@@ -77,153 +56,38 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
77
56
  logger.info(`[${c.yellow(method)}] ${c.yellow(fullUrl)}`, {
78
57
  timestamp: true,
79
58
  });
59
+ // MARK: 4. Handler dispatch
80
60
  let output;
81
- let providedLocals = {};
82
- if (locals && assert.isUnknownObject(locals))
83
- providedLocals = locals;
84
- // MARK: Server handler
85
- const handler = routeInfos.routeModule.handler;
86
- if (('handler' in routeInfos.routeModule && handler !== undefined) ||
87
- // TODO: Explain this condition
88
- (handler && 'GET' in handler === false && method !== 'GET')) {
89
- const routeContext = Object.freeze({
90
- request,
91
- url: new URL(fullUrl),
92
- responseInit,
93
- params: routeInfos.params,
94
- locals: providedLocals,
95
- });
96
- // MARK: Run user middleware
97
- // NOTE: Experimental
98
- /// eslint-disable-next-line no-inner-declarations
99
- // async function useHandler() {}
100
- // if (vite) {
101
- // const middleware = await vite
102
- // .ssrLoadModule('/src/middleware.ts')
103
- // .catch(() => null)
104
- // .then((m) => m.default);
105
- // if (middleware)
106
- // await middleware(
107
- // routeContext,
108
- // async () => {
109
- // await useHandler();
110
- // },
111
- // );
112
- // else await useHandler();
113
- // } else {
114
- // await useHandler();
115
- // }
116
- //
117
- // MARK: Handler(s)
118
- const hasTopLevelHandler = typeof handler === 'function';
119
- if (hasTopLevelHandler || method in handler) {
120
- const handlerWithMethod = hasTopLevelHandler
121
- ? handler
122
- : handler[method];
123
- if (typeof handlerWithMethod !== 'function')
124
- throw new TypeError('Handler must be a function.');
125
- const handlerOutput = await Promise.resolve(handlerWithMethod(routeContext));
126
- if (assert.isResponseOrPatchedResponse(handlerOutput))
127
- output = handlerOutput;
128
- else {
129
- routeTemplateOptions.routeInfos.props = hasTopLevelHandler
130
- ? handlerOutput
131
- : { [method]: handlerOutput };
132
- if (premises?.documentOnly) {
133
- const { document } = await renderRouteTemplate(routeTemplateOptions);
134
- return {
135
- response: new Response(document, {
136
- headers: { ...CONTENT_TYPE_HTML },
137
- }),
138
- };
139
- }
140
- if (premises?.propertiesOnly)
141
- return {
142
- response: Response.json(routeTemplateOptions.routeInfos.props),
143
- };
144
- output = await renderRouteTemplate(routeTemplateOptions).then((r) => r.output);
145
- }
146
- // MARK: No GET, render page
147
- }
148
- else {
149
- const statusText = `This route doesn't handle the \`${method}\` method!`;
150
- return {
151
- response: new Response(statusText, { status: 405, statusText }),
152
- };
153
- }
61
+ const handlerResult = await executeHandler({
62
+ routeInfos,
63
+ method,
64
+ request,
65
+ fullUrl,
66
+ locals,
67
+ responseInit,
68
+ premises,
69
+ routeTemplateOptions,
70
+ });
71
+ if (handlerResult.type === 'response')
72
+ return handlerResult.value;
73
+ if (handlerResult.type === 'output') {
74
+ output = handlerResult.value;
154
75
  }
155
76
  else {
156
- if (premises?.documentOnly) {
157
- const { document } = await renderRouteTemplate(routeTemplateOptions);
158
- return {
159
- response: new Response(document, {
160
- headers: { ...CONTENT_TYPE_HTML },
161
- }),
162
- };
163
- }
164
- if (premises?.propertiesOnly)
165
- return {
166
- response: Response.json(routeTemplateOptions.routeInfos.props || {}),
167
- };
168
- output = await renderRouteTemplate(routeTemplateOptions).then((r) => r.output);
169
- }
170
- // MARK: Return response
171
- // NOTE: try directly with the requestPonyfill. This might not be necessary
172
- if (assert.isResponseOrPatchedResponse(output)) {
173
- const redirect = isRedirect(output);
174
- if (redirect?.location)
175
- return {
176
- response: Response.redirect(redirect.location, output.status),
177
- };
178
- return { response: output };
179
- // MARK: Stream page render
180
- }
181
- // MARK: Page stream error
182
- if (output instanceof Readable) {
183
- responseInit.headers = {
184
- ...responseInit.headers,
185
- ...CONTENT_TYPE_HTML,
186
- };
187
- return {
188
- body: output.on('error', (error) => {
189
- const errorMessage = `[SSR Error] There was an error while rendering a template chunk on server-side.\n` +
190
- `It was omitted from the resulting HTML.\n`;
191
- if (vite) {
192
- logger.error(errorMessage + error.stack);
193
- // emitViteBetterError(new GracileError(GracileErrorData.FailedToGlobalLogger), vite);
194
- const payload = {
195
- type: 'error',
196
- // FIXME: Use the emitViteBetterError instead (but flaky for now with streaming)
197
- // err: new GracileError({}),
198
- err: {
199
- name: 'StreamingError',
200
- message: errorMessage,
201
- stack: error.stack,
202
- hint: 'This is often caused by a wrong template location dynamic interpolation.',
203
- cause: error,
204
- // highlightedCode: error.message,
205
- },
206
- };
207
- //
208
- setTimeout(() => {
209
- // @ts-expect-error ...........
210
- vite.hot.send(payload);
211
- // NOTE: Arbitrary value. Lower seems to be too fast, higher is not guaranteed to work.
212
- }, 200);
213
- }
214
- else {
215
- logger.error(errorMessage);
216
- }
217
- }),
218
- init: responseInit,
219
- };
77
+ // MARK: 5. Template-only render (no handler)
78
+ const renderResult = await renderWithoutHandler({
79
+ premises,
80
+ routeTemplateOptions,
81
+ });
82
+ if (renderResult && 'response' in renderResult)
83
+ return renderResult;
84
+ output = renderResult;
220
85
  }
221
- return null;
86
+ // MARK: 6. Build final response
87
+ return buildResponse({ output, responseInit, vite, logger });
222
88
  // MARK: Errors
223
89
  }
224
90
  catch (error) {
225
- // const safeError = createSafeError(error);
226
- // TODO: User defined dev/runtime 500 error
227
91
  const ultimateErrorPage = vite && emitViteBetterError
228
92
  ? await emitViteBetterError({ vite, error: error })
229
93
  : await renderLitTemplate(builtInErrorPage(error.name));
@@ -238,3 +102,25 @@ export function createGracileHandler({ vite, routes, routeImports, routeAssets,
238
102
  };
239
103
  return middleware;
240
104
  }
105
+ // MARK: Run user middleware
106
+ // NOTE: Experimental
107
+ /// eslint-disable-next-line no-inner-declarations
108
+ // async function useHandler() {}
109
+ // if (vite) {
110
+ // const middleware = await vite
111
+ // .ssrLoadModule('/src/middleware.ts')
112
+ // .catch(() => null)
113
+ // .then((m) => m.default);
114
+ // if (middleware)
115
+ // await middleware(
116
+ // routeContext,
117
+ // async () => {
118
+ // await useHandler();
119
+ // },
120
+ // );
121
+ // else await useHandler();
122
+ // } else {
123
+ // await useHandler();
124
+ // }
125
+ //
126
+ export { isRedirect, } from './request-pipeline.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/test/init.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Side-effect import: initializes the Gracile logger globally.
3
+ * Import this at the top of any engine unit test that touches modules
4
+ * which call `getLogger()` at module scope.
5
+ */
6
+ import { createLogger } from '@gracile/internal-utils/logger/helpers';
7
+ createLogger();
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Vite plugin: client-side build configuration.
3
+ *
4
+ * During `vite build`, this spins up a temporary dev server to render
5
+ * all routes into static HTML, then configures Rollup with the
6
+ * generated HTML pages as inputs.
7
+ *
8
+ * @internal
9
+ */
10
+ import { type PluginOption } from 'vite';
11
+ import type { PluginSharedState } from './plugin-shared-state.js';
12
+ export declare function gracileClientBuildPlugin({ state, virtualRoutesForClient, }: {
13
+ state: PluginSharedState;
14
+ virtualRoutesForClient: PluginOption;
15
+ }): PluginOption;
16
+ //# sourceMappingURL=plugin-client-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-client-build.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-client-build.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,wBAAgB,wBAAwB,CAAC,EACxC,KAAK,EACL,sBAAsB,GACtB,EAAE;IACF,KAAK,EAAE,iBAAiB,CAAC;IACzB,sBAAsB,EAAE,YAAY,CAAC;CACrC,GAAG,YAAY,CAkDf"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Vite plugin: client-side build configuration.
3
+ *
4
+ * During `vite build`, this spins up a temporary dev server to render
5
+ * all routes into static HTML, then configures Rollup with the
6
+ * generated HTML pages as inputs.
7
+ *
8
+ * @internal
9
+ */
10
+ import { join } from 'node:path';
11
+ import { getPluginContext } from '@gracile/internal-utils/plugin-context';
12
+ import { createServer } from 'vite';
13
+ import { buildRoutes } from './build-routes.js';
14
+ export function gracileClientBuildPlugin({ state, virtualRoutesForClient, }) {
15
+ return {
16
+ name: 'vite-plugin-gracile-build',
17
+ apply: 'build',
18
+ async config(viteConfig) {
19
+ const viteServerForClientHtmlBuild = await createServer({
20
+ root: viteConfig.root || process.cwd(),
21
+ server: { middlewareMode: true },
22
+ // NOTE: Stub. KEEP IT!
23
+ optimizeDeps: { include: [] },
24
+ plugins: [virtualRoutesForClient],
25
+ });
26
+ // NOTE: Important. Get the dev. server elements renderers.
27
+ state.gracileConfig.litSsr ??= {};
28
+ state.gracileConfig.litSsr.renderInfo = getPluginContext(viteServerForClientHtmlBuild.config)?.litSsrRenderInfo;
29
+ const htmlPages = await buildRoutes({
30
+ viteServerForBuild: viteServerForClientHtmlBuild,
31
+ root: viteConfig.root || process.cwd(),
32
+ gracileConfig: state.gracileConfig,
33
+ serverMode: state.outputMode === 'server',
34
+ routes: state.routes,
35
+ });
36
+ state.renderedRoutes = htmlPages.renderedRoutes;
37
+ await viteServerForClientHtmlBuild.close();
38
+ return {
39
+ build: {
40
+ rollupOptions: {
41
+ input: htmlPages.inputList,
42
+ plugins: [htmlPages.plugin],
43
+ },
44
+ outDir: join(viteConfig.build?.outDir || 'dist', state.outputMode === 'server' ? 'client' : ''),
45
+ },
46
+ };
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Vite plugin: development server middleware.
3
+ *
4
+ * Sets up the Gracile request handler, route watcher, and dev-time
5
+ * logging for `vite dev`.
6
+ *
7
+ * @internal
8
+ */
9
+ import type { Logger, PluginOption } from 'vite';
10
+ import type { GracileConfig } from '../user-config.js';
11
+ import type { PluginSharedState } from './plugin-shared-state.js';
12
+ export declare function gracileServePlugin({ state, config, logger, resetClientBuiltFlag, }: {
13
+ state: PluginSharedState;
14
+ config: GracileConfig | undefined;
15
+ logger: Logger;
16
+ resetClientBuiltFlag: () => void;
17
+ }): PluginOption;
18
+ //# sourceMappingURL=plugin-serve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-serve.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-serve.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAIjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,wBAAgB,kBAAkB,CAAC,EAClC,KAAK,EACL,MAAM,EACN,MAAM,EACN,oBAAoB,GACpB,EAAE;IACF,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB,EAAE,MAAM,IAAI,CAAC;CACjC,GAAG,YAAY,CA6Df"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Vite plugin: development server middleware.
3
+ *
4
+ * Sets up the Gracile request handler, route watcher, and dev-time
5
+ * logging for `vite dev`.
6
+ *
7
+ * @internal
8
+ */
9
+ import { getVersion } from '@gracile/internal-utils/version';
10
+ import c from 'picocolors';
11
+ import { createDevelopmentHandler } from '../dev/development.js';
12
+ import { nodeAdapter } from '../server/adapters/node.js';
13
+ export function gracileServePlugin({ state, config, logger, resetClientBuiltFlag, }) {
14
+ return {
15
+ name: 'vite-plugin-gracile-serve-middleware',
16
+ apply: 'serve',
17
+ config(_, environment) {
18
+ if (environment.isPreview)
19
+ return null;
20
+ return {
21
+ // NOTE: Supresses message: `Could not auto-determine entry point from rollupOptions or html files…`
22
+ // FIXME: It's not working when reloading the Vite config.
23
+ // Is user config, putting `optimizeDeps: { include: [] }` solve this.
24
+ optimizeDeps: { include: [] },
25
+ // NOTE: Useful? It breaks preview (expected)
26
+ appType: 'custom',
27
+ };
28
+ },
29
+ async configureServer(server) {
30
+ // HACK: We know we are in dev here, this will prevent incorrect
31
+ // vite.config hot reloading. Will be removed when adopting env. API.
32
+ resetClientBuiltFlag();
33
+ const version = getVersion();
34
+ logger.info(`${c.cyan(c.italic(c.underline('🧚 Gracile')))}` +
35
+ ` ${c.dim(`~`)} ${c.green(`v${version ?? 'X'}`)}`);
36
+ const { handler } = await createDevelopmentHandler({
37
+ routes: state.routes,
38
+ vite: server,
39
+ gracileConfig: state.gracileConfig,
40
+ });
41
+ logger.info(c.dim('Vite development server is starting…'), {
42
+ timestamp: true,
43
+ });
44
+ server.watcher.on('ready', () => {
45
+ setTimeout(() => {
46
+ logger.info('');
47
+ logger.info(c.green('Watching for file changes…'), {
48
+ timestamp: true,
49
+ });
50
+ logger.info('');
51
+ // NOTE: We want it to show after the Vite intro stuff
52
+ }, 100);
53
+ });
54
+ return () => {
55
+ server.middlewares.use((request, response, next) => {
56
+ const locals = config?.dev?.locals?.({ nodeRequest: request });
57
+ Promise.resolve(nodeAdapter(handler, { logger })(request, response, locals)).catch((error) => next(error));
58
+ });
59
+ };
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Vite plugins: server-side build pipeline.
3
+ *
4
+ * After the client build completes, these plugins run a nested
5
+ * `vite build` in SSR mode to produce the server entrypoint.
6
+ *
7
+ * Includes:
8
+ * - Client asset filename collector (from the client writeBundle)
9
+ * - Server build trigger (closeBundle)
10
+ * - Virtual entrypoint codegen
11
+ * - Server-to-client asset mover
12
+ *
13
+ * @internal
14
+ */
15
+ import { type PluginOption } from 'vite';
16
+ import type { PluginSharedState } from './plugin-shared-state.js';
17
+ /**
18
+ * Tracks client bundle assets so the server build can reference them
19
+ * with their hashed filenames.
20
+ */
21
+ export declare function gracileCollectClientAssetsPlugin({ state, }: {
22
+ state: PluginSharedState;
23
+ }): PluginOption;
24
+ /**
25
+ * After the client build finishes (`closeBundle`), run a nested SSR
26
+ * build that produces the server entrypoint and moves assets back into
27
+ * the client output directory.
28
+ */
29
+ export declare function gracileServerBuildPlugin({ state, virtualRoutesForClient, }: {
30
+ state: PluginSharedState;
31
+ virtualRoutesForClient: PluginOption;
32
+ }): PluginOption;
33
+ //# sourceMappingURL=plugin-server-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-server-build.d.ts","sourceRoot":"","sources":["../../src/vite/plugin-server-build.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;AAGhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAIlE;;;GAGG;AACH,wBAAgB,gCAAgC,CAAC,EAChD,KAAK,GACL,EAAE;IACF,KAAK,EAAE,iBAAiB,CAAC;CACzB,GAAG,YAAY,CAYf;AAID;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,EACxC,KAAK,EACL,sBAAsB,GACtB,EAAE;IACF,KAAK,EAAE,iBAAiB,CAAC;IACzB,sBAAsB,EAAE,YAAY,CAAC;CACrC,GAAG,YAAY,CAqEf"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Vite plugins: server-side build pipeline.
3
+ *
4
+ * After the client build completes, these plugins run a nested
5
+ * `vite build` in SSR mode to produce the server entrypoint.
6
+ *
7
+ * Includes:
8
+ * - Client asset filename collector (from the client writeBundle)
9
+ * - Server build trigger (closeBundle)
10
+ * - Virtual entrypoint codegen
11
+ * - Server-to-client asset mover
12
+ *
13
+ * @internal
14
+ */
15
+ import { join } from 'node:path';
16
+ import { rename, rm } from 'node:fs/promises';
17
+ import { build } from 'vite';
18
+ import { virtualRoutes } from './virtual-routes.js';
19
+ // ── Client asset collector ───────────────────────────────────────────
20
+ /**
21
+ * Tracks client bundle assets so the server build can reference them
22
+ * with their hashed filenames.
23
+ */
24
+ export function gracileCollectClientAssetsPlugin({ state, }) {
25
+ return {
26
+ name: 'vite-plugin-gracile-collect-client-assets-for-server',
27
+ writeBundle(_, bundle) {
28
+ if (state.outputMode === 'static')
29
+ return;
30
+ for (const file of Object.values(bundle))
31
+ if (file.type === 'asset' && file.name)
32
+ state.clientAssets[file.name] = file.fileName;
33
+ },
34
+ };
35
+ }
36
+ // ── Server build trigger ─────────────────────────────────────────────
37
+ /**
38
+ * After the client build finishes (`closeBundle`), run a nested SSR
39
+ * build that produces the server entrypoint and moves assets back into
40
+ * the client output directory.
41
+ */
42
+ export function gracileServerBuildPlugin({ state, virtualRoutesForClient, }) {
43
+ return {
44
+ name: 'vite-plugin-gracile-server-build',
45
+ apply: 'build',
46
+ config(viteConfig) {
47
+ state.root = viteConfig.root || null;
48
+ },
49
+ async closeBundle() {
50
+ if (state.outputMode === 'static' ||
51
+ !state.routes ||
52
+ !state.renderedRoutes)
53
+ return;
54
+ const root = state.root || process.cwd();
55
+ await build({
56
+ root,
57
+ ssr: { external: ['@gracile/gracile'] },
58
+ build: {
59
+ target: 'esnext',
60
+ ssr: true,
61
+ copyPublicDir: false,
62
+ outDir: 'dist/server',
63
+ ssrEmitAssets: true,
64
+ cssMinify: true,
65
+ cssCodeSplit: true,
66
+ rollupOptions: {
67
+ input: 'entrypoint.js',
68
+ output: {
69
+ entryFileNames: '[name].js',
70
+ assetFileNames: (chunkInfo) => {
71
+ if (chunkInfo.name) {
72
+ const fileName = state.clientAssets[chunkInfo.name];
73
+ if (fileName)
74
+ return fileName;
75
+ // NOTE: When not imported at all from client
76
+ return `assets/${chunkInfo.name.replace(/\.(.*)$/, '')}-[hash].[ext]`;
77
+ }
78
+ return 'assets/[name]-[hash].[ext]';
79
+ },
80
+ chunkFileNames: 'chunk/[name].js',
81
+ },
82
+ },
83
+ },
84
+ plugins: [
85
+ virtualRoutesForClient,
86
+ virtualRoutes({
87
+ routes: state.routes,
88
+ renderedRoutes: state.renderedRoutes,
89
+ }),
90
+ gracileEntrypointPlugin(state),
91
+ gracileMoveServerAssetsPlugin(state),
92
+ ],
93
+ });
94
+ },
95
+ };
96
+ }
97
+ // ── Virtual entrypoint ───────────────────────────────────────────────
98
+ /**
99
+ * Generates the virtual `entrypoint.js` module for the server build.
100
+ * This is the server's main entry: it imports routes and creates the
101
+ * Gracile handler.
102
+ */
103
+ function gracileEntrypointPlugin(state) {
104
+ return {
105
+ name: 'vite-plugin-gracile-entry',
106
+ resolveId(id) {
107
+ if (id === 'entrypoint.js') {
108
+ return id;
109
+ }
110
+ return null;
111
+ },
112
+ load(id) {
113
+ if (id === 'entrypoint.js' && state.routes && state.renderedRoutes) {
114
+ return `
115
+ import { routeAssets, routeImports, routes } from 'gracile:routes';
116
+ import { createGracileHandler } from '@gracile/gracile/_internals/server-runtime';
117
+ import { createLogger } from '@gracile/gracile/_internals/logger';
118
+
119
+ createLogger();
120
+
121
+ export const handler = createGracileHandler({
122
+ root: process.cwd(),
123
+ routes,
124
+ routeImports,
125
+ routeAssets,
126
+ serverMode: true,
127
+ gracileConfig: ${JSON.stringify(state.gracileConfig, null, 2)}
128
+ });
129
+ `;
130
+ }
131
+ return null;
132
+ },
133
+ };
134
+ }
135
+ // ── Server asset mover ───────────────────────────────────────────────
136
+ /**
137
+ * After the server build writes its bundle, move any assets from
138
+ * `dist/server/assets/` into `dist/client/assets/` so the client
139
+ * can serve them.
140
+ */
141
+ function gracileMoveServerAssetsPlugin(state) {
142
+ return {
143
+ name: 'gracile-move-server-assets',
144
+ async writeBundle(_, bundle) {
145
+ const cwd = state.root || process.cwd();
146
+ await Promise.all(Object.entries(bundle).map(async ([file]) => {
147
+ if (file.startsWith('assets/') === false)
148
+ return;
149
+ await rename(join(cwd, `/dist/server/${file}`), join(cwd, `/dist/client/${file}`));
150
+ }));
151
+ // NOTE: Disabled for now, because it conflict with test's folder comparer
152
+ await rm(join(cwd, `/dist/server/assets`), {
153
+ recursive: true,
154
+ }).catch(() => null);
155
+ },
156
+ };
157
+ }