satset-react 0.0.1-beta.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/dist/adapter/index.d.ts +4 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +8 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/adapter/node.d.ts +3 -0
- package/dist/adapter/node.d.ts.map +1 -0
- package/dist/adapter/node.js +244 -0
- package/dist/adapter/node.js.map +1 -0
- package/dist/adapter/types.d.ts +24 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/adapter/types.js +3 -0
- package/dist/adapter/types.js.map +1 -0
- package/dist/adapter/vercel.d.ts +3 -0
- package/dist/adapter/vercel.d.ts.map +1 -0
- package/dist/adapter/vercel.js +173 -0
- package/dist/adapter/vercel.js.map +1 -0
- package/dist/assets/favicon.d.ts +2 -0
- package/dist/assets/favicon.d.ts.map +1 -0
- package/dist/assets/favicon.js +67 -0
- package/dist/assets/favicon.js.map +1 -0
- package/dist/assets/index.d.ts +6 -0
- package/dist/assets/index.d.ts.map +1 -0
- package/dist/assets/index.js +14 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/assets/metadata.d.ts +23 -0
- package/dist/assets/metadata.d.ts.map +1 -0
- package/dist/assets/metadata.js +55 -0
- package/dist/assets/metadata.js.map +1 -0
- package/dist/assets/robots.d.ts +11 -0
- package/dist/assets/robots.d.ts.map +1 -0
- package/dist/assets/robots.js +70 -0
- package/dist/assets/robots.js.map +1 -0
- package/dist/assets/sitemap.d.ts +12 -0
- package/dist/assets/sitemap.d.ts.map +1 -0
- package/dist/assets/sitemap.js +71 -0
- package/dist/assets/sitemap.js.map +1 -0
- package/dist/cli/commands.d.ts +4 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +74 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +32 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/components/ErrorBoundary.d.ts +22 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +56 -0
- package/dist/components/ErrorBoundary.js.map +1 -0
- package/dist/components/Image.d.ts +15 -0
- package/dist/components/Image.d.ts.map +1 -0
- package/dist/components/Image.js +65 -0
- package/dist/components/Image.js.map +1 -0
- package/dist/components/Link.d.ts +14 -0
- package/dist/components/Link.d.ts.map +1 -0
- package/dist/components/Link.js +67 -0
- package/dist/components/Link.js.map +1 -0
- package/dist/components/Script.d.ts +10 -0
- package/dist/components/Script.d.ts.map +1 -0
- package/dist/components/Script.js +60 -0
- package/dist/components/Script.js.map +1 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +15 -0
- package/dist/components/index.js.map +1 -0
- package/dist/core/hydrate.d.ts +8 -0
- package/dist/core/hydrate.d.ts.map +1 -0
- package/dist/core/hydrate.js +73 -0
- package/dist/core/hydrate.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +19 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/ssr.d.ts +28 -0
- package/dist/core/ssr.d.ts.map +1 -0
- package/dist/core/ssr.js +91 -0
- package/dist/core/ssr.js.map +1 -0
- package/dist/core/types.d.ts +24 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/notFound.d.ts +2 -0
- package/dist/navigation/notFound.d.ts.map +1 -0
- package/dist/navigation/notFound.js +9 -0
- package/dist/navigation/notFound.js.map +1 -0
- package/dist/router/file-system.d.ts +19 -0
- package/dist/router/file-system.d.ts.map +1 -0
- package/dist/router/file-system.js +332 -0
- package/dist/router/file-system.js.map +1 -0
- package/dist/router/index.d.ts +4 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +12 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/router.d.ts +17 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/router.js +138 -0
- package/dist/router/router.js.map +1 -0
- package/dist/router/types.d.ts +11 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/router/types.js +3 -0
- package/dist/router/types.js.map +1 -0
- package/dist/server/build.d.ts +5 -0
- package/dist/server/build.d.ts.map +1 -0
- package/dist/server/build.js +449 -0
- package/dist/server/build.js.map +1 -0
- package/dist/server/bundler.d.ts +27 -0
- package/dist/server/bundler.d.ts.map +1 -0
- package/dist/server/bundler.js +213 -0
- package/dist/server/bundler.js.map +1 -0
- package/dist/server/dev.d.ts +11 -0
- package/dist/server/dev.d.ts.map +1 -0
- package/dist/server/dev.js +1052 -0
- package/dist/server/dev.js.map +1 -0
- package/dist/server/env.d.ts +10 -0
- package/dist/server/env.d.ts.map +1 -0
- package/dist/server/env.js +102 -0
- package/dist/server/env.js.map +1 -0
- package/dist/server/error-overlay.d.ts +11 -0
- package/dist/server/error-overlay.d.ts.map +1 -0
- package/dist/server/error-overlay.js +395 -0
- package/dist/server/error-overlay.js.map +1 -0
- package/dist/server/hmr.d.ts +8 -0
- package/dist/server/hmr.d.ts.map +1 -0
- package/dist/server/hmr.js +166 -0
- package/dist/server/hmr.js.map +1 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +10 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/response.d.ts +38 -0
- package/dist/server/response.d.ts.map +1 -0
- package/dist/server/response.js +142 -0
- package/dist/server/response.js.map +1 -0
- package/dist/server/types.d.ts +13 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +3 -0
- package/dist/server/types.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startDevServer = startDevServer;
|
|
7
|
+
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const file_system_1 = require("../router/file-system");
|
|
11
|
+
const hmr_1 = require("./hmr");
|
|
12
|
+
const env_1 = require("./env");
|
|
13
|
+
const error_overlay_1 = require("./error-overlay");
|
|
14
|
+
const bundler_1 = require("./bundler");
|
|
15
|
+
const response_1 = require("./response");
|
|
16
|
+
const util_1 = __importDefault(require("util"));
|
|
17
|
+
let buildCache = new Map();
|
|
18
|
+
async function startDevServer(config = {}) {
|
|
19
|
+
const { port = 3000, host = 'localhost', root = process.cwd(), publicDir = 'public', favicon, } = config;
|
|
20
|
+
// Favicon link - set during startup
|
|
21
|
+
let faviconHref = null;
|
|
22
|
+
// Load environment variables
|
|
23
|
+
const env = (0, env_1.loadEnv)(root, 'development');
|
|
24
|
+
console.log('🚀 Starting Satset dev server...');
|
|
25
|
+
// Debug: log current working root and contents to ensure correct project path
|
|
26
|
+
console.debug('[dev] starting with root:', root);
|
|
27
|
+
try {
|
|
28
|
+
const rootFiles = fs_1.default.readdirSync(root);
|
|
29
|
+
console.debug('[dev] root files:', rootFiles);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
const rootErrMsg = e?.message ?? String(e);
|
|
33
|
+
console.debug('[dev] could not read root dir', rootErrMsg);
|
|
34
|
+
}
|
|
35
|
+
// Scan routes
|
|
36
|
+
const { routes, apiRoutes } = (0, file_system_1.getRoutes)(root);
|
|
37
|
+
console.log(`📁 Found ${routes.length} pages and ${apiRoutes.length} API routes`);
|
|
38
|
+
// Generate sitemap and robots into public/ if not present
|
|
39
|
+
try {
|
|
40
|
+
const { generateAndSaveSitemap } = await import('../assets/sitemap.js');
|
|
41
|
+
const { generateAndSaveRobots } = await import('../assets/robots.js');
|
|
42
|
+
await generateAndSaveSitemap(root, routes);
|
|
43
|
+
await generateAndSaveRobots(root);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
// ignore if asset generators aren't available
|
|
47
|
+
}
|
|
48
|
+
// Ensure favicon if configured or generate default if missing
|
|
49
|
+
async function ensureFavicon(rootDir, publicDirName, faviconSetting) {
|
|
50
|
+
const publicPath = path_1.default.join(rootDir, publicDirName);
|
|
51
|
+
try {
|
|
52
|
+
if (!fs_1.default.existsSync(publicPath))
|
|
53
|
+
fs_1.default.mkdirSync(publicPath, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
catch (e) { }
|
|
56
|
+
// If a setting is provided, handle several types
|
|
57
|
+
if (faviconSetting) {
|
|
58
|
+
// github:username -> use generateFavicon helper
|
|
59
|
+
if (typeof faviconSetting === 'string' && faviconSetting.startsWith('github:')) {
|
|
60
|
+
const username = faviconSetting.split(':', 2)[1];
|
|
61
|
+
try {
|
|
62
|
+
const { generateFavicon } = await import('../assets/favicon.js');
|
|
63
|
+
await generateFavicon(rootDir, username);
|
|
64
|
+
faviconHref = '/favicon.png';
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.warn('Could not generate favicon from github username:', e);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// URL -> download into public
|
|
72
|
+
if (/^https?:\/\//i.test(faviconSetting)) {
|
|
73
|
+
try {
|
|
74
|
+
const url = faviconSetting;
|
|
75
|
+
const ext = path_1.default.extname(new URL(url).pathname) || '.png';
|
|
76
|
+
const destName = ext === '.ico' ? 'favicon.ico' : 'favicon.png';
|
|
77
|
+
const destPath = path_1.default.join(publicPath, destName);
|
|
78
|
+
await downloadToFile(url, destPath);
|
|
79
|
+
faviconHref = `/${destName}`;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
console.warn('Failed to download favicon from URL:', e);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Local path relative to project
|
|
87
|
+
try {
|
|
88
|
+
const srcPath = path_1.default.isAbsolute(faviconSetting) ? faviconSetting : path_1.default.join(rootDir, faviconSetting);
|
|
89
|
+
if (fs_1.default.existsSync(srcPath)) {
|
|
90
|
+
const ext = path_1.default.extname(srcPath) || '.png';
|
|
91
|
+
const destName = ext === '.ico' ? 'favicon.ico' : 'favicon.png';
|
|
92
|
+
const destPath = path_1.default.join(publicPath, destName);
|
|
93
|
+
fs_1.default.copyFileSync(srcPath, destPath);
|
|
94
|
+
faviconHref = `/${destName}`;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.warn('Failed to use local favicon path:', e);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// If still no favicon, try to generate a default favicon.png if it doesn't exist
|
|
103
|
+
const pngPath = path_1.default.join(publicPath, 'favicon.png');
|
|
104
|
+
const icoPath = path_1.default.join(publicPath, 'favicon.ico');
|
|
105
|
+
if (fs_1.default.existsSync(pngPath) || fs_1.default.existsSync(icoPath)) {
|
|
106
|
+
faviconHref = fs_1.default.existsSync(icoPath) ? '/favicon.ico' : '/favicon.png';
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const { generateFavicon } = await import('../assets/favicon.js');
|
|
111
|
+
await generateFavicon(rootDir);
|
|
112
|
+
faviconHref = '/favicon.png';
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
// ignore - favicon generation optional
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function downloadToFile(url, dest) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const proto = url.startsWith('https') ? require('https') : require('http');
|
|
121
|
+
const file = fs_1.default.createWriteStream(dest);
|
|
122
|
+
proto.get(url, (resp) => {
|
|
123
|
+
if (resp.statusCode !== 200)
|
|
124
|
+
return reject(new Error('Failed to download'));
|
|
125
|
+
resp.pipe(file);
|
|
126
|
+
file.on('finish', () => { file.close(); resolve(); });
|
|
127
|
+
}).on('error', (err) => {
|
|
128
|
+
try {
|
|
129
|
+
fs_1.default.unlinkSync(dest);
|
|
130
|
+
}
|
|
131
|
+
catch (e) { }
|
|
132
|
+
reject(err);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// Run now
|
|
137
|
+
await ensureFavicon(root, publicDir, favicon);
|
|
138
|
+
// Setup build output directory
|
|
139
|
+
const tempDir = path_1.default.join(root, '.satset', 'temp');
|
|
140
|
+
if (!fs_1.default.existsSync(tempDir)) {
|
|
141
|
+
fs_1.default.mkdirSync(tempDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
// Initial build
|
|
144
|
+
console.log('📦 Building client bundle...');
|
|
145
|
+
await buildClientBundle(root, tempDir, routes);
|
|
146
|
+
// Create HTTP server
|
|
147
|
+
const server = http_1.default.createServer(async (req, res) => {
|
|
148
|
+
const url = req.url || '/';
|
|
149
|
+
// Serve bundled JS files
|
|
150
|
+
if (url.startsWith('/_satset/')) {
|
|
151
|
+
const filePath = path_1.default.join(tempDir, url.replace('/_satset/', ''));
|
|
152
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
153
|
+
const ext = path_1.default.extname(filePath);
|
|
154
|
+
const contentType = getContentType(ext);
|
|
155
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
156
|
+
fs_1.default.createReadStream(filePath).pipe(res);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Serve static files from public/
|
|
161
|
+
if (url.startsWith('/public/') || url.startsWith('/assets/')) {
|
|
162
|
+
const filePath = path_1.default.join(root, publicDir, url.replace(/^\/(public|assets)\//, ''));
|
|
163
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
164
|
+
const ext = path_1.default.extname(filePath);
|
|
165
|
+
const contentType = getContentType(ext);
|
|
166
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
167
|
+
fs_1.default.createReadStream(filePath).pipe(res);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Serve direct public files at root URLs (e.g., /image.png -> public/image.png)
|
|
172
|
+
// This mirrors Vite's behavior where static files in `public/` are available at '/{file}'
|
|
173
|
+
try {
|
|
174
|
+
const reqPath = (url || '/').split('?')[0].split('#')[0];
|
|
175
|
+
const parsed = path_1.default.parse(reqPath);
|
|
176
|
+
if (parsed.ext) {
|
|
177
|
+
const publicFilePath = path_1.default.join(root, publicDir, reqPath.replace(/^\/+/, ''));
|
|
178
|
+
if (fs_1.default.existsSync(publicFilePath)) {
|
|
179
|
+
const contentType = getContentType(parsed.ext.toLowerCase());
|
|
180
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
181
|
+
fs_1.default.createReadStream(publicFilePath).pipe(res);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
// ignore file serving errors
|
|
188
|
+
}
|
|
189
|
+
// Handle error reporting endpoint
|
|
190
|
+
if (url === '/__satset_error' && req.method === 'POST') {
|
|
191
|
+
let body = '';
|
|
192
|
+
req.on('data', chunk => { body += chunk; });
|
|
193
|
+
req.on('end', () => {
|
|
194
|
+
try {
|
|
195
|
+
const errorInfo = JSON.parse(body);
|
|
196
|
+
console.error('🔴 Runtime Error:', errorInfo.message);
|
|
197
|
+
if (errorInfo.file && errorInfo.line) {
|
|
198
|
+
errorInfo.code = (0, error_overlay_1.extractCodeSnippet)(errorInfo.file, errorInfo.line);
|
|
199
|
+
}
|
|
200
|
+
const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
|
|
201
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
202
|
+
res.end(overlayHTML);
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
206
|
+
res.end('Bad Request');
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Simple Server Actions endpoint (POST JSON: { name: 'actionName', data: { ... } })
|
|
212
|
+
if (url === '/_satset/action' && req.method === 'POST') {
|
|
213
|
+
let body = '';
|
|
214
|
+
req.on('data', chunk => { body += chunk; });
|
|
215
|
+
req.on('end', async () => {
|
|
216
|
+
try {
|
|
217
|
+
const payload = JSON.parse(body || '{}');
|
|
218
|
+
const actionName = payload?.name;
|
|
219
|
+
const data = payload?.data || {};
|
|
220
|
+
if (!actionName) {
|
|
221
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
222
|
+
res.end(JSON.stringify({ error: 'Missing action name' }));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// try to find actions file
|
|
226
|
+
const candidates = ['src/app/actions.ts', 'src/app/actions.js', 'src/app/actions.tsx'];
|
|
227
|
+
let actionsPath = null;
|
|
228
|
+
for (const c of candidates) {
|
|
229
|
+
const p = path_1.default.join(root, c.replace(/^src\//, 'src/'));
|
|
230
|
+
if (fs_1.default.existsSync(p)) {
|
|
231
|
+
actionsPath = p;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!actionsPath) {
|
|
236
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
237
|
+
res.end(JSON.stringify({ error: 'No actions file found' }));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const baseName = path_1.default.basename(actionsPath).replace(/\.[^.]+$/, '');
|
|
241
|
+
const compiled = path_1.default.join(tempDir, baseName + '.actions.server.js');
|
|
242
|
+
try {
|
|
243
|
+
try {
|
|
244
|
+
await bundler_1.bundler.bundleServer({ entryPoint: actionsPath, outfile: compiled, minify: false, root, sourcemap: true });
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
const msg = String(err && err.message ? err.message : err);
|
|
248
|
+
if (/ENOSPC|not enough space|There is not enough space/i.test(msg)) {
|
|
249
|
+
console.warn('Low disk space detected while bundling actions. Attempting to clear temp and retry without sourcemaps.');
|
|
250
|
+
try {
|
|
251
|
+
if (fs_1.default.existsSync(tempDir)) {
|
|
252
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
fs_1.default.mkdirSync(tempDir, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
catch (cleanupErr) {
|
|
257
|
+
console.error('Failed to cleanup temp dir:', cleanupErr);
|
|
258
|
+
}
|
|
259
|
+
await bundler_1.bundler.bundleServer({ entryPoint: actionsPath, outfile: compiled, minify: false, root, sourcemap: false });
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
delete require.cache[require.resolve(compiled)];
|
|
267
|
+
}
|
|
268
|
+
catch (e) { }
|
|
269
|
+
const ActionsModule = require(compiled);
|
|
270
|
+
const fn = ActionsModule && (ActionsModule[actionName] || (ActionsModule.default && ActionsModule.default[actionName]));
|
|
271
|
+
if (!fn || typeof fn !== 'function') {
|
|
272
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
273
|
+
res.end(JSON.stringify({ error: `Action not found: ${actionName}` }));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// basic FormData-like helper
|
|
277
|
+
const formLike = { _map: data, get(k) { return this._map[k]; }, entries() { return Object.entries(this._map); } };
|
|
278
|
+
try {
|
|
279
|
+
global.__SATSET_ACTION_RES__ = res;
|
|
280
|
+
}
|
|
281
|
+
catch (e) { }
|
|
282
|
+
let result;
|
|
283
|
+
try {
|
|
284
|
+
result = await fn(formLike);
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
try {
|
|
288
|
+
delete global.__SATSET_ACTION_RES__;
|
|
289
|
+
}
|
|
290
|
+
catch (e) { }
|
|
291
|
+
}
|
|
292
|
+
// SatsetResponse handling
|
|
293
|
+
const respMod = require('./response');
|
|
294
|
+
if (result instanceof respMod.SatsetResponse) {
|
|
295
|
+
respMod.sendSatsetResponse(res, result);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
299
|
+
res.end(JSON.stringify({ result }));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
const errInfo = { message: (e && e.message) || String(e), stack: e && e.stack, file: actionsPath };
|
|
304
|
+
const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errInfo);
|
|
305
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
306
|
+
res.end(overlayHTML);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
312
|
+
res.end(JSON.stringify({ error: 'Invalid request' }));
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// Handle API routes (support dynamic patterns)
|
|
318
|
+
const apiMatch = (0, file_system_1.matchRoute)(url, apiRoutes);
|
|
319
|
+
if (apiMatch) {
|
|
320
|
+
await handleApiRoute(apiMatch.route, req, res, root, tempDir, apiMatch.params);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Handle page routes (SSR) with dynamic matching
|
|
324
|
+
const matched = (0, file_system_1.matchRoute)(url, routes);
|
|
325
|
+
if (matched) {
|
|
326
|
+
await handlePageRoute(matched.route, req, res, root, tempDir, matched.params, routes, publicDir);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// 404
|
|
330
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
331
|
+
res.end('<h1>404 - Page Not Found</h1>');
|
|
332
|
+
});
|
|
333
|
+
// Start HMR with bundler integration
|
|
334
|
+
const hmrServer = (0, hmr_1.startHMR)(server, root, async (changedFile) => {
|
|
335
|
+
console.log(`🔄 Rebuilding: ${changedFile}`);
|
|
336
|
+
await buildClientBundle(root, tempDir, routes);
|
|
337
|
+
});
|
|
338
|
+
const bindHost = host === true ? '0.0.0.0' : (host === false ? 'localhost' : host);
|
|
339
|
+
server.listen(port, bindHost, () => {
|
|
340
|
+
console.log(`✅ Server running at http://${bindHost === '0.0.0.0' ? 'localhost' : bindHost}:${port}`);
|
|
341
|
+
if (host === true) {
|
|
342
|
+
try {
|
|
343
|
+
const os = require('os');
|
|
344
|
+
const nets = os.networkInterfaces();
|
|
345
|
+
const addresses = [];
|
|
346
|
+
for (const name of Object.keys(nets)) {
|
|
347
|
+
for (const net of nets[name]) {
|
|
348
|
+
if (net.family === 'IPv4' && !net.internal)
|
|
349
|
+
addresses.push(net.address);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const a of addresses) {
|
|
353
|
+
console.log(`🔗 On your network: http://${a}:${port}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (e) { }
|
|
357
|
+
}
|
|
358
|
+
console.log(`🔥 HMR enabled`);
|
|
359
|
+
});
|
|
360
|
+
return { server, hmrServer };
|
|
361
|
+
}
|
|
362
|
+
async function buildClientBundle(root, outdir, routes) {
|
|
363
|
+
try {
|
|
364
|
+
// Create entry file that imports all pages
|
|
365
|
+
const importLines = [];
|
|
366
|
+
const routeDefLines = [];
|
|
367
|
+
let importIndex = 0;
|
|
368
|
+
for (const route of routes) {
|
|
369
|
+
if (!fs_1.default.existsSync(route.component)) {
|
|
370
|
+
// skip removed/missing components (can happen during HMR file moves)
|
|
371
|
+
console.warn('Skipping missing component while building client bundle:', route.component);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
// Only include client components ("use client") in the client bundle
|
|
375
|
+
let includeInClient = false;
|
|
376
|
+
try {
|
|
377
|
+
const content = fs_1.default.readFileSync(route.component, 'utf-8');
|
|
378
|
+
if (content.includes("'use client'") || content.includes('"use client"')) {
|
|
379
|
+
includeInClient = true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
includeInClient = false;
|
|
384
|
+
}
|
|
385
|
+
if (!includeInClient) {
|
|
386
|
+
// Skip server-only pages to avoid bundling Node-only imports (fs/path)
|
|
387
|
+
console.debug('Skipping server-only page for client bundle:', route.component);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const relativePath = path_1.default.relative(root, route.component).replace(/\\/g, '/');
|
|
391
|
+
importLines.push(`import Page${importIndex} from '../../${relativePath}';`);
|
|
392
|
+
routeDefLines.push(` { path: '${route.path}', component: Page${importIndex} },`);
|
|
393
|
+
importIndex++;
|
|
394
|
+
}
|
|
395
|
+
const entryContent = `
|
|
396
|
+
import React from 'react';
|
|
397
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
398
|
+
|
|
399
|
+
// Import all page components
|
|
400
|
+
${importLines.join('\n')}
|
|
401
|
+
|
|
402
|
+
// Route definitions
|
|
403
|
+
const routeDefs = [
|
|
404
|
+
${routeDefLines.join('\n')}
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
function matchPath(pathname) {
|
|
408
|
+
const pathSegments = pathname.split('/').filter(Boolean);
|
|
409
|
+
for (const r of routeDefs) {
|
|
410
|
+
const routeSegments = r.path.split('/').filter(Boolean);
|
|
411
|
+
|
|
412
|
+
// catch-all
|
|
413
|
+
if (r.path.includes('*')) {
|
|
414
|
+
const catchIndex = routeSegments.findIndex(s => s.startsWith('*'));
|
|
415
|
+
const paramName = routeSegments[catchIndex].slice(1);
|
|
416
|
+
const params = {};
|
|
417
|
+
params[paramName] = pathSegments.slice(catchIndex).join('/');
|
|
418
|
+
return { component: r.component, params };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (routeSegments.length !== pathSegments.length) continue;
|
|
422
|
+
|
|
423
|
+
let matched = true;
|
|
424
|
+
const params = {};
|
|
425
|
+
|
|
426
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
427
|
+
const rs = routeSegments[i];
|
|
428
|
+
const ps = pathSegments[i];
|
|
429
|
+
|
|
430
|
+
if (rs.startsWith(':')) {
|
|
431
|
+
params[rs.slice(1)] = ps;
|
|
432
|
+
} else if (rs !== ps) {
|
|
433
|
+
matched = false;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (matched) {
|
|
439
|
+
return { component: r.component, params };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// fallback to root route
|
|
444
|
+
return { component: routeDefs.find(r => r.path === '/')?.component, params: {} };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Hydrate based on current path
|
|
448
|
+
const currentPath = window.location.pathname;
|
|
449
|
+
const match = matchPath(currentPath);
|
|
450
|
+
// expose routes and params to Router
|
|
451
|
+
window.__SATSET_ROUTES__ = routeDefs.map(r => r.path);
|
|
452
|
+
window.__SATSET_PARAMS__ = match.params || {};
|
|
453
|
+
|
|
454
|
+
const PageComponent = match.component;
|
|
455
|
+
|
|
456
|
+
if (PageComponent) {
|
|
457
|
+
const root = document.getElementById('root');
|
|
458
|
+
if (root) {
|
|
459
|
+
hydrateRoot(root, React.createElement(PageComponent, match.params ? { params: match.params } : undefined));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
`;
|
|
463
|
+
const entryPath = path_1.default.join(outdir, '_entry.tsx');
|
|
464
|
+
fs_1.default.writeFileSync(entryPath, entryContent);
|
|
465
|
+
// Ensure a global CSS exists in outdir
|
|
466
|
+
const cssPath = path_1.default.join(outdir, 'globals.css');
|
|
467
|
+
try {
|
|
468
|
+
if (!fs_1.default.existsSync(cssPath)) {
|
|
469
|
+
// Try to copy project-specific styles if present
|
|
470
|
+
const projectCss = path_1.default.join(root, 'src', 'styles', 'global.css');
|
|
471
|
+
if (fs_1.default.existsSync(projectCss)) {
|
|
472
|
+
try {
|
|
473
|
+
console.debug('[dev] copying project global css from', projectCss, 'to', cssPath);
|
|
474
|
+
fs_1.default.copyFileSync(projectCss, cssPath);
|
|
475
|
+
console.debug('[dev] copied global css');
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
console.warn('[dev] failed to copy project css:', err);
|
|
479
|
+
fs_1.default.writeFileSync(cssPath, 'body{font-family:system-ui;}.container{max-width:900px;margin:0 auto;padding:24px}');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
fs_1.default.writeFileSync(cssPath, 'body{font-family:system-ui;}.container{max-width:900px;margin:0 auto;padding:24px}');
|
|
484
|
+
console.debug('[dev] wrote default globals.css to', cssPath);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
console.warn('[dev] error ensuring globals.css:', err);
|
|
490
|
+
}
|
|
491
|
+
// Bundle with esbuild
|
|
492
|
+
await bundler_1.bundler.bundle({
|
|
493
|
+
entryPoints: [entryPath],
|
|
494
|
+
outdir,
|
|
495
|
+
minify: false,
|
|
496
|
+
sourcemap: true,
|
|
497
|
+
watch: false,
|
|
498
|
+
root,
|
|
499
|
+
});
|
|
500
|
+
console.log('✅ Client bundle ready');
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
console.error('❌ Build error:', error.message);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function handleApiRoute(route, req, res, root, tempDir, params = {}) {
|
|
507
|
+
try {
|
|
508
|
+
// Compile API module to CJS before requiring to support TS/ESM sources
|
|
509
|
+
const baseName = path_1.default.basename(route.component).replace(/\.[^.]+$/, '');
|
|
510
|
+
const compiledApiPath = path_1.default.join(tempDir, baseName + '.api.server.js');
|
|
511
|
+
let ApiModule = null;
|
|
512
|
+
// Use the same safe bundler helper for API modules
|
|
513
|
+
async function safeBundleApi(entry, outfile) {
|
|
514
|
+
try {
|
|
515
|
+
await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: true });
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
const msg = String(err && err.message ? err.message : err);
|
|
519
|
+
if (/ENOSPC|not enough space|There is not enough space/i.test(msg)) {
|
|
520
|
+
try {
|
|
521
|
+
if (fs_1.default.existsSync(tempDir)) {
|
|
522
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
523
|
+
}
|
|
524
|
+
fs_1.default.mkdirSync(tempDir, { recursive: true });
|
|
525
|
+
}
|
|
526
|
+
catch (cleanupErr) {
|
|
527
|
+
console.error('Failed to cleanup temp dir:', cleanupErr);
|
|
528
|
+
}
|
|
529
|
+
await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: false });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
throw err;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
await safeBundleApi(route.component, compiledApiPath);
|
|
537
|
+
try {
|
|
538
|
+
delete require.cache[require.resolve(compiledApiPath)];
|
|
539
|
+
}
|
|
540
|
+
catch (e) { }
|
|
541
|
+
ApiModule = require(compiledApiPath);
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
console.error('API bundling failed for', route.component, err);
|
|
545
|
+
const errMsg = String(err && err.message ? err.message : err);
|
|
546
|
+
const errorInfo = {
|
|
547
|
+
message: /ENOSPC|not enough space|There is not enough space/i.test(errMsg) ? 'API bundling failed: not enough disk space. Free disk space or delete .satset/temp' : 'API bundling failed',
|
|
548
|
+
stack: err && err.stack ? err.stack : String(err),
|
|
549
|
+
file: route.component,
|
|
550
|
+
};
|
|
551
|
+
const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
|
|
552
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
553
|
+
res.end(overlayHTML);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
function resolveHandler(mod) {
|
|
557
|
+
if (!mod)
|
|
558
|
+
return null;
|
|
559
|
+
// default export function
|
|
560
|
+
if (typeof mod === 'function')
|
|
561
|
+
return { default: mod };
|
|
562
|
+
if (mod && typeof mod === 'object') {
|
|
563
|
+
if (typeof mod.default === 'function')
|
|
564
|
+
return { default: mod.default };
|
|
565
|
+
// method-named exports (GET, POST, etc.)
|
|
566
|
+
const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
|
|
567
|
+
const out = {};
|
|
568
|
+
for (const m of methods) {
|
|
569
|
+
if (typeof mod[m] === 'function')
|
|
570
|
+
out[m] = mod[m];
|
|
571
|
+
}
|
|
572
|
+
if (Object.keys(out).length)
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
const handlerObj = resolveHandler(ApiModule);
|
|
578
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
579
|
+
async function callHandlerWithNodeStyle(fn) {
|
|
580
|
+
// If function declares at least two args, assume Node-style (req, res)
|
|
581
|
+
if (fn.length >= 2) {
|
|
582
|
+
const maybe = await fn(req, res, params);
|
|
583
|
+
// If the handler returns a SatsetResponse, send it
|
|
584
|
+
if (response_1.SatsetResponse.isSatsetResponse(maybe)) {
|
|
585
|
+
(0, response_1.sendSatsetResponse)(res, maybe);
|
|
586
|
+
}
|
|
587
|
+
// assume the handler handled the Node res
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
async function callHandlerWithWebStyle(fn) {
|
|
593
|
+
// Build a lightweight Request-like object for route handlers
|
|
594
|
+
const webReq = await (0, response_1.buildSatsetRequest)(req);
|
|
595
|
+
const result = await fn(webReq, { params });
|
|
596
|
+
if (response_1.SatsetResponse.isSatsetResponse(result)) {
|
|
597
|
+
(0, response_1.sendSatsetResponse)(res, result);
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
// If result is a native Response-like object with json/text, try to handle
|
|
601
|
+
if (result && typeof result === 'object' && typeof result.status === 'number' && (result.headers || result.json)) {
|
|
602
|
+
// Try basic mapping
|
|
603
|
+
const status = result.status || 200;
|
|
604
|
+
// Normalize headers: support Headers instance, array of tuples, or plain object
|
|
605
|
+
let headers = {};
|
|
606
|
+
const rawHeaders = result.headers;
|
|
607
|
+
if (rawHeaders) {
|
|
608
|
+
if (typeof rawHeaders.get === 'function' && typeof rawHeaders.entries === 'function') {
|
|
609
|
+
headers = Object.fromEntries(rawHeaders.entries());
|
|
610
|
+
}
|
|
611
|
+
else if (Array.isArray(rawHeaders)) {
|
|
612
|
+
headers = Object.fromEntries(rawHeaders);
|
|
613
|
+
}
|
|
614
|
+
else if (typeof rawHeaders === 'object') {
|
|
615
|
+
headers = rawHeaders;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
let body = undefined;
|
|
619
|
+
try {
|
|
620
|
+
if (typeof result.json === 'function') {
|
|
621
|
+
body = await result.json();
|
|
622
|
+
res.writeHead(status, { 'Content-Type': 'application/json', ...headers });
|
|
623
|
+
res.end(JSON.stringify(body));
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
if (typeof result.text === 'function') {
|
|
627
|
+
body = await result.text();
|
|
628
|
+
res.writeHead(status, headers);
|
|
629
|
+
res.end(body);
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
catch (e) {
|
|
634
|
+
// fallthrough
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// If result is an object, return as JSON
|
|
638
|
+
if (result && typeof result === 'object') {
|
|
639
|
+
const sat = response_1.SatsetResponse.json(result);
|
|
640
|
+
(0, response_1.sendSatsetResponse)(res, sat);
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
// If result is string or number, send as text
|
|
644
|
+
if (result != null) {
|
|
645
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
646
|
+
res.end(String(result));
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
// undefined -> assume handler will manage res; if not, leave to caller
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
// Prefer method-named export for App-style handlers
|
|
653
|
+
if (handlerObj && typeof handlerObj[method] === 'function') {
|
|
654
|
+
const fn = handlerObj[method];
|
|
655
|
+
// Try node-style first
|
|
656
|
+
const nodeTook = await callHandlerWithNodeStyle(fn);
|
|
657
|
+
if (nodeTook)
|
|
658
|
+
return;
|
|
659
|
+
// Fallback to Web-style
|
|
660
|
+
const webTook = await callHandlerWithWebStyle(fn);
|
|
661
|
+
if (webTook)
|
|
662
|
+
return;
|
|
663
|
+
// no response produced
|
|
664
|
+
res.writeHead(204);
|
|
665
|
+
res.end();
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
// Default export fallback
|
|
669
|
+
if (handlerObj && typeof handlerObj.default === 'function') {
|
|
670
|
+
const fn = handlerObj.default;
|
|
671
|
+
const nodeTook = await callHandlerWithNodeStyle(fn);
|
|
672
|
+
if (nodeTook)
|
|
673
|
+
return;
|
|
674
|
+
const webTook = await callHandlerWithWebStyle(fn);
|
|
675
|
+
if (webTook)
|
|
676
|
+
return;
|
|
677
|
+
res.writeHead(204);
|
|
678
|
+
res.end();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
682
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
const errorInfo = {
|
|
686
|
+
message: error.message,
|
|
687
|
+
stack: error.stack,
|
|
688
|
+
file: route.component,
|
|
689
|
+
};
|
|
690
|
+
const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
|
|
691
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
692
|
+
res.end(overlayHTML);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async function handlePageRoute(route, req, res, root, tempDir, initialParams = {}, allRoutes = [], publicDirName = 'public') {
|
|
696
|
+
try {
|
|
697
|
+
// Load env
|
|
698
|
+
const env = (0, env_1.loadEnv)(root, 'development');
|
|
699
|
+
const envScript = (0, env_1.getPublicEnvScript)(env.publicVars);
|
|
700
|
+
// Server-side render the component
|
|
701
|
+
// Compile page module to a temporary server file so Node can require it (handles tsx/esm)
|
|
702
|
+
const baseName = path_1.default.basename(route.component).replace(/\.[^.]+$/, '');
|
|
703
|
+
const compiledServerPath = path_1.default.join(tempDir, baseName + '.server.js');
|
|
704
|
+
let PageModule = null;
|
|
705
|
+
// Helper: bundle server build with low-disk fallback
|
|
706
|
+
async function safeBundleServer(entry, outfile) {
|
|
707
|
+
try {
|
|
708
|
+
await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: true });
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
const msg = String(err && err.message ? err.message : err);
|
|
712
|
+
// detect low-disk / ENOSPC errors (esbuild reports "There is not enough space on the disk" in Windows)
|
|
713
|
+
if (/ENOSPC|not enough space|There is not enough space/i.test(msg)) {
|
|
714
|
+
console.warn('Low disk space detected while bundling. Attempting to clear temp and retry without sourcemaps.');
|
|
715
|
+
try {
|
|
716
|
+
// clear temp dir to free space
|
|
717
|
+
if (fs_1.default.existsSync(tempDir)) {
|
|
718
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
719
|
+
}
|
|
720
|
+
fs_1.default.mkdirSync(tempDir, { recursive: true });
|
|
721
|
+
}
|
|
722
|
+
catch (cleanupErr) {
|
|
723
|
+
console.error('Failed to cleanup temp dir:', cleanupErr);
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
await bundler_1.bundler.bundleServer({ entryPoint: entry, outfile, minify: false, root, sourcemap: false });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
catch (retryErr) {
|
|
730
|
+
// still failed
|
|
731
|
+
throw retryErr;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// Not a disk-space issue, rethrow
|
|
735
|
+
throw err;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
// Bundle the page for Node (CJS)
|
|
740
|
+
await safeBundleServer(route.component, compiledServerPath);
|
|
741
|
+
try {
|
|
742
|
+
delete require.cache[require.resolve(compiledServerPath)];
|
|
743
|
+
}
|
|
744
|
+
catch (e) {
|
|
745
|
+
// ignore
|
|
746
|
+
}
|
|
747
|
+
PageModule = require(compiledServerPath);
|
|
748
|
+
}
|
|
749
|
+
catch (err) {
|
|
750
|
+
// If bundling fails, show an overlay with the bundling error and do NOT require the original TSX (which would crash)
|
|
751
|
+
console.error('SSR bundling failed for', route.component, err);
|
|
752
|
+
let message = 'SSR bundling failed';
|
|
753
|
+
const errMsg = String(err && err.message ? err.message : err);
|
|
754
|
+
if (/ENOSPC|not enough space|There is not enough space/i.test(errMsg)) {
|
|
755
|
+
message = 'SSR bundling failed: not enough disk space. Try freeing disk space or deleting the .satset/temp folder.';
|
|
756
|
+
}
|
|
757
|
+
const errorInfo = {
|
|
758
|
+
message,
|
|
759
|
+
stack: err && err.stack ? err.stack : String(err),
|
|
760
|
+
file: route.component,
|
|
761
|
+
};
|
|
762
|
+
const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
|
|
763
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
764
|
+
res.end(overlayHTML);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
function resolveExportedComponent(mod) {
|
|
768
|
+
if (!mod)
|
|
769
|
+
return null;
|
|
770
|
+
if (typeof mod === 'function')
|
|
771
|
+
return mod;
|
|
772
|
+
if (mod && typeof mod === 'object') {
|
|
773
|
+
if (typeof mod.default === 'function')
|
|
774
|
+
return mod.default;
|
|
775
|
+
if (mod.default && typeof mod.default === 'object' && typeof mod.default.default === 'function')
|
|
776
|
+
return mod.default.default;
|
|
777
|
+
for (const key of Object.keys(mod)) {
|
|
778
|
+
if (typeof mod[key] === 'function')
|
|
779
|
+
return mod[key];
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
const PageComponent = resolveExportedComponent(PageModule);
|
|
785
|
+
console.log('SSR: used component from', compiledServerPath, 'PageModule keys:', Object.keys(PageModule || {}), 'Resolved component type:', typeof PageComponent);
|
|
786
|
+
console.log('SSR: PageModule keys:', Object.keys(PageModule || {}), 'PageComponent type:', typeof PageComponent);
|
|
787
|
+
// Simple SSR with optional global layout composition
|
|
788
|
+
const React = require('react');
|
|
789
|
+
const { renderToString } = require('react-dom/server');
|
|
790
|
+
let pageHTML = '<div>Loading...</div>';
|
|
791
|
+
// Helper to render a component (sync or async) to HTML string
|
|
792
|
+
async function renderComponentToHtml(Comp, props = {}) {
|
|
793
|
+
if (!Comp || typeof Comp !== 'function') {
|
|
794
|
+
throw new Error('Invalid component to render');
|
|
795
|
+
}
|
|
796
|
+
const isAsync = Comp.constructor && Comp.constructor.name === 'AsyncFunction';
|
|
797
|
+
if (isAsync) {
|
|
798
|
+
const maybeNode = Comp(props);
|
|
799
|
+
const resolved = await Promise.resolve(maybeNode);
|
|
800
|
+
// If the async component returned a React element, render it
|
|
801
|
+
return renderToString(resolved);
|
|
802
|
+
}
|
|
803
|
+
return renderToString(React.createElement(Comp, props));
|
|
804
|
+
}
|
|
805
|
+
let statusCode = 200;
|
|
806
|
+
try {
|
|
807
|
+
// Ensure we actually have a function/component to render
|
|
808
|
+
if (!PageComponent || typeof PageComponent !== 'function') {
|
|
809
|
+
console.error('SSR Error: resolved page component is not a function. PageModule:', util_1.default.inspect(PageModule, { depth: 2 }));
|
|
810
|
+
const diagHtml = `<!doctype html><html><body><h1>500 - Component export error</h1><pre>${util_1.default.format('PageModule keys: %o\nResolved: %o', Object.keys(PageModule || {}), PageComponent)}</pre></body></html>`;
|
|
811
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
812
|
+
res.end(diagHtml);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
// Try to find a global layout route (path '/layout') in the discovered routes
|
|
816
|
+
let LayoutComponent = null;
|
|
817
|
+
try {
|
|
818
|
+
const layoutRoute = allRoutes.find(r => r.path === '/layout');
|
|
819
|
+
if (layoutRoute && layoutRoute.component) {
|
|
820
|
+
const layoutBase = path_1.default.basename(layoutRoute.component).replace(/\.[^.]+$/, '');
|
|
821
|
+
const layoutCompiled = path_1.default.join(tempDir, layoutBase + '.layout.server.js');
|
|
822
|
+
await bundler_1.bundler.bundleServer({ entryPoint: layoutRoute.component, outfile: layoutCompiled, minify: false, root });
|
|
823
|
+
try {
|
|
824
|
+
delete require.cache[require.resolve(layoutCompiled)];
|
|
825
|
+
}
|
|
826
|
+
catch (e) { }
|
|
827
|
+
const LayoutModule = require(layoutCompiled);
|
|
828
|
+
// resolve exported component if present
|
|
829
|
+
function resolveExportedComp(mod) {
|
|
830
|
+
if (!mod)
|
|
831
|
+
return null;
|
|
832
|
+
if (typeof mod === 'function')
|
|
833
|
+
return mod;
|
|
834
|
+
if (mod && typeof mod === 'object') {
|
|
835
|
+
if (typeof mod.default === 'function')
|
|
836
|
+
return mod.default;
|
|
837
|
+
for (const key of Object.keys(mod)) {
|
|
838
|
+
if (typeof mod[key] === 'function')
|
|
839
|
+
return mod[key];
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
LayoutComponent = resolveExportedComp(LayoutModule);
|
|
845
|
+
if (!LayoutComponent) {
|
|
846
|
+
console.warn('Layout module found but no component exported:', layoutRoute.component);
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
console.log('Using layout module for SSR:', layoutRoute.component);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
catch (e) {
|
|
854
|
+
const msg = e?.message ?? String(e);
|
|
855
|
+
console.warn('Could not load layout module:', msg);
|
|
856
|
+
}
|
|
857
|
+
// Render page (possibly async)
|
|
858
|
+
if (LayoutComponent) {
|
|
859
|
+
// render page as child and pass to layout
|
|
860
|
+
const PageFragmentHtml = await renderComponentToHtml(PageComponent, { params: initialParams });
|
|
861
|
+
// We need to render the Layout with the page element as `children` prop.
|
|
862
|
+
// Create a small wrapper component that returns the layout with children
|
|
863
|
+
const Wrapper = (props) => React.createElement(LayoutComponent, null, React.createElement(PageComponent, { params: initialParams }));
|
|
864
|
+
pageHTML = await renderComponentToHtml(Wrapper);
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
pageHTML = await renderComponentToHtml(PageComponent, { params: initialParams });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch (err) {
|
|
871
|
+
console.error('SSR Error:', err);
|
|
872
|
+
// special handling: notFound() throws a sentinel that we can identify
|
|
873
|
+
if (err && err.__SATSET_NOT_FOUND) {
|
|
874
|
+
statusCode = 404;
|
|
875
|
+
// attempt to render a root-level /not-found route if present
|
|
876
|
+
try {
|
|
877
|
+
const nf = allRoutes.find(r => r.path === '/not-found' || r.path === '/404');
|
|
878
|
+
if (nf && nf.component) {
|
|
879
|
+
const nfBase = path_1.default.basename(nf.component).replace(/\.[^.]+$/, '');
|
|
880
|
+
const nfCompiled = path_1.default.join(tempDir, nfBase + '.notfound.server.js');
|
|
881
|
+
await bundler_1.bundler.bundleServer({ entryPoint: nf.component, outfile: nfCompiled, minify: false, root });
|
|
882
|
+
try {
|
|
883
|
+
delete require.cache[require.resolve(nfCompiled)];
|
|
884
|
+
}
|
|
885
|
+
catch (e) { }
|
|
886
|
+
const NfModule = require(nfCompiled);
|
|
887
|
+
const NfComp = (typeof NfModule === 'function') ? NfModule : (NfModule && typeof NfModule.default === 'function') ? NfModule.default : null;
|
|
888
|
+
if (NfComp) {
|
|
889
|
+
pageHTML = await renderComponentToHtml(NfComp, { params: initialParams });
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
pageHTML = '<h1>404 - Page Not Found</h1>';
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
pageHTML = '<h1>404 - Page Not Found</h1>';
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
catch (e) {
|
|
900
|
+
pageHTML = '<h1>404 - Page Not Found</h1>';
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
// general error: try to render /error route if present
|
|
905
|
+
statusCode = 500;
|
|
906
|
+
try {
|
|
907
|
+
const er = allRoutes.find(r => r.path === '/error');
|
|
908
|
+
if (er && er.component) {
|
|
909
|
+
const erBase = path_1.default.basename(er.component).replace(/\.[^.]+$/, '');
|
|
910
|
+
const erCompiled = path_1.default.join(tempDir, erBase + '.error.server.js');
|
|
911
|
+
await bundler_1.bundler.bundleServer({ entryPoint: er.component, outfile: erCompiled, minify: false, root });
|
|
912
|
+
try {
|
|
913
|
+
delete require.cache[require.resolve(erCompiled)];
|
|
914
|
+
}
|
|
915
|
+
catch (e) { }
|
|
916
|
+
const ErModule = require(erCompiled);
|
|
917
|
+
const ErComp = (typeof ErModule === 'function') ? ErModule : (ErModule && typeof ErModule.default === 'function') ? ErModule.default : null;
|
|
918
|
+
if (ErComp) {
|
|
919
|
+
// pass error details to the error component for display
|
|
920
|
+
pageHTML = await renderComponentToHtml(ErComp, { error: err, reset: () => { } });
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
throw err;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
throw err;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
catch (e) {
|
|
931
|
+
// If all else fails, produce the dev overlay
|
|
932
|
+
const errorInfo = {
|
|
933
|
+
message: err?.message || String(err),
|
|
934
|
+
stack: err?.stack,
|
|
935
|
+
file: route.component,
|
|
936
|
+
};
|
|
937
|
+
const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
|
|
938
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
939
|
+
res.end(overlayHTML);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// expose routes + initial params to client for Router
|
|
945
|
+
const routePaths = allRoutes.length ? allRoutes.map(r => r.path) : [];
|
|
946
|
+
// Check whether layout and CSS ended up in the rendered HTML for debugging
|
|
947
|
+
try {
|
|
948
|
+
const hasHeader = pageHTML.includes('<header');
|
|
949
|
+
const hasFooter = pageHTML.includes('<footer');
|
|
950
|
+
console.log(`SSR: rendered page includes header=${hasHeader} footer=${hasFooter}`);
|
|
951
|
+
const cssPath = path_1.default.join(tempDir, 'globals.css');
|
|
952
|
+
console.log('SSR: globals.css exists at', cssPath, fs_1.default.existsSync(cssPath));
|
|
953
|
+
}
|
|
954
|
+
catch (e) {
|
|
955
|
+
// ignore
|
|
956
|
+
}
|
|
957
|
+
// attempt to obtain metadata from the page module (static or via getMetadata)
|
|
958
|
+
let metaHtml = '';
|
|
959
|
+
try {
|
|
960
|
+
const { renderMetaTags } = await import('../assets/metadata.js');
|
|
961
|
+
// metadata may be exported as `metadata` or `getMetadata` function
|
|
962
|
+
let metaObj = null;
|
|
963
|
+
if (PageModule && PageModule.metadata) {
|
|
964
|
+
metaObj = PageModule.metadata;
|
|
965
|
+
}
|
|
966
|
+
else if (PageModule && typeof PageModule.getMetadata === 'function') {
|
|
967
|
+
try {
|
|
968
|
+
metaObj = await PageModule.getMetadata({ params: initialParams });
|
|
969
|
+
}
|
|
970
|
+
catch (e) {
|
|
971
|
+
metaObj = null;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
// The `<Head>` component was removed; rely on exported `metadata` or `getMetadata` from the page/layout module.
|
|
975
|
+
metaHtml = renderMetaTags(metaObj);
|
|
976
|
+
}
|
|
977
|
+
catch (e) {
|
|
978
|
+
// ignore
|
|
979
|
+
}
|
|
980
|
+
const initialParamsScript = `<script>window.__SATSET_ROUTES__ = ${JSON.stringify(routePaths)}; window.__SATSET_PARAMS__ = ${JSON.stringify(initialParams)};</script>`;
|
|
981
|
+
// Determine favicon at request time (in case it was created/changed)
|
|
982
|
+
let computedFavicon = null;
|
|
983
|
+
try {
|
|
984
|
+
const publicPath = path_1.default.join(root, publicDirName);
|
|
985
|
+
if (fs_1.default.existsSync(path_1.default.join(publicPath, 'favicon.ico')))
|
|
986
|
+
computedFavicon = '/favicon.ico';
|
|
987
|
+
else if (fs_1.default.existsSync(path_1.default.join(publicPath, 'favicon.png')))
|
|
988
|
+
computedFavicon = '/favicon.png';
|
|
989
|
+
}
|
|
990
|
+
catch (e) {
|
|
991
|
+
// ignore
|
|
992
|
+
}
|
|
993
|
+
const faviconLink = computedFavicon ? `<link rel="icon" href="${computedFavicon}" />` : '';
|
|
994
|
+
const html = `
|
|
995
|
+
<!DOCTYPE html>
|
|
996
|
+
<html lang="en">
|
|
997
|
+
<head>
|
|
998
|
+
<meta charset="UTF-8" />
|
|
999
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1000
|
+
${metaHtml}
|
|
1001
|
+
${faviconLink}
|
|
1002
|
+
<link rel="stylesheet" href="/_satset/globals.css" />
|
|
1003
|
+
</head>
|
|
1004
|
+
<body>
|
|
1005
|
+
<div id="root">${pageHTML}</div>
|
|
1006
|
+
<script>${envScript}</script>
|
|
1007
|
+
${initialParamsScript}
|
|
1008
|
+
<script type="module" src="/_satset/_entry.js"></script>
|
|
1009
|
+
<script src="/__hmr"></script>
|
|
1010
|
+
<!-- ... rest of scripts -->
|
|
1011
|
+
</body>
|
|
1012
|
+
</html>
|
|
1013
|
+
`;
|
|
1014
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1015
|
+
res.end(html);
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
const errorInfo = {
|
|
1019
|
+
message: error.message,
|
|
1020
|
+
stack: error.stack,
|
|
1021
|
+
file: route.component,
|
|
1022
|
+
};
|
|
1023
|
+
if (route.component && fs_1.default.existsSync(route.component)) {
|
|
1024
|
+
const stackLines = error.stack?.split('\n') || [];
|
|
1025
|
+
const lineMatch = stackLines[0]?.match(/:(\d+):(\d+)/);
|
|
1026
|
+
if (lineMatch) {
|
|
1027
|
+
const line = parseInt(lineMatch[1]);
|
|
1028
|
+
errorInfo.line = line;
|
|
1029
|
+
errorInfo.column = parseInt(lineMatch[2]);
|
|
1030
|
+
errorInfo.code = (0, error_overlay_1.extractCodeSnippet)(route.component, line);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const overlayHTML = (0, error_overlay_1.generateErrorOverlayHTML)(errorInfo);
|
|
1034
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
1035
|
+
res.end(overlayHTML);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
function getContentType(ext) {
|
|
1039
|
+
const types = {
|
|
1040
|
+
'.html': 'text/html',
|
|
1041
|
+
'.css': 'text/css',
|
|
1042
|
+
'.js': 'application/javascript',
|
|
1043
|
+
'.json': 'application/json',
|
|
1044
|
+
'.png': 'image/png',
|
|
1045
|
+
'.jpg': 'image/jpeg',
|
|
1046
|
+
'.svg': 'image/svg+xml',
|
|
1047
|
+
'.woff': 'font/woff',
|
|
1048
|
+
'.woff2': 'font/woff2',
|
|
1049
|
+
};
|
|
1050
|
+
return types[ext] || 'text/plain';
|
|
1051
|
+
}
|
|
1052
|
+
//# sourceMappingURL=dev.js.map
|