@jasonshimmy/vite-plugin-cer-app 0.28.2 → 0.29.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.
Files changed (184) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/adapters/cloudflare.d.ts +2 -2
  3. package/dist/cli/adapters/cloudflare.d.ts.map +1 -1
  4. package/dist/cli/adapters/cloudflare.js +83 -159
  5. package/dist/cli/adapters/cloudflare.js.map +1 -1
  6. package/dist/cli/adapters/deployment-mode.d.ts +10 -0
  7. package/dist/cli/adapters/deployment-mode.d.ts.map +1 -0
  8. package/dist/cli/adapters/deployment-mode.js +59 -0
  9. package/dist/cli/adapters/deployment-mode.js.map +1 -0
  10. package/dist/cli/adapters/netlify.d.ts +5 -4
  11. package/dist/cli/adapters/netlify.d.ts.map +1 -1
  12. package/dist/cli/adapters/netlify.js +26 -150
  13. package/dist/cli/adapters/netlify.js.map +1 -1
  14. package/dist/cli/adapters/vercel.d.ts +2 -3
  15. package/dist/cli/adapters/vercel.d.ts.map +1 -1
  16. package/dist/cli/adapters/vercel.js +36 -111
  17. package/dist/cli/adapters/vercel.js.map +1 -1
  18. package/dist/cli/adapters/web-bridge.d.ts +3 -0
  19. package/dist/cli/adapters/web-bridge.d.ts.map +1 -0
  20. package/dist/cli/adapters/web-bridge.js +110 -0
  21. package/dist/cli/adapters/web-bridge.js.map +1 -0
  22. package/dist/cli/checks/lighthouse.d.ts +7 -1
  23. package/dist/cli/checks/lighthouse.d.ts.map +1 -1
  24. package/dist/cli/checks/lighthouse.js +49 -44
  25. package/dist/cli/checks/lighthouse.js.map +1 -1
  26. package/dist/cli/checks/links.js.map +1 -1
  27. package/dist/cli/checks/performance.d.ts +3 -0
  28. package/dist/cli/checks/performance.d.ts.map +1 -1
  29. package/dist/cli/checks/performance.js +56 -15
  30. package/dist/cli/checks/performance.js.map +1 -1
  31. package/dist/cli/checks/quality.d.ts +35 -0
  32. package/dist/cli/checks/quality.d.ts.map +1 -0
  33. package/dist/cli/checks/quality.js +72 -0
  34. package/dist/cli/checks/quality.js.map +1 -0
  35. package/dist/cli/checks/seo.d.ts +9 -0
  36. package/dist/cli/checks/seo.d.ts.map +1 -0
  37. package/dist/cli/checks/seo.js +40 -0
  38. package/dist/cli/checks/seo.js.map +1 -0
  39. package/dist/cli/commands/adapt.js.map +1 -1
  40. package/dist/cli/commands/build.d.ts.map +1 -1
  41. package/dist/cli/commands/build.js +2 -51
  42. package/dist/cli/commands/build.js.map +1 -1
  43. package/dist/cli/commands/check.d.ts.map +1 -1
  44. package/dist/cli/commands/check.js +12 -0
  45. package/dist/cli/commands/check.js.map +1 -1
  46. package/dist/cli/commands/dev.d.ts +4 -0
  47. package/dist/cli/commands/dev.d.ts.map +1 -1
  48. package/dist/cli/commands/dev.js +1 -72
  49. package/dist/cli/commands/dev.js.map +1 -1
  50. package/dist/cli/commands/generate.d.ts +1 -4
  51. package/dist/cli/commands/generate.d.ts.map +1 -1
  52. package/dist/cli/commands/generate.js +4 -73
  53. package/dist/cli/commands/generate.js.map +1 -1
  54. package/dist/cli/commands/preview-paths.d.ts +3 -0
  55. package/dist/cli/commands/preview-paths.d.ts.map +1 -0
  56. package/dist/cli/commands/preview-paths.js +8 -0
  57. package/dist/cli/commands/preview-paths.js.map +1 -0
  58. package/dist/cli/commands/preview.d.ts +11 -0
  59. package/dist/cli/commands/preview.d.ts.map +1 -1
  60. package/dist/cli/commands/preview.js +124 -458
  61. package/dist/cli/commands/preview.js.map +1 -1
  62. package/dist/cli/config.d.ts +3 -0
  63. package/dist/cli/config.d.ts.map +1 -0
  64. package/dist/cli/config.js +68 -0
  65. package/dist/cli/config.js.map +1 -0
  66. package/dist/cli/create/index.js.map +1 -1
  67. package/dist/cli/create/templates/spa/package.json.tpl +1 -1
  68. package/dist/cli/create/templates/ssg/package.json.tpl +1 -1
  69. package/dist/cli/create/templates/ssr/package.json.tpl +1 -1
  70. package/dist/plugin/build-ssg.d.ts.map +1 -1
  71. package/dist/plugin/build-ssg.js +81 -15
  72. package/dist/plugin/build-ssg.js.map +1 -1
  73. package/dist/plugin/build-ssr.d.ts.map +1 -1
  74. package/dist/plugin/build-ssr.js +3 -1
  75. package/dist/plugin/build-ssr.js.map +1 -1
  76. package/dist/plugin/content/scanner.d.ts.map +1 -1
  77. package/dist/plugin/content/scanner.js +2 -7
  78. package/dist/plugin/content/scanner.js.map +1 -1
  79. package/dist/plugin/dev-server.d.ts +8 -0
  80. package/dist/plugin/dev-server.d.ts.map +1 -1
  81. package/dist/plugin/dev-server.js.map +1 -1
  82. package/dist/plugin/dts-generator.d.ts.map +1 -1
  83. package/dist/plugin/dts-generator.js +4 -1
  84. package/dist/plugin/dts-generator.js.map +1 -1
  85. package/dist/plugin/file-scanner.d.ts +4 -0
  86. package/dist/plugin/file-scanner.d.ts.map +1 -0
  87. package/dist/plugin/file-scanner.js +59 -0
  88. package/dist/plugin/file-scanner.js.map +1 -0
  89. package/dist/plugin/html-post-process.d.ts +1 -1
  90. package/dist/plugin/html-post-process.d.ts.map +1 -1
  91. package/dist/plugin/html-post-process.js +7 -5
  92. package/dist/plugin/html-post-process.js.map +1 -1
  93. package/dist/plugin/index.d.ts +1 -4
  94. package/dist/plugin/index.d.ts.map +1 -1
  95. package/dist/plugin/index.js +26 -8
  96. package/dist/plugin/index.js.map +1 -1
  97. package/dist/plugin/path-utils.js.map +1 -1
  98. package/dist/plugin/scanner.d.ts +2 -2
  99. package/dist/plugin/scanner.d.ts.map +1 -1
  100. package/dist/plugin/scanner.js +4 -10
  101. package/dist/plugin/scanner.js.map +1 -1
  102. package/dist/plugin/server-content.d.ts +3 -0
  103. package/dist/plugin/server-content.d.ts.map +1 -0
  104. package/dist/plugin/server-content.js +25 -0
  105. package/dist/plugin/server-content.js.map +1 -0
  106. package/dist/plugin/transforms/auto-import.d.ts.map +1 -1
  107. package/dist/plugin/transforms/auto-import.js +6 -5
  108. package/dist/plugin/transforms/auto-import.js.map +1 -1
  109. package/dist/plugin/transforms/head-inject.d.ts.map +1 -1
  110. package/dist/plugin/virtual/composables.js +1 -1
  111. package/dist/plugin/virtual/composables.js.map +1 -1
  112. package/dist/plugin/virtual/content-components.d.ts +3 -1
  113. package/dist/plugin/virtual/content-components.d.ts.map +1 -1
  114. package/dist/plugin/virtual/content-components.js +105 -22
  115. package/dist/plugin/virtual/content-components.js.map +1 -1
  116. package/dist/plugin/virtual/layouts.js +1 -1
  117. package/dist/plugin/virtual/layouts.js.map +1 -1
  118. package/dist/plugin/virtual/middleware.js +1 -1
  119. package/dist/plugin/virtual/middleware.js.map +1 -1
  120. package/dist/plugin/virtual/plugins.js +1 -1
  121. package/dist/plugin/virtual/plugins.js.map +1 -1
  122. package/dist/plugin/virtual/routes.js +1 -1
  123. package/dist/plugin/virtual/routes.js.map +1 -1
  124. package/dist/plugin/virtual/server-api.js +1 -1
  125. package/dist/plugin/virtual/server-api.js.map +1 -1
  126. package/dist/plugin/virtual/server-middleware.js +1 -1
  127. package/dist/plugin/virtual/server-middleware.js.map +1 -1
  128. package/dist/runtime/app-template.d.ts.map +1 -1
  129. package/dist/runtime/app-template.js +18 -8
  130. package/dist/runtime/app-template.js.map +1 -1
  131. package/dist/runtime/composables/index.d.ts +2 -0
  132. package/dist/runtime/composables/index.d.ts.map +1 -1
  133. package/dist/runtime/composables/index.js +1 -0
  134. package/dist/runtime/composables/index.js.map +1 -1
  135. package/dist/runtime/composables/render-content.d.ts +10 -0
  136. package/dist/runtime/composables/render-content.d.ts.map +1 -0
  137. package/dist/runtime/composables/render-content.js +21 -0
  138. package/dist/runtime/composables/render-content.js.map +1 -0
  139. package/dist/runtime/composables/use-active-headings.js.map +1 -1
  140. package/dist/runtime/composables/use-auth.d.ts.map +1 -1
  141. package/dist/runtime/composables/use-auth.js +14 -6
  142. package/dist/runtime/composables/use-auth.js.map +1 -1
  143. package/dist/runtime/composables/use-content-page.d.ts +2 -0
  144. package/dist/runtime/composables/use-content-page.d.ts.map +1 -1
  145. package/dist/runtime/composables/use-content-page.js +1 -1
  146. package/dist/runtime/composables/use-content-page.js.map +1 -1
  147. package/dist/runtime/composables/use-content.d.ts.map +1 -1
  148. package/dist/runtime/composables/use-content.js.map +1 -1
  149. package/dist/runtime/composables/use-cookie.js.map +1 -1
  150. package/dist/runtime/composables/use-runtime-config.js.map +1 -1
  151. package/dist/runtime/composables/use-session.js.map +1 -1
  152. package/dist/runtime/content/client.d.ts +2 -3
  153. package/dist/runtime/content/client.d.ts.map +1 -1
  154. package/dist/runtime/content/client.js +4 -5
  155. package/dist/runtime/content/client.js.map +1 -1
  156. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  157. package/dist/runtime/entry-server-template.js +45 -15
  158. package/dist/runtime/entry-server-template.js.map +1 -1
  159. package/dist/runtime/isr-handler.d.ts +4 -15
  160. package/dist/runtime/isr-handler.d.ts.map +1 -1
  161. package/dist/runtime/isr-handler.js +64 -45
  162. package/dist/runtime/isr-handler.js.map +1 -1
  163. package/dist/runtime/request-dispatcher.d.ts +31 -0
  164. package/dist/runtime/request-dispatcher.d.ts.map +1 -0
  165. package/dist/runtime/request-dispatcher.js +152 -0
  166. package/dist/runtime/request-dispatcher.js.map +1 -0
  167. package/dist/runtime/route-matching.d.ts +20 -0
  168. package/dist/runtime/route-matching.d.ts.map +1 -0
  169. package/dist/runtime/route-matching.js +45 -0
  170. package/dist/runtime/route-matching.js.map +1 -0
  171. package/dist/types/api.d.ts +3 -0
  172. package/dist/types/api.d.ts.map +1 -1
  173. package/dist/types/config.d.ts +27 -0
  174. package/dist/types/config.d.ts.map +1 -1
  175. package/dist/types/config.js.map +1 -1
  176. package/dist/types/content.d.ts +2 -0
  177. package/dist/types/content.d.ts.map +1 -1
  178. package/dist/types/page.d.ts +11 -7
  179. package/dist/types/page.d.ts.map +1 -1
  180. package/package.json +30 -16
  181. package/dist/cli/commands/preview-isr.d.ts +0 -66
  182. package/dist/cli/commands/preview-isr.d.ts.map +0 -1
  183. package/dist/cli/commands/preview-isr.js +0 -132
  184. package/dist/cli/commands/preview-isr.js.map +0 -1
