@octanejs/app-core 0.0.47 → 0.0.49
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/package.json +3 -3
- package/src/config-loader.js +26 -6
- package/src/server/node-http.js +89 -35
- package/src/server/router.js +171 -24
- package/src/server/server-route.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/app-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.49",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
"esbuild": "^0.28.1"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"octane": "0.1.51"
|
|
82
|
+
"octane": "^0.1.51 || ^0.2.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/node": "^24.13.3",
|
|
86
|
-
"octane": "0.
|
|
86
|
+
"octane": "0.2.4"
|
|
87
87
|
}
|
|
88
88
|
}
|
package/src/config-loader.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
-
import { createHash } from 'node:crypto';
|
|
17
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
18
18
|
import { builtinModules, createRequire } from 'node:module';
|
|
19
19
|
import { pathToFileURL } from 'node:url';
|
|
20
20
|
|
|
@@ -229,16 +229,36 @@ async function evaluateConfigModule(root, configPath, configuredCacheDir) {
|
|
|
229
229
|
const cacheDir = configuredCacheDir
|
|
230
230
|
? path.resolve(root, configuredCacheDir)
|
|
231
231
|
: path.join(root, 'node_modules/.cache/octane/config');
|
|
232
|
-
|
|
232
|
+
// A content-addressed path keeps concurrent evaluations from overwriting
|
|
233
|
+
// one another. A fresh import URL also reevaluates unchanged source when
|
|
234
|
+
// its top-level code reads a changing environment value.
|
|
235
|
+
const contentHash = createHash('sha256').update(output).digest('hex');
|
|
236
|
+
const outputPath = path.join(cacheDir, `octane.config-${contentHash}.mjs`);
|
|
233
237
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
234
|
-
if (!fs.existsSync(outputPath)
|
|
235
|
-
|
|
238
|
+
if (!fs.existsSync(outputPath)) {
|
|
239
|
+
// Publish only a complete file: another config load may import the same
|
|
240
|
+
// hash while this one writes it, including from a separate process.
|
|
241
|
+
const temporaryPath = path.join(cacheDir, `.octane.config-${contentHash}-${randomUUID()}.tmp`);
|
|
242
|
+
try {
|
|
243
|
+
fs.writeFileSync(temporaryPath, output, { flag: 'wx' });
|
|
244
|
+
try {
|
|
245
|
+
fs.renameSync(temporaryPath, outputPath);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
// Windows cannot replace an existing destination with rename. If a
|
|
248
|
+
// concurrent writer won this same-hash race, its complete output is
|
|
249
|
+
// already safe to import. Preserve other publication failures.
|
|
250
|
+
if (!fs.existsSync(outputPath) || fs.readFileSync(outputPath, 'utf8') !== output) {
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
256
|
+
}
|
|
236
257
|
}
|
|
237
|
-
const contentHash = createHash('sha256').update(output).digest('hex').slice(0, 16);
|
|
238
258
|
let configModule;
|
|
239
259
|
try {
|
|
240
260
|
configModule = await import(
|
|
241
|
-
/* @vite-ignore */ `${pathToFileURL(outputPath).href}?
|
|
261
|
+
/* @vite-ignore */ `${pathToFileURL(outputPath).href}?evaluation=${randomUUID()}`
|
|
242
262
|
);
|
|
243
263
|
} catch (error) {
|
|
244
264
|
attachDependencyMetadata(error, dependencies, missingDependencies);
|
package/src/server/node-http.js
CHANGED
|
@@ -330,6 +330,16 @@ const MIME_TYPES = {
|
|
|
330
330
|
* @returns {boolean} true when the request was handled as a static file
|
|
331
331
|
*/
|
|
332
332
|
export function serveStaticFile(req, res, staticDir) {
|
|
333
|
+
return serveStaticFileFromRoot(req, res, staticDir);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @param {import('node:http').IncomingMessage} req
|
|
338
|
+
* @param {import('node:http').ServerResponse} res
|
|
339
|
+
* @param {string} staticDir
|
|
340
|
+
* @param {string} [configuredRoot] Canonical root pinned by createNodeServer
|
|
341
|
+
*/
|
|
342
|
+
function serveStaticFileFromRoot(req, res, staticDir, configuredRoot) {
|
|
333
343
|
const method = (req.method || 'GET').toUpperCase();
|
|
334
344
|
if (method !== 'GET' && method !== 'HEAD') return false;
|
|
335
345
|
|
|
@@ -338,49 +348,79 @@ export function serveStaticFile(req, res, staticDir) {
|
|
|
338
348
|
const filePath = path.normalize(path.join(staticDir, pathname));
|
|
339
349
|
if (!filePath.startsWith(path.normalize(staticDir + path.sep))) return false;
|
|
340
350
|
|
|
351
|
+
/** @type {string} */
|
|
352
|
+
let resolvedFile;
|
|
341
353
|
/** @type {fs.Stats} */
|
|
342
354
|
let stat;
|
|
355
|
+
let fd = -1;
|
|
343
356
|
try {
|
|
344
|
-
|
|
357
|
+
const realRoot = configuredRoot ?? fs.realpathSync.native(staticDir);
|
|
358
|
+
resolvedFile = fs.realpathSync.native(filePath);
|
|
359
|
+
const rootPrefix = realRoot.endsWith(path.sep) ? realRoot : realRoot + path.sep;
|
|
360
|
+
if (!resolvedFile.startsWith(rootPrefix)) return false;
|
|
361
|
+
|
|
362
|
+
// Open the verified target before responding. Passing this descriptor to
|
|
363
|
+
// the stream prevents a later path swap from changing what is served.
|
|
364
|
+
// The built static tree must not be writable by an untrusted actor during
|
|
365
|
+
// serving: Node has no portable directory-relative open for ancestor swaps.
|
|
366
|
+
fd = fs.openSync(
|
|
367
|
+
resolvedFile,
|
|
368
|
+
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0),
|
|
369
|
+
);
|
|
370
|
+
stat = fs.fstatSync(fd);
|
|
371
|
+
if (!stat.isFile()) {
|
|
372
|
+
fs.closeSync(fd);
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
345
375
|
} catch {
|
|
376
|
+
if (fd !== -1) fs.closeSync(fd);
|
|
346
377
|
return false;
|
|
347
378
|
}
|
|
348
|
-
if (!stat.isFile()) return false;
|
|
349
379
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const gzip = shouldGzip(req, 200, headers, method !== 'HEAD');
|
|
360
|
-
if (gzip) {
|
|
361
|
-
headers.set('Content-Encoding', 'gzip');
|
|
362
|
-
headers.delete('Content-Length');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
res.statusCode = 200;
|
|
366
|
-
res.setHeader('Content-Type', /** @type {string} */ (headers.get('Content-Type')));
|
|
367
|
-
const contentLength = headers.get('Content-Length');
|
|
368
|
-
if (contentLength !== null) res.setHeader('Content-Length', contentLength);
|
|
369
|
-
res.setHeader('Cache-Control', /** @type {string} */ (headers.get('Cache-Control')));
|
|
370
|
-
const contentEncoding = headers.get('Content-Encoding');
|
|
371
|
-
if (contentEncoding !== null) res.setHeader('Content-Encoding', contentEncoding);
|
|
372
|
-
const vary = headers.get('Vary');
|
|
373
|
-
if (vary !== null) res.setHeader('Vary', vary);
|
|
374
|
-
if (method === 'HEAD') {
|
|
375
|
-
res.end();
|
|
376
|
-
} else if (gzip) {
|
|
377
|
-
pipeline(fs.createReadStream(filePath), createGzip(), res, (error) => {
|
|
378
|
-
if (error && !res.destroyed) res.destroy(error);
|
|
380
|
+
try {
|
|
381
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
382
|
+
const headers = new Headers({
|
|
383
|
+
'Content-Type': MIME_TYPES[ext] || 'application/octet-stream',
|
|
384
|
+
'Content-Length': String(stat.size),
|
|
385
|
+
'Cache-Control':
|
|
386
|
+
pathname.startsWith('/assets/') || pathname.startsWith('/static/')
|
|
387
|
+
? 'public, max-age=31536000, immutable'
|
|
388
|
+
: 'public, max-age=0, must-revalidate',
|
|
379
389
|
});
|
|
380
|
-
|
|
381
|
-
|
|
390
|
+
const gzip = shouldGzip(req, 200, headers, method !== 'HEAD');
|
|
391
|
+
if (gzip) {
|
|
392
|
+
headers.set('Content-Encoding', 'gzip');
|
|
393
|
+
headers.delete('Content-Length');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
res.statusCode = 200;
|
|
397
|
+
res.setHeader('Content-Type', /** @type {string} */ (headers.get('Content-Type')));
|
|
398
|
+
const contentLength = headers.get('Content-Length');
|
|
399
|
+
if (contentLength !== null) res.setHeader('Content-Length', contentLength);
|
|
400
|
+
res.setHeader('Cache-Control', /** @type {string} */ (headers.get('Cache-Control')));
|
|
401
|
+
const contentEncoding = headers.get('Content-Encoding');
|
|
402
|
+
if (contentEncoding !== null) res.setHeader('Content-Encoding', contentEncoding);
|
|
403
|
+
const vary = headers.get('Vary');
|
|
404
|
+
if (vary !== null) res.setHeader('Vary', vary);
|
|
405
|
+
if (method === 'HEAD') {
|
|
406
|
+
res.end();
|
|
407
|
+
} else {
|
|
408
|
+
const source = fs.createReadStream(resolvedFile, { fd, autoClose: true });
|
|
409
|
+
fd = -1; // The stream now owns and closes the descriptor.
|
|
410
|
+
if (gzip) {
|
|
411
|
+
pipeline(source, createGzip(), res, (error) => {
|
|
412
|
+
if (error && !res.destroyed) res.destroy(error);
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
pipeline(source, res, (error) => {
|
|
416
|
+
if (error && !res.destroyed) res.destroy(error);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return true;
|
|
421
|
+
} finally {
|
|
422
|
+
if (fd !== -1) fs.closeSync(fd);
|
|
382
423
|
}
|
|
383
|
-
return true;
|
|
384
424
|
}
|
|
385
425
|
|
|
386
426
|
/**
|
|
@@ -394,10 +434,24 @@ export function serveStaticFile(req, res, staticDir) {
|
|
|
394
434
|
*/
|
|
395
435
|
export function createNodeServer(handler, options = {}) {
|
|
396
436
|
const staticDir = options.staticDir;
|
|
437
|
+
/** @type {string | null} */
|
|
438
|
+
let configuredRoot = null;
|
|
439
|
+
try {
|
|
440
|
+
if (staticDir) configuredRoot = fs.realpathSync.native(staticDir);
|
|
441
|
+
} catch {
|
|
442
|
+
// A missing root must remain unavailable for this server's lifetime.
|
|
443
|
+
// Otherwise it could appear later as a symlink to a private directory.
|
|
444
|
+
}
|
|
397
445
|
|
|
398
446
|
const server = http.createServer((req, res) => {
|
|
399
447
|
(async () => {
|
|
400
|
-
if (
|
|
448
|
+
if (
|
|
449
|
+
staticDir &&
|
|
450
|
+
configuredRoot &&
|
|
451
|
+
serveStaticFileFromRoot(req, res, staticDir, configuredRoot)
|
|
452
|
+
) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
401
455
|
const response = await handler(nodeRequestToWebRequest(req));
|
|
402
456
|
await sendWebResponseForRequest(res, response, req);
|
|
403
457
|
})().catch((error) => {
|
package/src/server/router.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* @property {string | RegExp} pattern
|
|
18
18
|
* @property {string[]} paramNames
|
|
19
19
|
* @property {number} specificity - Higher = more specific (static > param > catch-all)
|
|
20
|
+
* @property {number} order - Specificity-desc index; breaks equal-spec ties
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -37,7 +38,7 @@ function compilePath(path) {
|
|
|
37
38
|
// Escape special regex characters except our param syntax
|
|
38
39
|
const regexString = path
|
|
39
40
|
.split('/')
|
|
40
|
-
.map((segment)
|
|
41
|
+
.map(function (segment) {
|
|
41
42
|
if (!segment) return '';
|
|
42
43
|
|
|
43
44
|
// Catch-all param: *slug
|
|
@@ -68,6 +69,53 @@ function compilePath(path) {
|
|
|
68
69
|
return { pattern, paramNames, specificity };
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Last static segment of a dynamic route pattern. Positions skip empty
|
|
74
|
+
* segments so they line up with a leading-slash pathname split.
|
|
75
|
+
* A static token after a catch-all is not at a fixed index (`(.+)` can
|
|
76
|
+
* consume several segments), so those patterns stay on the linear remainder.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} path
|
|
79
|
+
* @returns {{ pos: number, value: string } | null}
|
|
80
|
+
*/
|
|
81
|
+
function lastStaticSegment(path) {
|
|
82
|
+
const raw = path.split('/');
|
|
83
|
+
let pos = -1;
|
|
84
|
+
let value = '';
|
|
85
|
+
let index = 0;
|
|
86
|
+
let sawCatchAll = false;
|
|
87
|
+
for (let i = 0; i < raw.length; i++) {
|
|
88
|
+
const segment = raw[i];
|
|
89
|
+
if (!segment) continue;
|
|
90
|
+
const first = segment.charCodeAt(0);
|
|
91
|
+
if (first === 42 /* * */) {
|
|
92
|
+
sawCatchAll = true;
|
|
93
|
+
} else if (first !== 58 /* : */) {
|
|
94
|
+
if (sawCatchAll) return null;
|
|
95
|
+
pos = index;
|
|
96
|
+
value = segment;
|
|
97
|
+
}
|
|
98
|
+
index++;
|
|
99
|
+
}
|
|
100
|
+
if (pos === -1) return null;
|
|
101
|
+
return { pos, value };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Non-empty pathname segments, matching lastStaticSegment positions.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} pathname
|
|
108
|
+
* @returns {string[]}
|
|
109
|
+
*/
|
|
110
|
+
function pathnameSegments(pathname) {
|
|
111
|
+
const raw = pathname.split('/');
|
|
112
|
+
const parts = [];
|
|
113
|
+
for (let i = 0; i < raw.length; i++) {
|
|
114
|
+
if (raw[i]) parts.push(raw[i]);
|
|
115
|
+
}
|
|
116
|
+
return parts;
|
|
117
|
+
}
|
|
118
|
+
|
|
71
119
|
/**
|
|
72
120
|
* Escape special regex characters
|
|
73
121
|
* @param {string} str
|
|
@@ -77,6 +125,32 @@ function escapeRegex(str) {
|
|
|
77
125
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
78
126
|
}
|
|
79
127
|
|
|
128
|
+
/**
|
|
129
|
+
* @param {CompiledRoute} compiled
|
|
130
|
+
* @param {string} method
|
|
131
|
+
* @param {string} pathname
|
|
132
|
+
* @param {string | undefined} normalizedMethod
|
|
133
|
+
* @returns {RouteMatch | null}
|
|
134
|
+
*/
|
|
135
|
+
function matchCompiled(compiled, method, pathname, normalizedMethod) {
|
|
136
|
+
const route = compiled.route;
|
|
137
|
+
if (route.type === 'server') {
|
|
138
|
+
const methods = /** @type {ServerRoute} */ (route).methods;
|
|
139
|
+
if (normalizedMethod === undefined) normalizedMethod = method.toUpperCase();
|
|
140
|
+
if (!methods.includes(normalizedMethod)) return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const match = /** @type {RegExp} */ (compiled.pattern).exec(pathname);
|
|
144
|
+
if (match === null) return null;
|
|
145
|
+
const paramNames = compiled.paramNames;
|
|
146
|
+
/** @type {Record<string, string>} */
|
|
147
|
+
const params = {};
|
|
148
|
+
for (let p = 0; p < paramNames.length; p++) {
|
|
149
|
+
params[paramNames[p]] = decodeURIComponent(match[p + 1]);
|
|
150
|
+
}
|
|
151
|
+
return { route, params };
|
|
152
|
+
}
|
|
153
|
+
|
|
80
154
|
/**
|
|
81
155
|
* Create a router from a list of routes
|
|
82
156
|
* @param {Route[]} routes
|
|
@@ -84,13 +158,60 @@ function escapeRegex(str) {
|
|
|
84
158
|
*/
|
|
85
159
|
export function createRouter(routes) {
|
|
86
160
|
/** @type {CompiledRoute[]} */
|
|
87
|
-
const compiledRoutes =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
161
|
+
const compiledRoutes = [];
|
|
162
|
+
for (let i = 0; i < routes.length; i++) {
|
|
163
|
+
const route = routes[i];
|
|
164
|
+
const compiled = compilePath(route.path);
|
|
165
|
+
compiledRoutes.push({
|
|
166
|
+
route,
|
|
167
|
+
pattern: compiled.pattern,
|
|
168
|
+
paramNames: compiled.paramNames,
|
|
169
|
+
specificity: compiled.specificity,
|
|
170
|
+
order: i,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
91
173
|
|
|
92
174
|
// Sort by specificity (higher first) for correct matching order
|
|
93
|
-
compiledRoutes.sort((a, b)
|
|
175
|
+
compiledRoutes.sort(function (a, b) {
|
|
176
|
+
if (b.specificity !== a.specificity) return b.specificity - a.specificity;
|
|
177
|
+
return a.order - b.order;
|
|
178
|
+
});
|
|
179
|
+
for (let i = 0; i < compiledRoutes.length; i++) {
|
|
180
|
+
compiledRoutes[i].order = i;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Exact static paths are O(1). A matching static always out-scores a param
|
|
184
|
+
// or catch-all that could match the same pathname, so the map is consulted
|
|
185
|
+
// first. After a method miss, matching falls through to the dynamic index.
|
|
186
|
+
/** @type {Map<string, CompiledRoute[]>} */
|
|
187
|
+
const staticByPath = new Map();
|
|
188
|
+
/** @type {Array<Map<string, CompiledRoute[]> | undefined>} */
|
|
189
|
+
const dynamicByPos = [];
|
|
190
|
+
/** @type {CompiledRoute[]} */
|
|
191
|
+
const dynamicNoStatic = [];
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < compiledRoutes.length; i++) {
|
|
194
|
+
const compiled = compiledRoutes[i];
|
|
195
|
+
if (typeof compiled.pattern === 'string') {
|
|
196
|
+
const existing = staticByPath.get(compiled.pattern);
|
|
197
|
+
if (existing === undefined) staticByPath.set(compiled.pattern, [compiled]);
|
|
198
|
+
else existing.push(compiled);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const last = lastStaticSegment(compiled.route.path);
|
|
202
|
+
if (last === null) {
|
|
203
|
+
dynamicNoStatic.push(compiled);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
let bucketMap = dynamicByPos[last.pos];
|
|
207
|
+
if (bucketMap === undefined) {
|
|
208
|
+
bucketMap = new Map();
|
|
209
|
+
dynamicByPos[last.pos] = bucketMap;
|
|
210
|
+
}
|
|
211
|
+
const existing = bucketMap.get(last.value);
|
|
212
|
+
if (existing === undefined) bucketMap.set(last.value, [compiled]);
|
|
213
|
+
else existing.push(compiled);
|
|
214
|
+
}
|
|
94
215
|
|
|
95
216
|
return {
|
|
96
217
|
/**
|
|
@@ -99,31 +220,57 @@ export function createRouter(routes) {
|
|
|
99
220
|
* @param {string} pathname
|
|
100
221
|
* @returns {RouteMatch | null}
|
|
101
222
|
*/
|
|
102
|
-
match(method, pathname) {
|
|
223
|
+
match: function (method, pathname) {
|
|
103
224
|
let normalizedMethod;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
225
|
+
const staticMatches = staticByPath.get(pathname);
|
|
226
|
+
if (staticMatches !== undefined) {
|
|
227
|
+
for (let i = 0; i < staticMatches.length; i++) {
|
|
228
|
+
const route = staticMatches[i].route;
|
|
229
|
+
if (route.type === 'server') {
|
|
230
|
+
const methods = /** @type {ServerRoute} */ (route).methods;
|
|
231
|
+
if (normalizedMethod === undefined) normalizedMethod = method.toUpperCase();
|
|
232
|
+
if (!methods.includes(normalizedMethod)) continue;
|
|
111
233
|
}
|
|
234
|
+
return { route, params: {} };
|
|
112
235
|
}
|
|
236
|
+
}
|
|
113
237
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
238
|
+
const parts = pathnameSegments(pathname);
|
|
239
|
+
/** @type {CompiledRoute | null} */
|
|
240
|
+
let first = null;
|
|
241
|
+
/** @type {CompiledRoute[] | null} */
|
|
242
|
+
let extra = null;
|
|
243
|
+
for (let p = 0; p < parts.length; p++) {
|
|
244
|
+
const bucketMap = dynamicByPos[p];
|
|
245
|
+
if (bucketMap === undefined) continue;
|
|
246
|
+
const bucket = bucketMap.get(parts[p]);
|
|
247
|
+
if (bucket === undefined) continue;
|
|
248
|
+
for (let b = 0; b < bucket.length; b++) {
|
|
249
|
+
if (first === null) first = bucket[b];
|
|
250
|
+
else if (extra === null) extra = [bucket[b]];
|
|
251
|
+
else extra.push(bucket[b]);
|
|
117
252
|
}
|
|
253
|
+
}
|
|
254
|
+
for (let r = 0; r < dynamicNoStatic.length; r++) {
|
|
255
|
+
if (first === null) first = dynamicNoStatic[r];
|
|
256
|
+
else if (extra === null) extra = [dynamicNoStatic[r]];
|
|
257
|
+
else extra.push(dynamicNoStatic[r]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (first === null) return null;
|
|
261
|
+
if (extra === null) return matchCompiled(first, method, pathname, normalizedMethod);
|
|
118
262
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
263
|
+
const candidates = extra;
|
|
264
|
+
candidates.push(first);
|
|
265
|
+
candidates.sort(function (a, b) {
|
|
266
|
+
return a.order - b.order;
|
|
267
|
+
});
|
|
268
|
+
for (let c = 0; c < candidates.length; c++) {
|
|
269
|
+
const hit = matchCompiled(candidates[c], method, pathname, normalizedMethod);
|
|
270
|
+
if (hit !== null) return hit;
|
|
271
|
+
if (candidates[c].route.type === 'server' && normalizedMethod === undefined) {
|
|
272
|
+
normalizedMethod = method.toUpperCase();
|
|
125
273
|
}
|
|
126
|
-
return { route, params };
|
|
127
274
|
}
|
|
128
275
|
return null;
|
|
129
276
|
},
|
|
@@ -35,9 +35,9 @@ export async function handleServerRoute(route, context, globalMiddlewares) {
|
|
|
35
35
|
} catch (error) {
|
|
36
36
|
console.error('[octane] API route error:', error);
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
return new Response(JSON.stringify({ error:
|
|
38
|
+
// Thrown messages can contain credentials or internal details. The server
|
|
39
|
+
// log above retains the original error for diagnostics.
|
|
40
|
+
return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
|
|
41
41
|
status: 500,
|
|
42
42
|
headers: {
|
|
43
43
|
'Content-Type': 'application/json',
|