@zenithbuild/cli 0.6.5 → 0.6.7
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/build.d.ts +32 -0
- package/dist/build.js +193 -548
- package/dist/compiler-bridge-runner.d.ts +5 -0
- package/dist/compiler-bridge-runner.js +70 -0
- package/dist/component-instance-ir.d.ts +6 -0
- package/dist/component-instance-ir.js +0 -20
- package/dist/component-occurrences.d.ts +6 -0
- package/dist/component-occurrences.js +6 -28
- package/dist/dev-server.d.ts +18 -0
- package/dist/dev-server.js +76 -116
- package/dist/dev-watch.d.ts +1 -0
- package/dist/dev-watch.js +19 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -28
- package/dist/manifest.d.ts +23 -0
- package/dist/manifest.js +22 -48
- package/dist/preview.d.ts +100 -0
- package/dist/preview.js +418 -488
- package/dist/resolve-components.d.ts +39 -0
- package/dist/resolve-components.js +30 -104
- package/dist/server/resolve-request-route.d.ts +39 -0
- package/dist/server/resolve-request-route.js +104 -113
- package/dist/server-contract.d.ts +39 -0
- package/dist/server-contract.js +15 -67
- package/dist/toolchain-paths.d.ts +23 -0
- package/dist/toolchain-paths.js +111 -39
- package/dist/toolchain-runner.d.ts +33 -0
- package/dist/toolchain-runner.js +170 -0
- package/dist/types/generate-env-dts.d.ts +5 -0
- package/dist/types/generate-env-dts.js +4 -2
- package/dist/types/generate-routes-dts.d.ts +8 -0
- package/dist/types/generate-routes-dts.js +7 -5
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.js +16 -7
- package/dist/ui/env.d.ts +18 -0
- package/dist/ui/env.js +0 -12
- package/dist/ui/format.d.ts +33 -0
- package/dist/ui/format.js +7 -45
- package/dist/ui/logger.d.ts +59 -0
- package/dist/ui/logger.js +3 -32
- package/dist/version-check.d.ts +54 -0
- package/dist/version-check.js +41 -98
- package/package.json +17 -5
package/dist/preview.js
CHANGED
|
@@ -8,32 +8,24 @@
|
|
|
8
8
|
// - Executes non-prerender <script server> blocks per request and injects
|
|
9
9
|
// serialized SSR payload via an inline script (`window.__zenith_ssr_data`).
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
11
|
import { spawn } from 'node:child_process';
|
|
13
12
|
import { createServer } from 'node:http';
|
|
14
13
|
import { access, readFile } from 'node:fs/promises';
|
|
15
14
|
import { extname, join, normalize, resolve, sep, dirname } from 'node:path';
|
|
16
15
|
import { fileURLToPath } from 'node:url';
|
|
17
16
|
import { createSilentLogger } from './ui/logger.js';
|
|
18
|
-
import {
|
|
19
|
-
compareRouteSpecificity,
|
|
20
|
-
matchRoute as matchManifestRoute,
|
|
21
|
-
resolveRequestRoute
|
|
22
|
-
} from './server/resolve-request-route.js';
|
|
23
|
-
|
|
17
|
+
import { compareRouteSpecificity, matchRoute as matchManifestRoute, resolveRequestRoute } from './server/resolve-request-route.js';
|
|
24
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
|
|
26
19
|
const MIME_TYPES = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
'.html': 'text/html',
|
|
21
|
+
'.js': 'application/javascript',
|
|
22
|
+
'.css': 'text/css',
|
|
23
|
+
'.json': 'application/json',
|
|
24
|
+
'.png': 'image/png',
|
|
25
|
+
'.jpg': 'image/jpeg',
|
|
26
|
+
'.svg': 'image/svg+xml'
|
|
34
27
|
};
|
|
35
|
-
|
|
36
|
-
const SERVER_SCRIPT_RUNNER = String.raw`
|
|
28
|
+
const SERVER_SCRIPT_RUNNER = String.raw `
|
|
37
29
|
import vm from 'node:vm';
|
|
38
30
|
import fs from 'node:fs/promises';
|
|
39
31
|
import path from 'node:path';
|
|
@@ -383,7 +375,6 @@ try {
|
|
|
383
375
|
);
|
|
384
376
|
}
|
|
385
377
|
`;
|
|
386
|
-
|
|
387
378
|
/**
|
|
388
379
|
* Create and start a preview server.
|
|
389
380
|
*
|
|
@@ -391,215 +382,196 @@ try {
|
|
|
391
382
|
* @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
|
|
392
383
|
*/
|
|
393
384
|
export async function createPreviewServer(options) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
return '127.0.0.1';
|
|
402
|
-
}
|
|
403
|
-
return host;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function serverOrigin() {
|
|
407
|
-
return `http://${publicHost()}:${actualPort}`;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const server = createServer(async (req, res) => {
|
|
411
|
-
const requestBase = typeof req.headers.host === 'string' && req.headers.host.length > 0
|
|
412
|
-
? `http://${req.headers.host}`
|
|
413
|
-
: serverOrigin();
|
|
414
|
-
const url = new URL(req.url, requestBase);
|
|
415
|
-
|
|
416
|
-
try {
|
|
417
|
-
if (url.pathname === '/__zenith/route-check') {
|
|
418
|
-
// Security: Require explicitly designated header to prevent public oracle probing
|
|
419
|
-
if (req.headers['x-zenith-route-check'] !== '1') {
|
|
420
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
421
|
-
res.end(JSON.stringify({ error: 'forbidden', message: 'invalid request context' }));
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const targetPath = String(url.searchParams.get('path') || '/');
|
|
426
|
-
|
|
427
|
-
// Security: Prevent protocol/domain injection in path
|
|
428
|
-
if (targetPath.includes('://') || targetPath.startsWith('//') || /[\r\n]/.test(targetPath)) {
|
|
429
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
430
|
-
res.end(JSON.stringify({ error: 'invalid_path_format' }));
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const targetUrl = new URL(targetPath, url.origin);
|
|
435
|
-
if (targetUrl.origin !== url.origin) {
|
|
436
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
437
|
-
res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const routes = await loadRouteManifest(distDir);
|
|
442
|
-
const resolvedCheck = resolveRequestRoute(targetUrl, routes);
|
|
443
|
-
if (!resolvedCheck.matched || !resolvedCheck.route) {
|
|
444
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
445
|
-
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const checkResult = await executeServerRoute({
|
|
450
|
-
source: resolvedCheck.route.server_script || '',
|
|
451
|
-
sourcePath: resolvedCheck.route.server_script_path || '',
|
|
452
|
-
params: resolvedCheck.params,
|
|
453
|
-
requestUrl: targetUrl.toString(),
|
|
454
|
-
requestMethod: req.method || 'GET',
|
|
455
|
-
requestHeaders: req.headers,
|
|
456
|
-
routePattern: resolvedCheck.route.path,
|
|
457
|
-
routeFile: resolvedCheck.route.server_script_path || '',
|
|
458
|
-
routeId: resolvedCheck.route.route_id || routeIdFromSourcePath(resolvedCheck.route.server_script_path || ''),
|
|
459
|
-
guardOnly: true
|
|
460
|
-
});
|
|
461
|
-
// Security: Enforce relative or same-origin redirects
|
|
462
|
-
if (checkResult && checkResult.result && checkResult.result.kind === 'redirect') {
|
|
463
|
-
const loc = String(checkResult.result.location || '/');
|
|
464
|
-
if (loc.includes('://') || loc.startsWith('//')) {
|
|
465
|
-
try {
|
|
466
|
-
const parsedLoc = new URL(loc);
|
|
467
|
-
if (parsedLoc.origin !== targetUrl.origin) {
|
|
468
|
-
checkResult.result.location = '/'; // Fallback to root for open redirect attempt
|
|
469
|
-
}
|
|
470
|
-
} catch {
|
|
471
|
-
checkResult.result.location = '/';
|
|
472
|
-
}
|
|
473
|
-
}
|
|
385
|
+
const { distDir, port = 4000, host = '127.0.0.1', logger: providedLogger = null } = options;
|
|
386
|
+
const logger = providedLogger || createSilentLogger();
|
|
387
|
+
const verboseLogging = logger.mode?.logLevel === 'verbose';
|
|
388
|
+
let actualPort = port;
|
|
389
|
+
function publicHost() {
|
|
390
|
+
if (host === '0.0.0.0' || host === '::') {
|
|
391
|
+
return '127.0.0.1';
|
|
474
392
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
routeId: resolvedCheck.route.route_id || '',
|
|
486
|
-
to: targetUrl.toString()
|
|
487
|
-
}));
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (extname(url.pathname) && extname(url.pathname) !== '.html') {
|
|
492
|
-
const staticPath = resolveWithinDist(distDir, url.pathname);
|
|
493
|
-
if (!staticPath || !(await fileExists(staticPath))) {
|
|
494
|
-
throw new Error('not found');
|
|
495
|
-
}
|
|
496
|
-
const content = await readFile(staticPath);
|
|
497
|
-
const mime = MIME_TYPES[extname(staticPath)] || 'application/octet-stream';
|
|
498
|
-
res.writeHead(200, { 'Content-Type': mime });
|
|
499
|
-
res.end(content);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const routes = await loadRouteManifest(distDir);
|
|
504
|
-
const resolved = resolveRequestRoute(url, routes);
|
|
505
|
-
let htmlPath = null;
|
|
506
|
-
|
|
507
|
-
if (resolved.matched && resolved.route) {
|
|
508
|
-
if (verboseLogging) {
|
|
509
|
-
logger.router(
|
|
510
|
-
`${req.method || 'GET'} ${url.pathname} -> ${resolved.route.path} params=${JSON.stringify(resolved.params)}`
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
const output = resolved.route.output.startsWith('/')
|
|
514
|
-
? resolved.route.output.slice(1)
|
|
515
|
-
: resolved.route.output;
|
|
516
|
-
htmlPath = resolveWithinDist(distDir, output);
|
|
517
|
-
} else {
|
|
518
|
-
htmlPath = toStaticFilePath(distDir, url.pathname);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (!htmlPath || !(await fileExists(htmlPath))) {
|
|
522
|
-
throw new Error('not found');
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
let ssrPayload = null;
|
|
526
|
-
if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
|
|
527
|
-
let routeExecution = null;
|
|
393
|
+
return host;
|
|
394
|
+
}
|
|
395
|
+
function serverOrigin() {
|
|
396
|
+
return `http://${publicHost()}:${actualPort}`;
|
|
397
|
+
}
|
|
398
|
+
const server = createServer(async (req, res) => {
|
|
399
|
+
const requestBase = typeof req.headers.host === 'string' && req.headers.host.length > 0
|
|
400
|
+
? `http://${req.headers.host}`
|
|
401
|
+
: serverOrigin();
|
|
402
|
+
const url = new URL(req.url, requestBase);
|
|
528
403
|
try {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
404
|
+
if (url.pathname === '/__zenith/route-check') {
|
|
405
|
+
// Security: Require explicitly designated header to prevent public oracle probing
|
|
406
|
+
if (req.headers['x-zenith-route-check'] !== '1') {
|
|
407
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
408
|
+
res.end(JSON.stringify({ error: 'forbidden', message: 'invalid request context' }));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const targetPath = String(url.searchParams.get('path') || '/');
|
|
412
|
+
// Security: Prevent protocol/domain injection in path
|
|
413
|
+
if (targetPath.includes('://') || targetPath.startsWith('//') || /[\r\n]/.test(targetPath)) {
|
|
414
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
415
|
+
res.end(JSON.stringify({ error: 'invalid_path_format' }));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const targetUrl = new URL(targetPath, url.origin);
|
|
419
|
+
if (targetUrl.origin !== url.origin) {
|
|
420
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
421
|
+
res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const routes = await loadRouteManifest(distDir);
|
|
425
|
+
const resolvedCheck = resolveRequestRoute(targetUrl, routes);
|
|
426
|
+
if (!resolvedCheck.matched || !resolvedCheck.route) {
|
|
427
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
428
|
+
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const checkResult = await executeServerRoute({
|
|
432
|
+
source: resolvedCheck.route.server_script || '',
|
|
433
|
+
sourcePath: resolvedCheck.route.server_script_path || '',
|
|
434
|
+
params: resolvedCheck.params,
|
|
435
|
+
requestUrl: targetUrl.toString(),
|
|
436
|
+
requestMethod: req.method || 'GET',
|
|
437
|
+
requestHeaders: req.headers,
|
|
438
|
+
routePattern: resolvedCheck.route.path,
|
|
439
|
+
routeFile: resolvedCheck.route.server_script_path || '',
|
|
440
|
+
routeId: resolvedCheck.route.route_id || routeIdFromSourcePath(resolvedCheck.route.server_script_path || ''),
|
|
441
|
+
guardOnly: true
|
|
442
|
+
});
|
|
443
|
+
// Security: Enforce relative or same-origin redirects
|
|
444
|
+
if (checkResult && checkResult.result && checkResult.result.kind === 'redirect') {
|
|
445
|
+
const loc = String(checkResult.result.location || '/');
|
|
446
|
+
if (loc.includes('://') || loc.startsWith('//')) {
|
|
447
|
+
try {
|
|
448
|
+
const parsedLoc = new URL(loc);
|
|
449
|
+
if (parsedLoc.origin !== targetUrl.origin) {
|
|
450
|
+
checkResult.result.location = '/'; // Fallback to root for open redirect attempt
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
checkResult.result.location = '/';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
res.writeHead(200, {
|
|
459
|
+
'Content-Type': 'application/json',
|
|
460
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
461
|
+
'Pragma': 'no-cache',
|
|
462
|
+
'Expires': '0',
|
|
463
|
+
'Vary': 'Cookie'
|
|
464
|
+
});
|
|
465
|
+
res.end(JSON.stringify({
|
|
466
|
+
result: checkResult?.result || checkResult,
|
|
467
|
+
routeId: resolvedCheck.route.route_id || '',
|
|
468
|
+
to: targetUrl.toString()
|
|
469
|
+
}));
|
|
470
|
+
return;
|
|
545
471
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
472
|
+
if (extname(url.pathname) && extname(url.pathname) !== '.html') {
|
|
473
|
+
const staticPath = resolveWithinDist(distDir, url.pathname);
|
|
474
|
+
if (!staticPath || !(await fileExists(staticPath))) {
|
|
475
|
+
throw new Error('not found');
|
|
476
|
+
}
|
|
477
|
+
const content = await readFile(staticPath);
|
|
478
|
+
const mime = MIME_TYPES[extname(staticPath)] || 'application/octet-stream';
|
|
479
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
480
|
+
res.end(content);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const routes = await loadRouteManifest(distDir);
|
|
484
|
+
const resolved = resolveRequestRoute(url, routes);
|
|
485
|
+
let htmlPath = null;
|
|
486
|
+
if (resolved.matched && resolved.route) {
|
|
487
|
+
if (verboseLogging) {
|
|
488
|
+
logger.router(`${req.method || 'GET'} ${url.pathname} -> ${resolved.route.path} params=${JSON.stringify(resolved.params)}`);
|
|
489
|
+
}
|
|
490
|
+
const output = resolved.route.output.startsWith('/')
|
|
491
|
+
? resolved.route.output.slice(1)
|
|
492
|
+
: resolved.route.output;
|
|
493
|
+
htmlPath = resolveWithinDist(distDir, output);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
htmlPath = toStaticFilePath(distDir, url.pathname);
|
|
497
|
+
}
|
|
498
|
+
if (!htmlPath || !(await fileExists(htmlPath))) {
|
|
499
|
+
throw new Error('not found');
|
|
500
|
+
}
|
|
501
|
+
let ssrPayload = null;
|
|
502
|
+
if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
|
|
503
|
+
let routeExecution = null;
|
|
504
|
+
try {
|
|
505
|
+
routeExecution = await executeServerRoute({
|
|
506
|
+
source: resolved.route.server_script,
|
|
507
|
+
sourcePath: resolved.route.server_script_path || '',
|
|
508
|
+
params: resolved.params,
|
|
509
|
+
requestUrl: url.toString(),
|
|
510
|
+
requestMethod: req.method || 'GET',
|
|
511
|
+
requestHeaders: req.headers,
|
|
512
|
+
routePattern: resolved.route.path,
|
|
513
|
+
routeFile: resolved.route.server_script_path || '',
|
|
514
|
+
routeId: resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '')
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
ssrPayload = {
|
|
519
|
+
__zenith_error: {
|
|
520
|
+
code: 'LOAD_FAILED',
|
|
521
|
+
message: error instanceof Error ? error.message : String(error)
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const trace = routeExecution?.trace || { guard: 'none', load: 'none' };
|
|
526
|
+
const routeId = resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '');
|
|
527
|
+
if (verboseLogging) {
|
|
528
|
+
logger.router(`${routeId} guard=${trace.guard} load=${trace.load}`);
|
|
529
|
+
}
|
|
530
|
+
const result = routeExecution?.result;
|
|
531
|
+
if (result && result.kind === 'redirect') {
|
|
532
|
+
const status = Number.isInteger(result.status) ? result.status : 302;
|
|
533
|
+
res.writeHead(status, {
|
|
534
|
+
Location: result.location,
|
|
535
|
+
'Cache-Control': 'no-store'
|
|
536
|
+
});
|
|
537
|
+
res.end('');
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (result && result.kind === 'deny') {
|
|
541
|
+
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
542
|
+
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
543
|
+
res.end(result.message || (status === 401 ? 'Unauthorized' : 'Forbidden'));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
547
|
+
ssrPayload = result.data;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
let html = await readFile(htmlPath, 'utf8');
|
|
551
|
+
if (ssrPayload) {
|
|
552
|
+
html = injectSsrPayload(html, ssrPayload);
|
|
553
|
+
}
|
|
554
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
555
|
+
res.end(html);
|
|
573
556
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (ssrPayload) {
|
|
578
|
-
html = injectSsrPayload(html, ssrPayload);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
582
|
-
res.end(html);
|
|
583
|
-
} catch {
|
|
584
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
585
|
-
res.end('404 Not Found');
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
return new Promise((resolveServer) => {
|
|
590
|
-
server.listen(port, host, () => {
|
|
591
|
-
actualPort = server.address().port;
|
|
592
|
-
resolveServer({
|
|
593
|
-
server,
|
|
594
|
-
port: actualPort,
|
|
595
|
-
close: () => {
|
|
596
|
-
server.close();
|
|
557
|
+
catch {
|
|
558
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
559
|
+
res.end('404 Not Found');
|
|
597
560
|
}
|
|
598
|
-
});
|
|
599
561
|
});
|
|
600
|
-
|
|
562
|
+
return new Promise((resolveServer) => {
|
|
563
|
+
server.listen(port, host, () => {
|
|
564
|
+
actualPort = server.address().port;
|
|
565
|
+
resolveServer({
|
|
566
|
+
server,
|
|
567
|
+
port: actualPort,
|
|
568
|
+
close: () => {
|
|
569
|
+
server.close();
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
});
|
|
601
574
|
}
|
|
602
|
-
|
|
603
575
|
/**
|
|
604
576
|
* @typedef {{
|
|
605
577
|
* path: string;
|
|
@@ -616,331 +588,289 @@ export async function createPreviewServer(options) {
|
|
|
616
588
|
* load_module_ref?: string | null;
|
|
617
589
|
* }} PreviewRoute
|
|
618
590
|
*/
|
|
619
|
-
|
|
620
591
|
/**
|
|
621
592
|
* @param {string} distDir
|
|
622
593
|
* @returns {Promise<PreviewRoute[]>}
|
|
623
594
|
*/
|
|
624
595
|
export async function loadRouteManifest(distDir) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
596
|
+
const manifestPath = join(distDir, 'assets', 'router-manifest.json');
|
|
597
|
+
try {
|
|
598
|
+
const source = await readFile(manifestPath, 'utf8');
|
|
599
|
+
const parsed = JSON.parse(source);
|
|
600
|
+
const routes = Array.isArray(parsed?.routes) ? parsed.routes : [];
|
|
601
|
+
return routes
|
|
602
|
+
.filter((entry) => entry &&
|
|
603
|
+
typeof entry === 'object' &&
|
|
604
|
+
typeof entry.path === 'string' &&
|
|
605
|
+
typeof entry.output === 'string')
|
|
606
|
+
.sort((a, b) => compareRouteSpecificity(a.path, b.path));
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return [];
|
|
610
|
+
}
|
|
641
611
|
}
|
|
642
|
-
|
|
643
612
|
export const matchRoute = matchManifestRoute;
|
|
644
|
-
|
|
645
613
|
/**
|
|
646
614
|
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, routePattern?: string, routeFile?: string, routeId?: string }} input
|
|
647
615
|
* @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, load: string } }>}
|
|
648
616
|
*/
|
|
649
|
-
export async function executeServerRoute({
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const result = payload.result;
|
|
704
|
-
const trace = payload.trace;
|
|
705
|
-
if (result && typeof result === 'object' && !Array.isArray(result) && typeof result.kind === 'string') {
|
|
617
|
+
export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, routePattern, routeFile, routeId, guardOnly = false }) {
|
|
618
|
+
if (!source || !String(source).trim()) {
|
|
619
|
+
return {
|
|
620
|
+
result: { kind: 'data', data: {} },
|
|
621
|
+
trace: { guard: 'none', load: 'none' }
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const payload = await spawnNodeServerRunner({
|
|
625
|
+
source,
|
|
626
|
+
sourcePath,
|
|
627
|
+
params,
|
|
628
|
+
requestUrl: requestUrl || 'http://localhost/',
|
|
629
|
+
requestMethod: requestMethod || 'GET',
|
|
630
|
+
requestHeaders: sanitizeRequestHeaders(requestHeaders || {}),
|
|
631
|
+
routePattern: routePattern || '',
|
|
632
|
+
routeFile: routeFile || sourcePath || '',
|
|
633
|
+
routeId: routeId || routeIdFromSourcePath(sourcePath || ''),
|
|
634
|
+
guardOnly
|
|
635
|
+
});
|
|
636
|
+
if (payload === null || payload === undefined) {
|
|
637
|
+
return {
|
|
638
|
+
result: { kind: 'data', data: {} },
|
|
639
|
+
trace: { guard: 'none', load: 'none' }
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
if (typeof payload !== 'object' || Array.isArray(payload)) {
|
|
643
|
+
throw new Error('[zenith-preview] server script payload must be an object');
|
|
644
|
+
}
|
|
645
|
+
const errorEnvelope = payload.__zenith_error;
|
|
646
|
+
if (errorEnvelope && typeof errorEnvelope === 'object') {
|
|
647
|
+
return {
|
|
648
|
+
result: {
|
|
649
|
+
kind: 'deny',
|
|
650
|
+
status: 500,
|
|
651
|
+
message: String(errorEnvelope.message || 'Server route execution failed')
|
|
652
|
+
},
|
|
653
|
+
trace: { guard: 'none', load: 'deny' }
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const result = payload.result;
|
|
657
|
+
const trace = payload.trace;
|
|
658
|
+
if (result && typeof result === 'object' && !Array.isArray(result) && typeof result.kind === 'string') {
|
|
659
|
+
return {
|
|
660
|
+
result,
|
|
661
|
+
trace: trace && typeof trace === 'object'
|
|
662
|
+
? {
|
|
663
|
+
guard: String(trace.guard || 'none'),
|
|
664
|
+
load: String(trace.load || 'none')
|
|
665
|
+
}
|
|
666
|
+
: { guard: 'none', load: 'none' }
|
|
667
|
+
};
|
|
668
|
+
}
|
|
706
669
|
return {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
: { guard: 'none', load: 'none' }
|
|
670
|
+
result: {
|
|
671
|
+
kind: 'data',
|
|
672
|
+
data: payload
|
|
673
|
+
},
|
|
674
|
+
trace: { guard: 'none', load: 'data' }
|
|
714
675
|
};
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return {
|
|
718
|
-
result: {
|
|
719
|
-
kind: 'data',
|
|
720
|
-
data: payload
|
|
721
|
-
},
|
|
722
|
-
trace: { guard: 'none', load: 'data' }
|
|
723
|
-
};
|
|
724
676
|
}
|
|
725
|
-
|
|
726
677
|
/**
|
|
727
678
|
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, routePattern?: string, routeFile?: string, routeId?: string }} input
|
|
728
679
|
* @returns {Promise<Record<string, unknown> | null>}
|
|
729
680
|
*/
|
|
730
681
|
export async function executeServerScript(input) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
return {};
|
|
682
|
+
const execution = await executeServerRoute(input);
|
|
683
|
+
const result = execution?.result;
|
|
684
|
+
if (!result || typeof result !== 'object') {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
if (result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
688
|
+
return result.data;
|
|
689
|
+
}
|
|
690
|
+
if (result.kind === 'redirect') {
|
|
691
|
+
return {
|
|
692
|
+
__zenith_error: {
|
|
693
|
+
status: Number.isInteger(result.status) ? result.status : 302,
|
|
694
|
+
code: 'REDIRECT',
|
|
695
|
+
message: `Redirect to ${String(result.location || '')}`
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
if (result.kind === 'deny') {
|
|
700
|
+
return {
|
|
701
|
+
__zenith_error: {
|
|
702
|
+
status: Number.isInteger(result.status) ? result.status : 403,
|
|
703
|
+
code: 'ACCESS_DENIED',
|
|
704
|
+
message: String(result.message || 'Access denied')
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return {};
|
|
761
709
|
}
|
|
762
|
-
|
|
763
710
|
/**
|
|
764
711
|
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl: string, requestMethod: string, requestHeaders: Record<string, string>, routePattern: string, routeFile: string, routeId: string }} input
|
|
765
712
|
* @returns {Promise<unknown>}
|
|
766
713
|
*/
|
|
767
714
|
function spawnNodeServerRunner(input) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
try {
|
|
817
|
-
resolvePromise(JSON.parse(raw));
|
|
818
|
-
} catch (error) {
|
|
819
|
-
rejectPromise(
|
|
820
|
-
new Error(
|
|
821
|
-
`[zenith-preview] invalid server payload JSON: ${error instanceof Error ? error.message : String(error)
|
|
822
|
-
}`
|
|
823
|
-
)
|
|
824
|
-
);
|
|
825
|
-
}
|
|
715
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
716
|
+
const child = spawn(process.execPath, ['--experimental-vm-modules', '--input-type=module', '-e', SERVER_SCRIPT_RUNNER], {
|
|
717
|
+
env: {
|
|
718
|
+
...process.env,
|
|
719
|
+
ZENITH_SERVER_SOURCE: input.source,
|
|
720
|
+
ZENITH_SERVER_SOURCE_PATH: input.sourcePath || '',
|
|
721
|
+
ZENITH_SERVER_PARAMS: JSON.stringify(input.params || {}),
|
|
722
|
+
ZENITH_SERVER_REQUEST_URL: input.requestUrl || 'http://localhost/',
|
|
723
|
+
ZENITH_SERVER_REQUEST_METHOD: input.requestMethod || 'GET',
|
|
724
|
+
ZENITH_SERVER_REQUEST_HEADERS: JSON.stringify(input.requestHeaders || {}),
|
|
725
|
+
ZENITH_SERVER_ROUTE_PATTERN: input.routePattern || '',
|
|
726
|
+
ZENITH_SERVER_ROUTE_FILE: input.routeFile || input.sourcePath || '',
|
|
727
|
+
ZENITH_SERVER_ROUTE_ID: input.routeId || '',
|
|
728
|
+
ZENITH_SERVER_GUARD_ONLY: input.guardOnly ? '1' : '',
|
|
729
|
+
ZENITH_SERVER_CONTRACT_PATH: join(__dirname, 'server-contract.js')
|
|
730
|
+
},
|
|
731
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
732
|
+
});
|
|
733
|
+
let stdout = '';
|
|
734
|
+
let stderr = '';
|
|
735
|
+
child.stdout.on('data', (chunk) => {
|
|
736
|
+
stdout += String(chunk);
|
|
737
|
+
});
|
|
738
|
+
child.stderr.on('data', (chunk) => {
|
|
739
|
+
stderr += String(chunk);
|
|
740
|
+
});
|
|
741
|
+
child.on('error', (error) => {
|
|
742
|
+
rejectPromise(error);
|
|
743
|
+
});
|
|
744
|
+
child.on('close', (code) => {
|
|
745
|
+
if (code !== 0) {
|
|
746
|
+
rejectPromise(new Error(`[zenith-preview] server script execution failed (${code}): ${stderr.trim() || stdout.trim()}`));
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const raw = stdout.trim();
|
|
750
|
+
if (!raw || raw === 'null') {
|
|
751
|
+
resolvePromise(null);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
resolvePromise(JSON.parse(raw));
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
rejectPromise(new Error(`[zenith-preview] invalid server payload JSON: ${error instanceof Error ? error.message : String(error)}`));
|
|
759
|
+
}
|
|
760
|
+
});
|
|
826
761
|
});
|
|
827
|
-
});
|
|
828
762
|
}
|
|
829
|
-
|
|
830
763
|
/**
|
|
831
764
|
* @param {string} html
|
|
832
765
|
* @param {Record<string, unknown>} payload
|
|
833
766
|
* @returns {string}
|
|
834
767
|
*/
|
|
835
768
|
export function injectSsrPayload(html, payload) {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
return
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
return `${scriptTag}${html}`;
|
|
769
|
+
const serialized = serializeInlineScriptJson(payload);
|
|
770
|
+
const scriptTag = `<script id="zenith-ssr-data">window.__zenith_ssr_data = ${serialized};</script>`;
|
|
771
|
+
const existingTagRe = /<script\b[^>]*\bid=(["'])zenith-ssr-data\1[^>]*>[\s\S]*?<\/script>/i;
|
|
772
|
+
if (existingTagRe.test(html)) {
|
|
773
|
+
return html.replace(existingTagRe, scriptTag);
|
|
774
|
+
}
|
|
775
|
+
const headClose = html.match(/<\/head>/i);
|
|
776
|
+
if (headClose) {
|
|
777
|
+
return html.replace(/<\/head>/i, `${scriptTag}</head>`);
|
|
778
|
+
}
|
|
779
|
+
const bodyOpen = html.match(/<body\b[^>]*>/i);
|
|
780
|
+
if (bodyOpen) {
|
|
781
|
+
return html.replace(bodyOpen[0], `${bodyOpen[0]}${scriptTag}`);
|
|
782
|
+
}
|
|
783
|
+
return `${scriptTag}${html}`;
|
|
854
784
|
}
|
|
855
|
-
|
|
856
785
|
/**
|
|
857
786
|
* @param {Record<string, unknown>} payload
|
|
858
787
|
* @returns {string}
|
|
859
788
|
*/
|
|
860
789
|
function serializeInlineScriptJson(payload) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
790
|
+
return JSON.stringify(payload)
|
|
791
|
+
.replace(/</g, '\\u003C')
|
|
792
|
+
.replace(/>/g, '\\u003E')
|
|
793
|
+
.replace(/\//g, '\\u002F')
|
|
794
|
+
.replace(/\u2028/g, '\\u2028')
|
|
795
|
+
.replace(/\u2029/g, '\\u2029');
|
|
867
796
|
}
|
|
868
|
-
|
|
869
797
|
export function toStaticFilePath(distDir, pathname) {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
resolved
|
|
875
|
-
|
|
876
|
-
|
|
798
|
+
let resolved = pathname;
|
|
799
|
+
if (resolved === '/') {
|
|
800
|
+
resolved = '/index.html';
|
|
801
|
+
}
|
|
802
|
+
else if (!extname(resolved)) {
|
|
803
|
+
resolved += '/index.html';
|
|
804
|
+
}
|
|
805
|
+
return resolveWithinDist(distDir, resolved);
|
|
877
806
|
}
|
|
878
|
-
|
|
879
807
|
export function resolveWithinDist(distDir, requestPath) {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
808
|
+
let decoded = requestPath;
|
|
809
|
+
try {
|
|
810
|
+
decoded = decodeURIComponent(requestPath);
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
const normalized = normalize(decoded).replace(/\\/g, '/');
|
|
816
|
+
const relative = normalized.replace(/^\/+/, '');
|
|
817
|
+
const root = resolve(distDir);
|
|
818
|
+
const candidate = resolve(root, relative);
|
|
819
|
+
if (candidate === root || candidate.startsWith(`${root}${sep}`)) {
|
|
820
|
+
return candidate;
|
|
821
|
+
}
|
|
884
822
|
return null;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
const normalized = normalize(decoded).replace(/\\/g, '/');
|
|
888
|
-
const relative = normalized.replace(/^\/+/, '');
|
|
889
|
-
const root = resolve(distDir);
|
|
890
|
-
const candidate = resolve(root, relative);
|
|
891
|
-
if (candidate === root || candidate.startsWith(`${root}${sep}`)) {
|
|
892
|
-
return candidate;
|
|
893
|
-
}
|
|
894
|
-
return null;
|
|
895
823
|
}
|
|
896
|
-
|
|
897
824
|
/**
|
|
898
825
|
* @param {Record<string, string | string[] | undefined>} headers
|
|
899
826
|
* @returns {Record<string, string>}
|
|
900
827
|
*/
|
|
901
828
|
function sanitizeRequestHeaders(headers) {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
829
|
+
const out = Object.create(null);
|
|
830
|
+
const denyExact = new Set(['proxy-authorization', 'set-cookie']);
|
|
831
|
+
const denyPrefixes = ['x-forwarded-', 'cf-'];
|
|
832
|
+
for (const [rawKey, rawValue] of Object.entries(headers || {})) {
|
|
833
|
+
const key = String(rawKey || '').toLowerCase();
|
|
834
|
+
if (!key)
|
|
835
|
+
continue;
|
|
836
|
+
if (denyExact.has(key))
|
|
837
|
+
continue;
|
|
838
|
+
if (denyPrefixes.some((prefix) => key.startsWith(prefix)))
|
|
839
|
+
continue;
|
|
840
|
+
let value = '';
|
|
841
|
+
if (Array.isArray(rawValue)) {
|
|
842
|
+
value = rawValue.filter((entry) => entry !== undefined).map(String).join(', ');
|
|
843
|
+
}
|
|
844
|
+
else if (rawValue !== undefined) {
|
|
845
|
+
value = String(rawValue);
|
|
846
|
+
}
|
|
847
|
+
out[key] = value;
|
|
915
848
|
}
|
|
916
|
-
out
|
|
917
|
-
}
|
|
918
|
-
return out;
|
|
849
|
+
return out;
|
|
919
850
|
}
|
|
920
|
-
|
|
921
851
|
/**
|
|
922
852
|
* @param {string} sourcePath
|
|
923
853
|
* @returns {string}
|
|
924
854
|
*/
|
|
925
855
|
function routeIdFromSourcePath(sourcePath) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
856
|
+
const normalized = String(sourcePath || '').replaceAll('\\', '/');
|
|
857
|
+
const marker = '/pages/';
|
|
858
|
+
const markerIndex = normalized.lastIndexOf(marker);
|
|
859
|
+
let routeId = markerIndex >= 0
|
|
860
|
+
? normalized.slice(markerIndex + marker.length)
|
|
861
|
+
: normalized.split('/').pop() || normalized;
|
|
862
|
+
routeId = routeId.replace(/\.zen$/i, '');
|
|
863
|
+
if (routeId.endsWith('/index')) {
|
|
864
|
+
routeId = routeId.slice(0, -('/index'.length));
|
|
865
|
+
}
|
|
866
|
+
return routeId || 'index';
|
|
937
867
|
}
|
|
938
|
-
|
|
939
868
|
async function fileExists(fullPath) {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
869
|
+
try {
|
|
870
|
+
await access(fullPath);
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
946
876
|
}
|