@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.
- package/README.md +2 -0
- package/dist/cli/adapters/cloudflare.d.ts +2 -2
- package/dist/cli/adapters/cloudflare.d.ts.map +1 -1
- package/dist/cli/adapters/cloudflare.js +83 -159
- package/dist/cli/adapters/cloudflare.js.map +1 -1
- package/dist/cli/adapters/deployment-mode.d.ts +10 -0
- package/dist/cli/adapters/deployment-mode.d.ts.map +1 -0
- package/dist/cli/adapters/deployment-mode.js +59 -0
- package/dist/cli/adapters/deployment-mode.js.map +1 -0
- package/dist/cli/adapters/netlify.d.ts +5 -4
- package/dist/cli/adapters/netlify.d.ts.map +1 -1
- package/dist/cli/adapters/netlify.js +26 -150
- package/dist/cli/adapters/netlify.js.map +1 -1
- package/dist/cli/adapters/vercel.d.ts +2 -3
- package/dist/cli/adapters/vercel.d.ts.map +1 -1
- package/dist/cli/adapters/vercel.js +36 -111
- package/dist/cli/adapters/vercel.js.map +1 -1
- package/dist/cli/adapters/web-bridge.d.ts +3 -0
- package/dist/cli/adapters/web-bridge.d.ts.map +1 -0
- package/dist/cli/adapters/web-bridge.js +110 -0
- package/dist/cli/adapters/web-bridge.js.map +1 -0
- package/dist/cli/checks/lighthouse.d.ts +7 -1
- package/dist/cli/checks/lighthouse.d.ts.map +1 -1
- package/dist/cli/checks/lighthouse.js +49 -44
- package/dist/cli/checks/lighthouse.js.map +1 -1
- package/dist/cli/checks/links.js.map +1 -1
- package/dist/cli/checks/performance.d.ts +3 -0
- package/dist/cli/checks/performance.d.ts.map +1 -1
- package/dist/cli/checks/performance.js +56 -15
- package/dist/cli/checks/performance.js.map +1 -1
- package/dist/cli/checks/quality.d.ts +35 -0
- package/dist/cli/checks/quality.d.ts.map +1 -0
- package/dist/cli/checks/quality.js +72 -0
- package/dist/cli/checks/quality.js.map +1 -0
- package/dist/cli/checks/seo.d.ts +9 -0
- package/dist/cli/checks/seo.d.ts.map +1 -0
- package/dist/cli/checks/seo.js +40 -0
- package/dist/cli/checks/seo.js.map +1 -0
- package/dist/cli/commands/adapt.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +2 -51
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +12 -0
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +4 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +1 -72
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/generate.d.ts +1 -4
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +4 -73
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/preview-paths.d.ts +3 -0
- package/dist/cli/commands/preview-paths.d.ts.map +1 -0
- package/dist/cli/commands/preview-paths.js +8 -0
- package/dist/cli/commands/preview-paths.js.map +1 -0
- package/dist/cli/commands/preview.d.ts +11 -0
- package/dist/cli/commands/preview.d.ts.map +1 -1
- package/dist/cli/commands/preview.js +124 -458
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/cli/config.d.ts +3 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +68 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/create/index.js.map +1 -1
- package/dist/cli/create/templates/spa/package.json.tpl +1 -1
- package/dist/cli/create/templates/ssg/package.json.tpl +1 -1
- package/dist/cli/create/templates/ssr/package.json.tpl +1 -1
- package/dist/plugin/build-ssg.d.ts.map +1 -1
- package/dist/plugin/build-ssg.js +81 -15
- package/dist/plugin/build-ssg.js.map +1 -1
- package/dist/plugin/build-ssr.d.ts.map +1 -1
- package/dist/plugin/build-ssr.js +3 -1
- package/dist/plugin/build-ssr.js.map +1 -1
- package/dist/plugin/content/scanner.d.ts.map +1 -1
- package/dist/plugin/content/scanner.js +2 -7
- package/dist/plugin/content/scanner.js.map +1 -1
- package/dist/plugin/dev-server.d.ts +8 -0
- package/dist/plugin/dev-server.d.ts.map +1 -1
- package/dist/plugin/dev-server.js.map +1 -1
- package/dist/plugin/dts-generator.d.ts.map +1 -1
- package/dist/plugin/dts-generator.js +4 -1
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/file-scanner.d.ts +4 -0
- package/dist/plugin/file-scanner.d.ts.map +1 -0
- package/dist/plugin/file-scanner.js +59 -0
- package/dist/plugin/file-scanner.js.map +1 -0
- package/dist/plugin/html-post-process.d.ts +1 -1
- package/dist/plugin/html-post-process.d.ts.map +1 -1
- package/dist/plugin/html-post-process.js +7 -5
- package/dist/plugin/html-post-process.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +26 -8
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/path-utils.js.map +1 -1
- package/dist/plugin/scanner.d.ts +2 -2
- package/dist/plugin/scanner.d.ts.map +1 -1
- package/dist/plugin/scanner.js +4 -10
- package/dist/plugin/scanner.js.map +1 -1
- package/dist/plugin/server-content.d.ts +3 -0
- package/dist/plugin/server-content.d.ts.map +1 -0
- package/dist/plugin/server-content.js +25 -0
- package/dist/plugin/server-content.js.map +1 -0
- package/dist/plugin/transforms/auto-import.d.ts.map +1 -1
- package/dist/plugin/transforms/auto-import.js +6 -5
- package/dist/plugin/transforms/auto-import.js.map +1 -1
- package/dist/plugin/transforms/head-inject.d.ts.map +1 -1
- package/dist/plugin/virtual/composables.js +1 -1
- package/dist/plugin/virtual/composables.js.map +1 -1
- package/dist/plugin/virtual/content-components.d.ts +3 -1
- package/dist/plugin/virtual/content-components.d.ts.map +1 -1
- package/dist/plugin/virtual/content-components.js +105 -22
- package/dist/plugin/virtual/content-components.js.map +1 -1
- package/dist/plugin/virtual/layouts.js +1 -1
- package/dist/plugin/virtual/layouts.js.map +1 -1
- package/dist/plugin/virtual/middleware.js +1 -1
- package/dist/plugin/virtual/middleware.js.map +1 -1
- package/dist/plugin/virtual/plugins.js +1 -1
- package/dist/plugin/virtual/plugins.js.map +1 -1
- package/dist/plugin/virtual/routes.js +1 -1
- package/dist/plugin/virtual/routes.js.map +1 -1
- package/dist/plugin/virtual/server-api.js +1 -1
- package/dist/plugin/virtual/server-api.js.map +1 -1
- package/dist/plugin/virtual/server-middleware.js +1 -1
- package/dist/plugin/virtual/server-middleware.js.map +1 -1
- package/dist/runtime/app-template.d.ts.map +1 -1
- package/dist/runtime/app-template.js +18 -8
- package/dist/runtime/app-template.js.map +1 -1
- package/dist/runtime/composables/index.d.ts +2 -0
- package/dist/runtime/composables/index.d.ts.map +1 -1
- package/dist/runtime/composables/index.js +1 -0
- package/dist/runtime/composables/index.js.map +1 -1
- package/dist/runtime/composables/render-content.d.ts +10 -0
- package/dist/runtime/composables/render-content.d.ts.map +1 -0
- package/dist/runtime/composables/render-content.js +21 -0
- package/dist/runtime/composables/render-content.js.map +1 -0
- package/dist/runtime/composables/use-active-headings.js.map +1 -1
- package/dist/runtime/composables/use-auth.d.ts.map +1 -1
- package/dist/runtime/composables/use-auth.js +14 -6
- package/dist/runtime/composables/use-auth.js.map +1 -1
- package/dist/runtime/composables/use-content-page.d.ts +2 -0
- package/dist/runtime/composables/use-content-page.d.ts.map +1 -1
- package/dist/runtime/composables/use-content-page.js +1 -1
- package/dist/runtime/composables/use-content-page.js.map +1 -1
- package/dist/runtime/composables/use-content.d.ts.map +1 -1
- package/dist/runtime/composables/use-content.js.map +1 -1
- package/dist/runtime/composables/use-cookie.js.map +1 -1
- package/dist/runtime/composables/use-runtime-config.js.map +1 -1
- package/dist/runtime/composables/use-session.js.map +1 -1
- package/dist/runtime/content/client.d.ts +2 -3
- package/dist/runtime/content/client.d.ts.map +1 -1
- package/dist/runtime/content/client.js +4 -5
- package/dist/runtime/content/client.js.map +1 -1
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +45 -15
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/dist/runtime/isr-handler.d.ts +4 -15
- package/dist/runtime/isr-handler.d.ts.map +1 -1
- package/dist/runtime/isr-handler.js +64 -45
- package/dist/runtime/isr-handler.js.map +1 -1
- package/dist/runtime/request-dispatcher.d.ts +31 -0
- package/dist/runtime/request-dispatcher.d.ts.map +1 -0
- package/dist/runtime/request-dispatcher.js +152 -0
- package/dist/runtime/request-dispatcher.js.map +1 -0
- package/dist/runtime/route-matching.d.ts +20 -0
- package/dist/runtime/route-matching.d.ts.map +1 -0
- package/dist/runtime/route-matching.js +45 -0
- package/dist/runtime/route-matching.js.map +1 -0
- package/dist/types/api.d.ts +3 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/config.d.ts +27 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/content.d.ts +2 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/page.d.ts +11 -7
- package/dist/types/page.d.ts.map +1 -1
- package/package.json +30 -16
- package/dist/cli/commands/preview-isr.d.ts +0 -66
- package/dist/cli/commands/preview-isr.d.ts.map +0 -1
- package/dist/cli/commands/preview-isr.js +0 -132
- package/dist/cli/commands/preview-isr.js.map +0 -1
|
@@ -1,477 +1,143 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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.
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|