@@ -1,477 +1,143 @@
1
1
  import { Command } from 'commander';
2
- import { createServer as createHttpServer } from 'node:http';
3
- import { createReadStream, existsSync, statSync } from 'node:fs';
4
- import { createGzip } from 'node:zlib';
5
- import { resolve, join, extname } from 'pathe';
2
+ import { preview } from 'vite';
3
+ import { existsSync, readFileSync, statSync } from 'node:fs';
4
+ import { resolve, join } from 'pathe';
6
5
  import { pathToFileURL } from 'node:url';
7
- import { isPathBounded, findRevalidate, findRenderMode, renderForIsr, serveFromIsrCache, } from './preview-isr.js';
8
- // ─── API route matching & body parsing ───────────────────────────────────────
9
- /**
10
- * Matches an API route pattern (e.g. '/api/items/:id') against a URL path.
11
- * Returns a params object on match, or null if the pattern does not match.
12
- */
13
- function matchApiPattern(pattern, urlPath) {
14
- const patternParts = pattern.split('/');
15
- const urlParts = urlPath.split('/');
16
- if (patternParts.length !== urlParts.length)
17
- return null;
18
- const params = {};
19
- for (let i = 0; i < patternParts.length; i++) {
20
- const p = patternParts[i];
21
- const u = urlParts[i];
22
- if (p.startsWith(':')) {
23
- params[p.slice(1)] = decodeURIComponent(u);
24
- }
25
- else if (p !== u) {
26
- return null;
27
- }
28
- }
29
- return params;
30
- }
31
- const MIME_TYPES = {
32
- '.html': 'text/html; charset=utf-8',
33
- '.js': 'application/javascript; charset=utf-8',
34
- '.mjs': 'application/javascript; charset=utf-8',
35
- '.css': 'text/css; charset=utf-8',
36
- '.json': 'application/json; charset=utf-8',
37
- '.png': 'image/png',
38
- '.jpg': 'image/jpeg',
39
- '.jpeg': 'image/jpeg',
40
- '.gif': 'image/gif',
41
- '.svg': 'image/svg+xml',
42
- '.ico': 'image/x-icon',
43
- '.woff': 'font/woff',
44
- '.woff2': 'font/woff2',
45
- '.ttf': 'font/ttf',
46
- '.eot': 'application/vnd.ms-fontobject',
47
- '.map': 'application/json',
48
- };
49
- /**
50
- * Reads the raw body from an IncomingMessage as a Buffer.
51
- */
52
- function readBody(req) {
53
- return new Promise((resolve, reject) => {
54
- const chunks = [];
55
- req.on('data', (chunk) => chunks.push(chunk));
56
- req.on('end', () => resolve(Buffer.concat(chunks)));
57
- req.on('error', reject);
58
- });
59
- }
60
- /**
61
- * Parses URL query string into a plain object.
62
- */
63
- function parseQuery(url) {
64
- const qIndex = url.indexOf('?');
65
- if (qIndex === -1)
66
- return {};
67
- const qs = url.slice(qIndex + 1);
68
- const result = {};
69
- for (const part of qs.split('&')) {
70
- if (!part)
71
- continue;
72
- const eqIdx = part.indexOf('=');
73
- if (eqIdx === -1) {
74
- result[decodeURIComponent(part)] = '';
75
- }
76
- else {
77
- result[decodeURIComponent(part.slice(0, eqIdx))] = decodeURIComponent(part.slice(eqIdx + 1));
78
- }
79
- }
80
- return result;
6
+ import { loadCerConfig } from '../config.js';
7
+ import { createRequestDispatcher, normalizeDocumentPath, isPrivateBuildPath } from '../../runtime/request-dispatcher.js';
8
+ import { isPathBounded } from './preview-paths.js';
9
+ const connections = new WeakMap();
10
+ export async function closePreview(server) {
11
+ for (const socket of connections.get(server) ?? [])
12
+ socket.destroy();
13
+ await new Promise((done) => server.httpServer.close(() => done()));
81
14
  }
82
- /**
83
- * Parses the request body for JSON content types.
84
- * Returns the parsed object for application/json, raw Buffer for other
85
- * bodies, or undefined for methods that carry no body.
86
- */
87
- async function parseBody(req) {
88
- const contentType = req.headers['content-type'] ?? '';
89
- const method = req.method?.toUpperCase() ?? 'GET';
90
- if (!['POST', 'PUT', 'PATCH'].includes(method))
91
- return undefined;
92
- const buf = await readBody(req);
93
- if (contentType.includes('application/json')) {
94
- try {
95
- return JSON.parse(buf.toString('utf-8'));
96
- }
97
- catch {
98
- return undefined;
99
- }
15
+ /** Vite owns transport, compression, media types and range requests. CER owns routing. */
16
+ export async function startPreview(options = {}) {
17
+ const root = resolve(options.root ?? process.cwd());
18
+ const distDir = join(root, 'dist');
19
+ if (!existsSync(distDir))
20
+ throw new Error(`No dist/ directory at ${distDir}. Run cer-app build first.`);
21
+ const manifestPath = join(distDir, 'ssg-manifest.json');
22
+ const ssg = existsSync(manifestPath);
23
+ const manifest = ssg ? JSON.parse(readFileSync(manifestPath, 'utf8')) : undefined;
24
+ const paths = new Set(manifest?.paths ?? []);
25
+ const bundle = join(distDir, 'server/server.js');
26
+ const useSSR = options.ssr || (existsSync(bundle) && (!ssg || manifest?.fallback || manifest?.hybrid));
27
+ let app;
28
+ if (useSSR) {
29
+ if (!existsSync(bundle))
30
+ throw new Error(`Missing server bundle ${bundle}`);
31
+ process.env.__CER_APP_ROOT__ = root;
32
+ app = await import(pathToFileURL(bundle).href);
33
+ if (typeof app?.handler !== 'function')
34
+ throw new Error('Server bundle does not export handler');
100
35
  }
101
- return buf;
102
- }
103
- function getMimeType(filePath) {
104
- const ext = extname(filePath).toLowerCase();
105
- return MIME_TYPES[ext] ?? 'application/octet-stream';
106
- }
107
- // MIME types that benefit from gzip compression. Binary formats (woff2, images)
108
- // are already compressed and should not be re-compressed.
109
- const GZIP_TYPES = new Set([
110
- 'text/html; charset=utf-8',
111
- 'application/javascript; charset=utf-8',
112
- 'text/css; charset=utf-8',
113
- 'application/json; charset=utf-8',
114
- 'image/svg+xml',
115
- 'application/json',
116
- ]);
117
- function acceptsGzip(req) {
118
- const ae = req.headers['accept-encoding'] ?? '';
119
- return ae.includes('gzip');
120
- }
121
- /**
122
- * Returns the appropriate Cache-Control header value for a file.
123
- * Vite content-hashes assets placed in the /assets/ directory, so they
124
- * can be cached indefinitely. Everything else (HTML, etc.) must not be cached.
125
- */
126
- function getCacheControl(filePath) {
127
- if (filePath.includes('/assets/'))
128
- return 'public, max-age=31536000, immutable';
129
- return 'no-cache';
130
- }
131
- /**
132
- * Sets standard security headers on every response.
133
- */
134
- function setSecurityHeaders(res) {
135
- res.setHeader('X-Content-Type-Options', 'nosniff');
136
- res.setHeader('X-Frame-Options', 'DENY');
137
- res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
138
- }
139
- /**
140
- * Serves a static file from the dist directory.
141
- * Applies gzip compression for compressible text types when the client
142
- * signals support via the Accept-Encoding request header.
143
- * Returns true if the file was served, false otherwise.
144
- */
145
- function serveStaticFile(req, res, distDir, fallbackStatus = 200) {
146
- const urlPath = (req.url ?? '/').split('?')[0];
147
- // Guard against path traversal: resolved path must stay within distDir.
148
- if (!isPathBounded(distDir, urlPath)) {
149
- res.statusCode = 400;
150
- res.end('Bad Request');
151
- return true;
152
- }
153
- // Try exact file path
154
- let filePath = join(distDir, urlPath);
155
- if (!existsSync(filePath) || statSync(filePath).isDirectory()) {
156
- // Try index.html in the directory
157
- const indexPath = join(distDir, urlPath, 'index.html');
158
- if (existsSync(indexPath)) {
159
- filePath = indexPath;
160
- }
161
- else if (existsSync(join(distDir, 'index.html'))) {
162
- // SPA fallback: serve root index.html
163
- filePath = join(distDir, 'index.html');
164
- res.statusCode = fallbackStatus;
165
- }
166
- else {
167
- return false;
168
- }
169
- }
170
- const mimeType = getMimeType(filePath);
171
- res.setHeader('Content-Type', mimeType);
172
- res.setHeader('Cache-Control', getCacheControl(filePath));
173
- setSecurityHeaders(res);
174
- const stream = createReadStream(filePath);
175
- if (GZIP_TYPES.has(mimeType) && acceptsGzip(req)) {
176
- res.setHeader('Content-Encoding', 'gzip');
177
- stream.pipe(createGzip()).pipe(res);
178
- }
179
- else {
180
- stream.pipe(res);
181
- }
182
- return true;
183
- }
184
- /**
185
- * Registers SIGTERM/SIGINT handlers that gracefully drain the server before exiting.
186
- * Waits up to 10 seconds for in-flight requests to complete, then force-exits.
187
- */
188
- function registerGracefulShutdown(server) {
189
- function shutdown(signal) {
190
- console.log(`[cer-app] Received ${signal}, shutting down gracefully...`);
191
- server.close(() => {
192
- console.log('[cer-app] Server closed.');
193
- process.exit(0);
194
- });
195
- // Force exit if connections haven't drained within 10 seconds.
196
- const t = setTimeout(() => {
197
- console.error('[cer-app] Forced shutdown after 10 s — connections did not drain.');
198
- process.exit(1);
199
- }, 10_000);
200
- // Allow the Node.js event loop to exit before the timeout fires if all other work is done.
201
- t.unref();
202
- }
203
- process.on('SIGTERM', () => shutdown('SIGTERM'));
204
- process.on('SIGINT', () => shutdown('SIGINT'));
205
- }
206
- export function previewCommand() {
207
- return new Command('preview')
208
- .description('Preview the production build')
209
- .option('-p, --port <port>', 'Port to listen on', '4173')
210
- .option('--host <host>', 'Host to bind to', 'localhost')
211
- .option('--root <root>', 'Project root directory', process.cwd())
212
- .option('--ssr', 'Serve using SSR handler from dist/server/server.js')
213
- .action(async (options) => {
214
- const root = resolve(options.root);
215
- const port = parseInt(options.port, 10);
216
- const distDir = join(root, 'dist');
217
- const serverBundle = join(distDir, 'server/server.js');
218
- // Check if SSR server bundle exists.
219
- // An SSG build also produces a server bundle, but previewing SSG means serving
220
- // the pre-rendered static HTML — detect the SSG manifest to avoid switching to
221
- // live SSR mode for SSG builds.
222
- const hasServerBundle = existsSync(serverBundle);
223
- const hasSsgManifest = existsSync(join(distDir, 'ssg-manifest.json'));
224
- const useSSR = options.ssr || (hasServerBundle && !hasSsgManifest);
225
- if (useSSR && hasServerBundle) {
226
- console.log('[cer-app] Starting SSR preview server...');
227
- // Load the server bundle
228
- let serverMod;
229
- try {
230
- // Expose the app root so the server bundle can resolve content files at runtime.
231
- process.env.__CER_APP_ROOT__ = root;
232
- serverMod = await import(pathToFileURL(serverBundle).href);
233
- }
234
- catch (err) {
235
- console.error('[cer-app] Failed to load server bundle:', err);
236
- process.exit(1);
237
- }
238
- const handler = serverMod.handler ?? serverMod.default;
239
- if (typeof handler !== 'function') {
240
- console.error('[cer-app] Server bundle does not export a handler function.');
241
- process.exit(1);
242
- }
243
- // Server middleware chain exported by the server bundle
244
- const runServerMiddleware = serverMod.runServerMiddleware;
245
- // Request context wrapper — runs fn inside _cerReqStore so useCookie / useSession work in API handlers
246
- const runWithRequestContext = serverMod.runWithRequestContext;
247
- // API route array exported by the server bundle: [{ path, handlers }]
248
- const apiRoutes = Array.isArray(serverMod.apiRoutes) ? serverMod.apiRoutes : [];
249
- // Page routes exported by the server bundle (used for ISR revalidate lookup).
250
- const pageRoutes = Array.isArray(serverMod.routes)
251
- ? serverMod.routes
252
- : [];
253
- // ISR cache: path → cached render entry.
254
- const isrCache = new Map();
255
- const server = createHttpServer(async (req, res) => {
256
- setSecurityHeaders(res);
257
- // Default: HTML and API responses must not be cached. Asset paths below
258
- // override this with getCacheControl() for content-hashed files.
259
- res.setHeader('Cache-Control', 'no-cache');
260
- const url = req.url ?? '/';
261
- const urlPath = url.split('?')[0];
262
- const method = req.method ?? 'GET';
263
- // Run server middleware before routing — stops chain if it returns false
264
- if (runServerMiddleware && !(await runServerMiddleware(req, res)))
265
- return;
266
- // Route /api/* requests to the server bundle's API handlers
267
- if (urlPath.startsWith('/api/')) {
268
- for (const route of apiRoutes) {
269
- const matched = matchApiPattern(route.path, urlPath);
270
- if (matched !== null) {
271
- const augReq = req;
272
- augReq.params = matched;
273
- augReq.query = parseQuery(url);
274
- augReq.body = await parseBody(req);
275
- const augRes = res;
276
- augRes.json = function (data) {
277
- this.setHeader('Content-Type', 'application/json; charset=utf-8');
278
- this.end(JSON.stringify(data));
279
- };
280
- augRes.status = function (code) { this.statusCode = code; return this; };
281
- const handlerFn = route.handlers[method.toLowerCase()] ??
282
- route.handlers[method.toUpperCase()] ??
283
- route.handlers['default'];
284
- if (typeof handlerFn === 'function') {
285
- try {
286
- const invoke = () => Promise.resolve(handlerFn(augReq, augRes));
287
- await (runWithRequestContext ? runWithRequestContext(req, res, invoke) : invoke());
288
- }
289
- catch (err) {
290
- console.error(`[cer-app] API handler error at ${route.path}:`, err);
291
- res.statusCode = 500;
292
- res.setHeader('Content-Type', 'application/json');
293
- res.end(JSON.stringify({ error: 'Internal Server Error' }));
294
- }
295
- return;
296
- }
297
- }
298
- }
299
- res.statusCode = 404;
300
- res.end('Not Found');
301
- return;
302
- }
303
- // Serve static assets from dist/client first
304
- const clientDist = join(distDir, 'client');
305
- if (existsSync(clientDist) && url !== '/' && url.includes('.')) {
306
- const served = serveStaticFile(req, res, clientDist);
307
- if (served)
308
- return;
36
+ const dispatch = app ? createRequestDispatcher(app, { prerendered: {
37
+ paths: [...paths],
38
+ fallback: ssg ? manifest?.fallback === true : undefined,
39
+ notFound: () => existsSync(join(distDir, '404.html')) ? readFileSync(join(distDir, '404.html'), 'utf8') : null,
40
+ read: (path) => readFileSync(join(distDir, path === '/' ? 'index.html' : path.slice(1) + '/index.html'), 'utf8'),
41
+ } }) : undefined;
42
+ const plugin = {
43
+ name: 'cer-preview-routing',
44
+ configurePreviewServer(server) {
45
+ server.middlewares.use((req, res, next) => {
46
+ for (const [name, value] of Object.entries({
47
+ 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY',
48
+ 'Referrer-Policy': 'strict-origin-when-cross-origin', ...options.preview?.headers,
49
+ }))
50
+ res.setHeader(name, value);
51
+ res.setHeader('Cache-Control', req.url?.startsWith('/assets/') ? 'public, max-age=31536000, immutable' : 'no-cache');
52
+ const raw = (req.url ?? '/').split('?')[0];
53
+ let path;
54
+ try {
55
+ path = normalizeDocumentPath(raw);
309
56
  }
310
- // Per-route render strategy — checked before ISR.
311
- const renderMode = findRenderMode(pageRoutes, urlPath);
312
- // render: 'spa' — skip SSR, serve the client index.html shell.
313
- if (renderMode === 'spa') {
314
- const spaIndex = join(distDir, 'client/index.html');
315
- if (existsSync(spaIndex)) {
316
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
317
- res.setHeader('Cache-Control', 'no-cache');
318
- createReadStream(spaIndex).pipe(res);
319
- }
320
- else {
321
- res.statusCode = 404;
322
- res.setHeader('Content-Type', 'text/plain');
323
- res.end('Not Found');
324
- }
57
+ catch {
58
+ res.statusCode = 400;
59
+ res.end('Bad Request');
325
60
  return;
326
61
  }
327
- // render: 'static' — try pre-rendered HTML from dist/, fall through to SSR.
328
- if (renderMode === 'static') {
329
- const staticFile = urlPath === '/'
330
- ? join(distDir, 'index.html')
331
- : join(distDir, urlPath.replace(/^\//, ''), 'index.html');
332
- if (existsSync(staticFile)) {
333
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
334
- res.setHeader('Cache-Control', 'no-cache');
335
- createReadStream(staticFile).pipe(res);
336
- return;
337
- }
338
- // Fall through to SSR if no pre-rendered file exists.
339
- }
340
- // render: 'server' — always SSR, bypass ISR cache.
341
- if (renderMode === 'server') {
342
- try {
343
- await handler(req, res);
344
- }
345
- catch (err) {
346
- console.error('[cer-app] SSR handler error:', err);
347
- res.statusCode = 500;
348
- res.setHeader('Content-Type', 'text/plain');
349
- res.end('Internal Server Error');
350
- }
62
+ if (!isPathBounded(distDir, path) || path.includes('\\')) {
63
+ res.statusCode = 400;
64
+ res.end('Bad Request');
351
65
  return;
352
66
  }
353
- // ISR: check whether this route has a revalidate TTL.
354
- const revalidate = findRevalidate(pageRoutes, urlPath);
355
- if (revalidate !== null) {
356
- const cached = isrCache.get(urlPath);
357
- const now = Date.now();
358
- if (cached) {
359
- const ageSeconds = (now - cached.builtAt) / 1000;
360
- if (ageSeconds < cached.revalidate) {
361
- // Fresh — serve from cache.
362
- serveFromIsrCache(cached, res, 'HIT');
363
- return;
364
- }
365
- // Stale — serve stale immediately, revalidate in background.
366
- if (!cached.revalidating) {
367
- cached.revalidating = true;
368
- serveFromIsrCache(cached, res, 'STALE');
369
- const revalidateTimeout = setTimeout(() => {
370
- if (cached)
371
- cached.revalidating = false;
372
- }, 30_000);
373
- renderForIsr(urlPath, handler, revalidate).then((entry) => {
374
- clearTimeout(revalidateTimeout);
375
- if (entry)
376
- isrCache.set(urlPath, entry);
377
- else if (cached)
378
- cached.revalidating = false;
379
- }).catch(() => {
380
- clearTimeout(revalidateTimeout);
381
- if (cached)
382
- cached.revalidating = false;
383
- });
384
- return;
385
- }
386
- // Already revalidating — serve stale without spawning another render.
387
- serveFromIsrCache(cached, res, 'STALE');
388
- return;
389
- }
390
- // Cache miss — render, cache, then serve.
391
- try {
392
- const entry = await renderForIsr(urlPath, handler, revalidate);
393
- if (entry) {
394
- isrCache.set(urlPath, entry);
395
- serveFromIsrCache(entry, res, 'HIT');
396
- }
397
- else {
398
- await handler(req, res);
399
- }
400
- }
401
- catch (err) {
402
- console.error('[cer-app] ISR render error:', err);
403
- res.statusCode = 500;
404
- res.end('Internal Server Error');
405
- }
67
+ const normalized = path.replace(/\/+$/, '') || '/';
68
+ if (isPrivateBuildPath(normalized)) {
69
+ res.statusCode = 404;
70
+ res.setHeader('Content-Type', 'text/plain');
71
+ res.end('Not found');
406
72
  return;
407
73
  }
408
- // Non-ISR: fall through to SSR handler directly.
409
- try {
410
- await handler(req, res);
411
- }
412
- catch (err) {
413
- console.error('[cer-app] SSR handler error:', err);
414
- res.statusCode = 500;
415
- res.end('Internal Server Error');
74
+ // Existing public files retain Vite's transport (including range requests).
75
+ // All other requests are documents or API routes, even when they contain dots.
76
+ const publicFile = join(distDir, app && !ssg ? 'client' : '', path);
77
+ if (dispatch && (normalized.startsWith('/api/') || paths.has(normalized) || !existsSync(publicFile) || !statSync(publicFile).isFile())) {
78
+ void Promise.resolve(dispatch(req, res)).catch(next);
79
+ return;
416
80
  }
81
+ if (ssg && paths.has(normalized))
82
+ req.url = normalized === '/' ? '/index.html' : normalized + '/index.html';
83
+ next();
417
84
  });
418
- // Protect against slow-send attacks: abort requests that stall during
419
- // header delivery or that take too long to complete.
420
- server.headersTimeout = 10_000; // 10 s to receive all request headers
421
- server.requestTimeout = 30_000; // 30 s for the full request/response cycle
422
- server.listen(port, options.host, () => {
423
- console.log(`[cer-app] SSR preview running at http://${options.host}:${port}`);
424
- });
425
- registerGracefulShutdown(server);
426
- }
427
- else {
428
- // Static file server (SPA / SSG)
429
- console.log('[cer-app] Starting static preview server...');
430
- if (!existsSync(distDir)) {
431
- console.error(`[cer-app] No dist/ directory found at ${distDir}. Run 'cer-app build' first.`);
432
- process.exit(1);
433
- }
434
- const server = createHttpServer((req, res) => {
435
- setSecurityHeaders(res);
436
- res.setHeader('Cache-Control', 'no-cache');
437
- const urlPath = (req.url ?? '/').split('?')[0];
438
- // SSG builds put assets in dist/client/ while HTML lives in dist/.
439
- // For requests with a non-HTML file extension, check dist/client/ first
440
- // so the static server finds the Vite-built JS/CSS bundles.
441
- const clientDist = join(distDir, 'client');
442
- const ext = extname(urlPath).toLowerCase();
443
- if (ext && ext !== '.html' && existsSync(clientDist)) {
444
- const assetPath = join(clientDist, urlPath);
445
- if (isPathBounded(clientDist, urlPath) &&
446
- existsSync(assetPath) && !statSync(assetPath).isDirectory()) {
447
- const mimeType = getMimeType(assetPath);
448
- res.setHeader('Content-Type', mimeType);
449
- res.setHeader('Cache-Control', getCacheControl(assetPath));
450
- const stream = createReadStream(assetPath);
451
- if (GZIP_TYPES.has(mimeType) && acceptsGzip(req)) {
452
- res.setHeader('Content-Encoding', 'gzip');
453
- stream.pipe(createGzip()).pipe(res);
454
- }
455
- else {
456
- stream.pipe(res);
457
- }
85
+ return () => {
86
+ server.middlewares.use((req, res, next) => {
87
+ if (!ssg || res.headersSent) {
88
+ next();
458
89
  return;
459
90
  }
460
- }
461
- const fallbackStatus = existsSync(join(distDir, 'ssg-manifest.json')) ? 404 : 200;
462
- const served = serveStaticFile(req, res, distDir, fallbackStatus);
463
- if (!served) {
464
91
  res.statusCode = 404;
465
- res.end('Not Found');
466
- }
467
- });
468
- server.headersTimeout = 10_000;
469
- server.requestTimeout = 30_000;
470
- server.listen(port, options.host, () => {
471
- console.log(`[cer-app] Static preview running at http://${options.host}:${port}`);
472
- });
473
- registerGracefulShutdown(server);
474
- }
92
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
93
+ const notFound = join(distDir, '404.html');
94
+ const html = existsSync(notFound) ? readFileSync(notFound) : '<!doctype html><meta name="robots" content="noindex"><title>Not found</title><h1>Not found</h1>';
95
+ res.end(req.method === 'HEAD' ? undefined : html);
96
+ });
97
+ };
98
+ },
99
+ };
100
+ const server = await preview({
101
+ root, configFile: false, appType: ssg || app ? 'mpa' : 'spa', plugins: [plugin],
102
+ build: { outDir: app && !ssg ? join(distDir, 'client') : distDir },
103
+ preview: {
104
+ ...options.preview, host: options.host ?? options.preview?.host ?? 'localhost',
105
+ // Vite's default CORS middleware adds Vary: Origin to every document,
106
+ // which correctly bypasses shared ISR. Enable CORS only when requested.
107
+ cors: options.preview?.cors ?? false,
108
+ port: options.port ?? options.preview?.port ?? 4173, strictPort: options.preview?.strictPort ?? true,
109
+ headers: {
110
+ 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY',
111
+ 'Referrer-Policy': 'strict-origin-when-cross-origin', ...options.preview?.headers,
112
+ },
113
+ },
114
+ });
115
+ const sockets = new Set();
116
+ connections.set(server, sockets);
117
+ server.httpServer.on('connection', (socket) => { sockets.add(socket); socket.once('close', () => sockets.delete(socket)); });
118
+ if ('headersTimeout' in server.httpServer)
119
+ server.httpServer.headersTimeout = 10_000;
120
+ if ('requestTimeout' in server.httpServer)
121
+ server.httpServer.requestTimeout = 30_000;
122
+ return server;
123
+ }
124
+ export function previewCommand() {
125
+ return new Command('preview')
126
+ .description('Preview the production build with Vite and CER routing')
127
+ .option('-p, --port <port>', 'Port to listen on')
128
+ .option('--host <host>', 'Host to bind to')
129
+ .option('--root <root>', 'Project root directory', process.cwd())
130
+ .option('--ssr', 'Serve using SSR handler from dist/server/server.js')
131
+ .action(async (options) => {
132
+ const config = await loadCerConfig(resolve(options.root));
133
+ const server = await startPreview({ ...options, port: options.port ? Number(options.port) : undefined, preview: config.preview });
134
+ server.printUrls();
135
+ const shutdown = async () => {
136
+ await closePreview(server);
137
+ process.exit(0);
138
+ };
139
+ process.once('SIGINT', shutdown);
140
+ process.once('SIGTERM', shutdown);
475
141
  });
476
142
  }
477
143
  //# sourceMappingURL=preview.js.map