@ecopages/core 0.2.0-beta.28 → 0.2.0-beta.29
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 +2 -2
- package/src/adapters/abstract/application-adapter.d.ts +22 -4
- package/src/adapters/abstract/application-adapter.js +27 -4
- package/src/adapters/abstract/server-adapter.d.ts +10 -0
- package/src/adapters/bun/create-app.d.ts +1 -0
- package/src/adapters/bun/create-app.js +11 -4
- package/src/adapters/bun/server-adapter.js +1 -0
- package/src/adapters/node/create-app.d.ts +1 -0
- package/src/adapters/node/create-app.js +10 -3
- package/src/adapters/node/server-adapter.js +1 -0
- package/src/adapters/shared/fs-server-response-factory.d.ts +5 -0
- package/src/adapters/shared/fs-server-response-factory.js +20 -0
- package/src/adapters/shared/fs-server-response-matcher.d.ts +18 -0
- package/src/adapters/shared/fs-server-response-matcher.js +75 -15
- package/src/config/config-builder.d.ts +12 -2
- package/src/config/config-builder.js +41 -2
- package/src/config/constants.d.ts +1 -0
- package/src/config/constants.js +2 -1
- package/src/route-renderer/orchestration/route-pipeline/robots-meta.contribution.d.ts +15 -0
- package/src/route-renderer/orchestration/route-pipeline/robots-meta.contribution.js +36 -0
- package/src/route-renderer/orchestration/route-pipeline/route-html-finalization.service.d.ts +4 -0
- package/src/route-renderer/orchestration/route-pipeline/route-html-finalization.service.js +5 -2
- package/src/route-renderer/orchestration/route-pipeline/route-prepared-options.builder.js +6 -2
- package/src/static-site-generator/README.md +9 -1
- package/src/static-site-generator/sitemap-routes.d.ts +14 -0
- package/src/static-site-generator/sitemap-routes.js +31 -0
- package/src/static-site-generator/sitemap.d.ts +15 -0
- package/src/static-site-generator/sitemap.js +33 -0
- package/src/static-site-generator/static-export-context.d.ts +9 -1
- package/src/static-site-generator/static-site-generator.d.ts +27 -6
- package/src/static-site-generator/static-site-generator.js +200 -50
- package/src/types/internal-types.d.ts +8 -1
- package/src/types/public-types.d.ts +77 -1
- package/src/utils/ecopages-route-info.d.ts +26 -0
- package/src/utils/ecopages-route-info.js +26 -0
- package/src/utils/html-escaping.d.ts +7 -0
- package/src/utils/html-escaping.js +5 -1
- package/src/utils/path-pattern.d.ts +23 -0
- package/src/utils/path-pattern.js +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/core",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.29",
|
|
4
4
|
"description": "Core package for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"directory": "packages/core"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@ecopages/file-system": "0.2.0-beta.
|
|
20
|
+
"@ecopages/file-system": "0.2.0-beta.29",
|
|
21
21
|
"@ecopages/logger": "^0.2.3",
|
|
22
22
|
"@ecopages/scripts-injector": "^0.1.5",
|
|
23
23
|
"@oxc-project/runtime": "0.134.0",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { SourceModuleLoader } from '../../services/module-loading/module-loading-types.js';
|
|
10
10
|
import type { EcoPagesAppConfig } from '../../types/internal-types.js';
|
|
11
|
-
import type { ApiHandler, ApiHandlerContext, ErrorHandler, Middleware, RouteOptions, StaticRoute, ViewLoader } from '../../types/public-types.js';
|
|
11
|
+
import type { ApiHandler, ApiHandlerContext, ErrorHandler, Middleware, RouteOptions, StaticRoute, ViewLoader, EcopagesRouteInfo } from '../../types/public-types.js';
|
|
12
12
|
import type { EcopagesWebSocketHandler } from '../../types/public-types.js';
|
|
13
13
|
import { type EcopagesRuntimeLabel } from '../../dev/runtime-server-started-message.js';
|
|
14
14
|
import { type ReturnParseCliArgs } from '../../utils/parse-cli-args.js';
|
|
@@ -52,6 +52,15 @@ export interface ApplicationRuntimeOptions {
|
|
|
52
52
|
}
|
|
53
53
|
export interface AppStartInfo {
|
|
54
54
|
origin: string;
|
|
55
|
+
/**
|
|
56
|
+
* Static-generation routes available when the runtime becomes ready.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* Empty when the server adapter has no route registry yet, or when route
|
|
60
|
+
* resolution fails (failures never block startup). Route resolution completes
|
|
61
|
+
* before the `onAppStart` callback runs.
|
|
62
|
+
*/
|
|
63
|
+
routes: EcopagesRouteInfo[];
|
|
55
64
|
}
|
|
56
65
|
export type OnAppStartCallback = (info: AppStartInfo) => void;
|
|
57
66
|
/** @deprecated Use {@link AppStartInfo}. */
|
|
@@ -82,7 +91,7 @@ export interface ApplicationAdapter<T = any> extends AsyncDisposable {
|
|
|
82
91
|
/** Boot the server. Pass a callback to run when the runtime is ready (optional). */
|
|
83
92
|
start(onAppStart?: OnAppStartCallback): Promise<T | void>;
|
|
84
93
|
/** Invoked by embedded hosts once the app can take traffic. */
|
|
85
|
-
handleListening(origin: string): void
|
|
94
|
+
handleListening(origin: string): void | Promise<void>;
|
|
86
95
|
stop(force?: boolean): Promise<void>;
|
|
87
96
|
}
|
|
88
97
|
/**
|
|
@@ -272,8 +281,17 @@ export declare abstract class AbstractApplicationAdapter<TOptions extends Applic
|
|
|
272
281
|
/**
|
|
273
282
|
* Invoked by embedded hosts (for example Vite) once the app can take traffic.
|
|
274
283
|
*/
|
|
275
|
-
handleListening(origin: string): void
|
|
276
|
-
protected notifyListening(origin: string): void
|
|
284
|
+
handleListening(origin: string): Promise<void>;
|
|
285
|
+
protected notifyListening(origin: string): Promise<void>;
|
|
286
|
+
/**
|
|
287
|
+
* Resolves routes exposed on {@link AppStartInfo}.
|
|
288
|
+
*
|
|
289
|
+
* @remarks
|
|
290
|
+
* Default is empty. Runtime adapters override this to read from the
|
|
291
|
+
* initialized server route registry.
|
|
292
|
+
*/
|
|
293
|
+
protected resolveAppRoutes(): Promise<EcopagesRouteInfo[]>;
|
|
294
|
+
private invokeAppStartCallback;
|
|
277
295
|
/**
|
|
278
296
|
* Stops the application server and releases runtime resources.
|
|
279
297
|
*
|
|
@@ -218,16 +218,39 @@ class AbstractApplicationAdapter {
|
|
|
218
218
|
/**
|
|
219
219
|
* Invoked by embedded hosts (for example Vite) once the app can take traffic.
|
|
220
220
|
*/
|
|
221
|
-
handleListening(origin) {
|
|
222
|
-
this.notifyListening(origin);
|
|
221
|
+
async handleListening(origin) {
|
|
222
|
+
await this.notifyListening(origin);
|
|
223
223
|
}
|
|
224
|
-
notifyListening(origin) {
|
|
224
|
+
async notifyListening(origin) {
|
|
225
225
|
const normalizedOrigin = origin.replace(/\/$/, "");
|
|
226
226
|
startupTrace.markServerListening();
|
|
227
227
|
if (!this.onAppStartCallback) {
|
|
228
228
|
this.logServerStarted(normalizedOrigin);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
await this.invokeAppStartCallback(normalizedOrigin);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Resolves routes exposed on {@link AppStartInfo}.
|
|
235
|
+
*
|
|
236
|
+
* @remarks
|
|
237
|
+
* Default is empty. Runtime adapters override this to read from the
|
|
238
|
+
* initialized server route registry.
|
|
239
|
+
*/
|
|
240
|
+
async resolveAppRoutes() {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
async invokeAppStartCallback(origin) {
|
|
244
|
+
let routes = [];
|
|
245
|
+
try {
|
|
246
|
+
routes = await this.resolveAppRoutes();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
appLogger.debug(
|
|
249
|
+
"Failed to resolve app start routes; continuing with empty routes",
|
|
250
|
+
error instanceof Error ? error.message : String(error)
|
|
251
|
+
);
|
|
229
252
|
}
|
|
230
|
-
this.onAppStartCallback?.({ origin
|
|
253
|
+
this.onAppStartCallback?.({ origin, routes });
|
|
231
254
|
}
|
|
232
255
|
/**
|
|
233
256
|
* Stops the application server and releases runtime resources.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { EcoPagesAppConfig } from '../../types/internal-types.js';
|
|
10
10
|
import type { ApiHandler } from '../../types/public-types.js';
|
|
11
|
+
import type { StaticGenerationRoute } from '../../router/server/route-registry.js';
|
|
11
12
|
/**
|
|
12
13
|
* Configuration options for all server adapters
|
|
13
14
|
*/
|
|
@@ -35,6 +36,15 @@ export interface ServerAdapterResult {
|
|
|
35
36
|
}) => Promise<string | undefined>;
|
|
36
37
|
servePreviewOnly: () => Promise<string | undefined>;
|
|
37
38
|
dispose(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Lists static-generation routes for app-start consumers.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Optional so lightweight adapter stubs in tests need not implement routing.
|
|
44
|
+
*/
|
|
45
|
+
listStaticGenerationRoutes?: (input: {
|
|
46
|
+
runtimeOrigin: string;
|
|
47
|
+
}) => Promise<readonly StaticGenerationRoute[]>;
|
|
38
48
|
}
|
|
39
49
|
/**
|
|
40
50
|
* Abstract base class for server adapters across different runtimes
|
|
@@ -55,6 +55,7 @@ export declare class BunEcopagesApp<WebSocketData = undefined> extends SharedApp
|
|
|
55
55
|
*/
|
|
56
56
|
protected bootServer(): Promise<Server<WebSocketData> | void>;
|
|
57
57
|
stop(force?: boolean): Promise<void>;
|
|
58
|
+
protected resolveAppRoutes(): Promise<import("../../index.browser.js").EcopagesRouteInfo[]>;
|
|
58
59
|
}
|
|
59
60
|
/**
|
|
60
61
|
* Factory function to create a Bun application
|
|
@@ -5,6 +5,7 @@ import { startupTrace } from "../../diagnostics/startup-trace.js";
|
|
|
5
5
|
import { createBunServerAdapter } from "./server-adapter.js";
|
|
6
6
|
import { BunRuntimeHost } from "./runtime-host.js";
|
|
7
7
|
import { hostOwnsDevClient } from "../../dev/dev-client-ownership.js";
|
|
8
|
+
import { resolveAppStartRoutes } from "../../utils/ecopages-route-info.js";
|
|
8
9
|
class BunEcopagesApp extends SharedApplicationAdapter {
|
|
9
10
|
serverAdapter;
|
|
10
11
|
server = null;
|
|
@@ -83,7 +84,7 @@ class BunEcopagesApp extends SharedApplicationAdapter {
|
|
|
83
84
|
if (preview && serveOnly) {
|
|
84
85
|
const previewOrigin2 = await this.serverAdapter.servePreviewOnly();
|
|
85
86
|
if (previewOrigin2) {
|
|
86
|
-
this.notifyListening(previewOrigin2);
|
|
87
|
+
await this.notifyListening(previewOrigin2);
|
|
87
88
|
}
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
@@ -92,7 +93,7 @@ class BunEcopagesApp extends SharedApplicationAdapter {
|
|
|
92
93
|
const previewOrigin2 = await this.serverAdapter.buildStatic({ preview, force });
|
|
93
94
|
appLogger.debugTimeEnd("Building static pages");
|
|
94
95
|
if (preview && previewOrigin2) {
|
|
95
|
-
this.notifyListening(previewOrigin2);
|
|
96
|
+
await this.notifyListening(previewOrigin2);
|
|
96
97
|
}
|
|
97
98
|
if (build) {
|
|
98
99
|
process.exit(0);
|
|
@@ -131,10 +132,10 @@ class BunEcopagesApp extends SharedApplicationAdapter {
|
|
|
131
132
|
process.exit(0);
|
|
132
133
|
}
|
|
133
134
|
} else {
|
|
134
|
-
this.notifyListening(runtimeOrigin);
|
|
135
|
+
await this.notifyListening(runtimeOrigin);
|
|
135
136
|
}
|
|
136
137
|
if (preview && previewOrigin) {
|
|
137
|
-
this.notifyListening(previewOrigin);
|
|
138
|
+
await this.notifyListening(previewOrigin);
|
|
138
139
|
}
|
|
139
140
|
return this.server ?? void 0;
|
|
140
141
|
}
|
|
@@ -152,6 +153,12 @@ class BunEcopagesApp extends SharedApplicationAdapter {
|
|
|
152
153
|
}
|
|
153
154
|
this.stopped = true;
|
|
154
155
|
}
|
|
156
|
+
async resolveAppRoutes() {
|
|
157
|
+
return resolveAppStartRoutes({
|
|
158
|
+
listStaticGenerationRoutes: this.serverAdapter?.listStaticGenerationRoutes,
|
|
159
|
+
runtimeOrigin: this.appConfig.baseUrl
|
|
160
|
+
});
|
|
161
|
+
}
|
|
155
162
|
}
|
|
156
163
|
async function createApp(options) {
|
|
157
164
|
return new BunEcopagesApp(options, {
|
|
@@ -539,6 +539,7 @@ class BunServerAdapter extends SharedServerAdapter {
|
|
|
539
539
|
completeInitialization: this.completeInitialization.bind(this),
|
|
540
540
|
handleRequest: this.handleRequest.bind(this),
|
|
541
541
|
attachUserWebSocketUpgrades: this.attachUserWebSocketUpgrades.bind(this),
|
|
542
|
+
listStaticGenerationRoutes: (input) => this.router.listStaticGenerationRoutes(input),
|
|
542
543
|
dispose: this.dispose.bind(this)
|
|
543
544
|
};
|
|
544
545
|
}
|
|
@@ -17,6 +17,7 @@ export declare class NodeEcopagesApp extends SharedApplicationAdapter<EcopagesAp
|
|
|
17
17
|
});
|
|
18
18
|
protected createServerAdapter(params: Parameters<typeof createNodeServerAdapter>[0]): Promise<NodeServerAdapterResult>;
|
|
19
19
|
stop(force?: boolean): Promise<void>;
|
|
20
|
+
protected resolveAppRoutes(): Promise<import("../../index.browser.js").EcopagesRouteInfo[]>;
|
|
20
21
|
protected initializeServerAdapter(): Promise<NodeServerAdapterResult>;
|
|
21
22
|
protected bootServer(): Promise<NodeServerInstance | void>;
|
|
22
23
|
fetch(request: Request): Promise<Response>;
|
|
@@ -6,6 +6,7 @@ import { NodeHttpRequestBridge } from "./http-request-bridge.js";
|
|
|
6
6
|
import { NodeRuntimeHost } from "./runtime-host.js";
|
|
7
7
|
import { hostOwnsDevClient } from "../../dev/dev-client-ownership.js";
|
|
8
8
|
import { startupTrace } from "../../diagnostics/startup-trace.js";
|
|
9
|
+
import { resolveAppStartRoutes } from "../../utils/ecopages-route-info.js";
|
|
9
10
|
class NodeEcopagesApp extends SharedApplicationAdapter {
|
|
10
11
|
serverAdapter;
|
|
11
12
|
server = null;
|
|
@@ -33,6 +34,12 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
|
|
|
33
34
|
}
|
|
34
35
|
this.stopped = true;
|
|
35
36
|
}
|
|
37
|
+
async resolveAppRoutes() {
|
|
38
|
+
return resolveAppStartRoutes({
|
|
39
|
+
listStaticGenerationRoutes: this.serverAdapter?.listStaticGenerationRoutes,
|
|
40
|
+
runtimeOrigin: this.appConfig.baseUrl
|
|
41
|
+
});
|
|
42
|
+
}
|
|
36
43
|
async initializeServerAdapter() {
|
|
37
44
|
const binding = resolveRuntimeBinding({
|
|
38
45
|
cliArgs: this.cliArgs,
|
|
@@ -71,7 +78,7 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
|
|
|
71
78
|
if (preview && serveOnly) {
|
|
72
79
|
const previewOrigin = await this.serverAdapter.servePreviewOnly();
|
|
73
80
|
if (previewOrigin) {
|
|
74
|
-
this.notifyListening(previewOrigin);
|
|
81
|
+
await this.notifyListening(previewOrigin);
|
|
75
82
|
}
|
|
76
83
|
return;
|
|
77
84
|
}
|
|
@@ -80,7 +87,7 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
|
|
|
80
87
|
const previewOrigin = await this.serverAdapter.buildStatic({ preview, force });
|
|
81
88
|
appLogger.debugTimeEnd("Building static pages");
|
|
82
89
|
if (preview && previewOrigin) {
|
|
83
|
-
this.notifyListening(previewOrigin);
|
|
90
|
+
await this.notifyListening(previewOrigin);
|
|
84
91
|
}
|
|
85
92
|
if (build) {
|
|
86
93
|
process.exit(0);
|
|
@@ -97,7 +104,7 @@ class NodeEcopagesApp extends SharedApplicationAdapter {
|
|
|
97
104
|
});
|
|
98
105
|
this.runtimeOrigin = this.runtimeHost.getOrigin(this.server, serveOptions);
|
|
99
106
|
await this.serverAdapter.completeInitialization(this.server);
|
|
100
|
-
this.notifyListening(this.runtimeOrigin);
|
|
107
|
+
await this.notifyListening(this.runtimeOrigin);
|
|
101
108
|
return this.server;
|
|
102
109
|
}
|
|
103
110
|
async fetch(request) {
|
|
@@ -198,6 +198,7 @@ class NodeServerAdapter extends SharedServerAdapter {
|
|
|
198
198
|
completeInitialization: this.completeInitialization.bind(this),
|
|
199
199
|
handleRequest: this.handleRequest.bind(this),
|
|
200
200
|
attachUserWebSocketUpgrades: this.attachUserWebSocketUpgrades.bind(this),
|
|
201
|
+
listStaticGenerationRoutes: (input) => this.router.listStaticGenerationRoutes(input),
|
|
201
202
|
dispose: this.dispose.bind(this)
|
|
202
203
|
};
|
|
203
204
|
}
|
|
@@ -16,6 +16,11 @@ export declare class FileSystemServerResponseFactory {
|
|
|
16
16
|
* Wraps already-rendered HTML in a 404 response envelope.
|
|
17
17
|
*/
|
|
18
18
|
createHtmlNotFoundResponse(body: RouteRendererBody): Promise<Response>;
|
|
19
|
+
createDefaultServerErrorResponse(): Promise<Response>;
|
|
20
|
+
/**
|
|
21
|
+
* Wraps already-rendered HTML in a 500 response envelope.
|
|
22
|
+
*/
|
|
23
|
+
createHtmlServerErrorResponse(body: RouteRendererBody): Promise<Response>;
|
|
19
24
|
/**
|
|
20
25
|
* Reads a static file response, returning `null` when the file is missing.
|
|
21
26
|
*/
|
|
@@ -38,6 +38,26 @@ class FileSystemServerResponseFactory {
|
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
async createDefaultServerErrorResponse() {
|
|
42
|
+
return new Response(STATUS_MESSAGE[500], {
|
|
43
|
+
status: 500,
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Wraps already-rendered HTML in a 500 response envelope.
|
|
51
|
+
*/
|
|
52
|
+
async createHtmlServerErrorResponse(body) {
|
|
53
|
+
return await this.createResponseWithBody(body, {
|
|
54
|
+
status: 500,
|
|
55
|
+
statusText: STATUS_MESSAGE[500],
|
|
56
|
+
headers: {
|
|
57
|
+
"Content-Type": "text/html"
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
41
61
|
/**
|
|
42
62
|
* Reads a static file response, returning `null` when the file is missing.
|
|
43
63
|
*/
|
|
@@ -55,7 +55,25 @@ export declare class FileSystemResponseMatcher {
|
|
|
55
55
|
* when the page template cannot be resolved.
|
|
56
56
|
*/
|
|
57
57
|
private renderCustomNotFoundResponse;
|
|
58
|
+
/**
|
|
59
|
+
* Renders the app-owned custom 500 page, falling back to the default text 500
|
|
60
|
+
* when the page template cannot be resolved.
|
|
61
|
+
*/
|
|
62
|
+
private renderCustomServerErrorResponse;
|
|
63
|
+
/**
|
|
64
|
+
* Builds development-only error details for the custom 500 page.
|
|
65
|
+
* @remarks Production omits these fields so stacks are not serialized into HTML.
|
|
66
|
+
*/
|
|
67
|
+
private buildServerErrorPageProps;
|
|
68
|
+
private renderCustomErrorPageResponse;
|
|
58
69
|
private renderCustomNotFoundResponseOrServerError;
|
|
70
|
+
/**
|
|
71
|
+
* Logs the original render failure, then tries the custom 500 page once.
|
|
72
|
+
* @remarks Any failure while rendering the custom 500 page falls back to the
|
|
73
|
+
* default plain-text response and never re-enters the custom page path.
|
|
74
|
+
* In development the thrown error's `message` and `stack` are passed into the
|
|
75
|
+
* custom 500 page props.
|
|
76
|
+
*/
|
|
59
77
|
private createInternalServerErrorResponse;
|
|
60
78
|
private createExecutionPlan;
|
|
61
79
|
/**
|
|
@@ -100,9 +100,9 @@ class FileSystemResponseMatcher {
|
|
|
100
100
|
return error;
|
|
101
101
|
}
|
|
102
102
|
if (error instanceof LocalsAccessError) {
|
|
103
|
-
return this.createInternalServerErrorResponse(error.message, match.requestedPathname, error);
|
|
103
|
+
return await this.createInternalServerErrorResponse(error.message, match.requestedPathname, error);
|
|
104
104
|
}
|
|
105
|
-
return this.createInternalServerErrorResponse(
|
|
105
|
+
return await this.createInternalServerErrorResponse(
|
|
106
106
|
error instanceof Error ? error.message : "Internal Server Error",
|
|
107
107
|
match.requestedPathname,
|
|
108
108
|
error
|
|
@@ -114,21 +114,66 @@ class FileSystemResponseMatcher {
|
|
|
114
114
|
* when the page template cannot be resolved.
|
|
115
115
|
*/
|
|
116
116
|
async renderCustomNotFoundResponse() {
|
|
117
|
-
|
|
117
|
+
return this.renderCustomErrorPageResponse({
|
|
118
|
+
templatePath: this.appConfig.absolutePaths.error404TemplatePath,
|
|
119
|
+
label: "404",
|
|
120
|
+
createDefaultResponse: () => this.fileSystemResponseFactory.createDefaultNotFoundResponse(),
|
|
121
|
+
createHtmlResponse: (body) => this.fileSystemResponseFactory.createHtmlNotFoundResponse(body)
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Renders the app-owned custom 500 page, falling back to the default text 500
|
|
126
|
+
* when the page template cannot be resolved.
|
|
127
|
+
*/
|
|
128
|
+
async renderCustomServerErrorResponse(error) {
|
|
129
|
+
return this.renderCustomErrorPageResponse({
|
|
130
|
+
templatePath: this.appConfig.absolutePaths.error500TemplatePath,
|
|
131
|
+
label: "500",
|
|
132
|
+
props: this.buildServerErrorPageProps(error),
|
|
133
|
+
createDefaultResponse: () => this.fileSystemResponseFactory.createDefaultServerErrorResponse(),
|
|
134
|
+
createHtmlResponse: (body) => this.fileSystemResponseFactory.createHtmlServerErrorResponse(body)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Builds development-only error details for the custom 500 page.
|
|
139
|
+
* @remarks Production omits these fields so stacks are not serialized into HTML.
|
|
140
|
+
*/
|
|
141
|
+
buildServerErrorPageProps(error) {
|
|
142
|
+
if (!isDevelopmentRuntime()) {
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
if (error instanceof Error) {
|
|
146
|
+
return {
|
|
147
|
+
message: error.message,
|
|
148
|
+
stack: error.stack
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
message: String(error)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async renderCustomErrorPageResponse({
|
|
156
|
+
templatePath,
|
|
157
|
+
label,
|
|
158
|
+
props,
|
|
159
|
+
createDefaultResponse,
|
|
160
|
+
createHtmlResponse
|
|
161
|
+
}) {
|
|
118
162
|
let routeRenderer;
|
|
119
163
|
try {
|
|
120
|
-
routeRenderer = this.routeRendererFactory.getPageRenderer(
|
|
164
|
+
routeRenderer = this.routeRendererFactory.getPageRenderer(templatePath);
|
|
121
165
|
} catch {
|
|
122
166
|
appLogger.debug(
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
`Custom ${label} template not found, falling back to default ${label} response`,
|
|
168
|
+
templatePath
|
|
125
169
|
);
|
|
126
|
-
return
|
|
170
|
+
return createDefaultResponse();
|
|
127
171
|
}
|
|
128
172
|
const result = await routeRenderer.execute({
|
|
129
|
-
file:
|
|
173
|
+
file: templatePath,
|
|
174
|
+
props
|
|
130
175
|
});
|
|
131
|
-
return
|
|
176
|
+
return createHtmlResponse(result.body);
|
|
132
177
|
}
|
|
133
178
|
async renderCustomNotFoundResponseOrServerError(pathname) {
|
|
134
179
|
try {
|
|
@@ -137,23 +182,38 @@ class FileSystemResponseMatcher {
|
|
|
137
182
|
if (error instanceof Response) {
|
|
138
183
|
return error;
|
|
139
184
|
}
|
|
140
|
-
return this.createInternalServerErrorResponse(
|
|
185
|
+
return await this.createInternalServerErrorResponse(
|
|
141
186
|
error instanceof Error ? error.message : "Internal Server Error",
|
|
142
187
|
pathname,
|
|
143
188
|
error
|
|
144
189
|
);
|
|
145
190
|
}
|
|
146
191
|
}
|
|
147
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Logs the original render failure, then tries the custom 500 page once.
|
|
194
|
+
* @remarks Any failure while rendering the custom 500 page falls back to the
|
|
195
|
+
* default plain-text response and never re-enters the custom page path.
|
|
196
|
+
* In development the thrown error's `message` and `stack` are passed into the
|
|
197
|
+
* custom 500 page props.
|
|
198
|
+
*/
|
|
199
|
+
async createInternalServerErrorResponse(message, pathname, error) {
|
|
148
200
|
if (isDevelopmentRuntime() || appLogger.isDebugEnabled()) {
|
|
149
201
|
appLogger.error(`[FileSystemResponseMatcher] ${message} at ${pathname}`, error);
|
|
150
202
|
} else {
|
|
151
203
|
appLogger.error(`[FileSystemResponseMatcher] Render error at ${pathname}`, error);
|
|
152
204
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
205
|
+
try {
|
|
206
|
+
return await this.renderCustomServerErrorResponse(error);
|
|
207
|
+
} catch (serverErrorPageError) {
|
|
208
|
+
if (serverErrorPageError instanceof Response) {
|
|
209
|
+
return serverErrorPageError;
|
|
210
|
+
}
|
|
211
|
+
appLogger.error(
|
|
212
|
+
`[FileSystemResponseMatcher] Custom 500 template failed at ${pathname}`,
|
|
213
|
+
serverErrorPageError
|
|
214
|
+
);
|
|
215
|
+
return this.fileSystemResponseFactory.createDefaultServerErrorResponse();
|
|
216
|
+
}
|
|
157
217
|
}
|
|
158
218
|
async createExecutionPlan(match, request) {
|
|
159
219
|
const cacheKey = this.pageRequestCacheCoordinator.buildCacheKey({
|
|
@@ -8,7 +8,7 @@ import type { EcoPagesAppConfig, RobotsPreference } from '../types/internal-type
|
|
|
8
8
|
import type { AnyIntegrationPlugin } from '../plugins/integration-plugin.js';
|
|
9
9
|
import type { Processor } from '../plugins/processor.js';
|
|
10
10
|
import type { EcoSourceTransform } from '../plugins/source-transform.js';
|
|
11
|
-
import type { PageMetadataProps } from '../types/public-types.js';
|
|
11
|
+
import type { PageMetadataProps, SitemapConfig } from '../types/public-types.js';
|
|
12
12
|
import type { CacheConfig } from '../services/cache/cache.types.js';
|
|
13
13
|
export declare const CONFIG_BUILDER_ERRORS: {
|
|
14
14
|
readonly DUPLICATE_INTEGRATION_NAMES: "Integrations names must be unique";
|
|
@@ -16,7 +16,7 @@ export declare const CONFIG_BUILDER_ERRORS: {
|
|
|
16
16
|
readonly MIXED_JSX_ENGINES: "Both kitajs and react integrations are enabled. Use per-file JSX import source/pragma consistently (e.g. `/** @jsxImportSource react */` for React files and `/** @jsxImportSource @kitajs/html */` for Kita files).";
|
|
17
17
|
readonly duplicateProcessorName: (name: string) => string;
|
|
18
18
|
readonly duplicateLoaderName: (name: string) => string;
|
|
19
|
-
readonly duplicateSemanticTemplate: (kind: "html" | "404", matches: string[]) => string;
|
|
19
|
+
readonly duplicateSemanticTemplate: (kind: "html" | "404" | "500", matches: string[]) => string;
|
|
20
20
|
readonly incompatibleRuntimeCapability: (kind: "integration" | "processor", name: string, runtime: RuntimeKind, reason: string) => string;
|
|
21
21
|
readonly unsupportedRuntimeVersion: (kind: "integration" | "processor", name: string, runtime: RuntimeKind, current: string, min: string) => string;
|
|
22
22
|
readonly invalidRuntimeVersion: (kind: "integration" | "processor", name: string, version: string) => string;
|
|
@@ -138,6 +138,16 @@ export declare class ConfigBuilder {
|
|
|
138
138
|
setRobotsTxt(robotsTxt: {
|
|
139
139
|
preferences: RobotsPreference;
|
|
140
140
|
}): this;
|
|
141
|
+
/**
|
|
142
|
+
* Sets automatic sitemap.xml generation during static export.
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* Merged over `{ enabled: false, fileName: 'sitemap.xml', extraUrls: [], exclude: [] }`.
|
|
146
|
+
* Existing apps stay unchanged until `enabled: true` is set. Page-level
|
|
147
|
+
* `metadata.robots.index: false` and metadata resolution failures are handled
|
|
148
|
+
* during static generation (not by this config merge).
|
|
149
|
+
*/
|
|
150
|
+
setSitemap(sitemap: SitemapConfig): this;
|
|
141
151
|
/**
|
|
142
152
|
* Sets the integration plugins to use.
|
|
143
153
|
* These plugins provide additional functionality to the application.
|
|
@@ -53,6 +53,12 @@ class ConfigBuilder {
|
|
|
53
53
|
"*": []
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
|
+
sitemap: {
|
|
57
|
+
enabled: false,
|
|
58
|
+
fileName: "sitemap.xml",
|
|
59
|
+
extraUrls: [],
|
|
60
|
+
exclude: []
|
|
61
|
+
},
|
|
56
62
|
integrations: [],
|
|
57
63
|
integrationsDependencies: [],
|
|
58
64
|
distDir: DEFAULT_ECOPAGES_DIST_DIR,
|
|
@@ -74,7 +80,8 @@ class ConfigBuilder {
|
|
|
74
80
|
publicDir: "",
|
|
75
81
|
srcDir: "",
|
|
76
82
|
htmlTemplatePath: "",
|
|
77
|
-
error404TemplatePath: ""
|
|
83
|
+
error404TemplatePath: "",
|
|
84
|
+
error500TemplatePath: ""
|
|
78
85
|
},
|
|
79
86
|
processors: /* @__PURE__ */ new Map(),
|
|
80
87
|
loaders: /* @__PURE__ */ new Map(),
|
|
@@ -201,6 +208,31 @@ class ConfigBuilder {
|
|
|
201
208
|
this.config.robotsTxt = robotsTxt;
|
|
202
209
|
return this;
|
|
203
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Sets automatic sitemap.xml generation during static export.
|
|
213
|
+
*
|
|
214
|
+
* @remarks
|
|
215
|
+
* Merged over `{ enabled: false, fileName: 'sitemap.xml', extraUrls: [], exclude: [] }`.
|
|
216
|
+
* Existing apps stay unchanged until `enabled: true` is set. Page-level
|
|
217
|
+
* `metadata.robots.index: false` and metadata resolution failures are handled
|
|
218
|
+
* during static generation (not by this config merge).
|
|
219
|
+
*/
|
|
220
|
+
setSitemap(sitemap) {
|
|
221
|
+
const defaults = {
|
|
222
|
+
enabled: false,
|
|
223
|
+
fileName: "sitemap.xml",
|
|
224
|
+
extraUrls: [],
|
|
225
|
+
exclude: []
|
|
226
|
+
};
|
|
227
|
+
this.config.sitemap = {
|
|
228
|
+
...defaults,
|
|
229
|
+
...this.config.sitemap,
|
|
230
|
+
...sitemap,
|
|
231
|
+
extraUrls: sitemap.extraUrls ?? this.config.sitemap.extraUrls ?? defaults.extraUrls,
|
|
232
|
+
exclude: sitemap.exclude ?? this.config.sitemap.exclude ?? defaults.exclude
|
|
233
|
+
};
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
204
236
|
/**
|
|
205
237
|
* Sets the integration plugins to use.
|
|
206
238
|
* These plugins provide additional functionality to the application.
|
|
@@ -381,11 +413,18 @@ class ConfigBuilder {
|
|
|
381
413
|
error404TemplatePath: this.resolveSemanticTemplatePath({
|
|
382
414
|
dirPath: absolutePagesDir,
|
|
383
415
|
basename: "404"
|
|
416
|
+
}),
|
|
417
|
+
error500TemplatePath: this.resolveSemanticTemplatePath({
|
|
418
|
+
dirPath: absolutePagesDir,
|
|
419
|
+
basename: "500"
|
|
384
420
|
})
|
|
385
421
|
};
|
|
386
422
|
return this;
|
|
387
423
|
}
|
|
388
|
-
resolveSemanticTemplatePath({
|
|
424
|
+
resolveSemanticTemplatePath({
|
|
425
|
+
dirPath,
|
|
426
|
+
basename
|
|
427
|
+
}) {
|
|
389
428
|
const extensions = this.config.templatesExt.length > 0 ? this.config.templatesExt : [".ghtml.ts"];
|
|
390
429
|
const matches = extensions.map((extension) => path.join(dirPath, `${basename}${extension}`)).filter((candidate) => fileSystem.exists(candidate));
|
|
391
430
|
invariant(matches.length <= 1, CONFIG_BUILDER_ERRORS.duplicateSemanticTemplate(basename, matches));
|
package/src/config/constants.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HtmlDocumentContribution } from '../../../services/html/html-transformer.service.js';
|
|
2
|
+
import type { PageMetadataProps, PageRobotsMetadata } from '../../../types/public-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds a head contribution for non-default `metadata.robots` values.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* When `robots` is omitted or matches the default `index,follow` behaviour,
|
|
8
|
+
* no tag is emitted (Next.js parity). Otherwise a single
|
|
9
|
+
* `<meta name="robots" content="...">` contribution is returned.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildRobotsMetaContribution(metadata: Pick<PageMetadataProps, 'robots'>): HtmlDocumentContribution | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Formats robots directives for a meta tag, or `undefined` when defaults apply.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatRobotsMetaContent(robots: PageRobotsMetadata | undefined): string | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { escapeHtmlAttribute } from "../../../utils/html-escaping.js";
|
|
2
|
+
function buildRobotsMetaContribution(metadata) {
|
|
3
|
+
const content = formatRobotsMetaContent(metadata.robots);
|
|
4
|
+
if (!content) {
|
|
5
|
+
return void 0;
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
placement: "head-append",
|
|
9
|
+
html: `<meta name="robots" content="${escapeHtmlAttribute(content)}">`
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function formatRobotsMetaContent(robots) {
|
|
13
|
+
if (!robots) {
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
const index = robots.index !== false;
|
|
17
|
+
const follow = robots.follow !== false;
|
|
18
|
+
const directives = [];
|
|
19
|
+
if (!index) {
|
|
20
|
+
directives.push("noindex");
|
|
21
|
+
}
|
|
22
|
+
if (!follow) {
|
|
23
|
+
directives.push("nofollow");
|
|
24
|
+
}
|
|
25
|
+
if (robots.nocache) {
|
|
26
|
+
directives.push("nocache");
|
|
27
|
+
}
|
|
28
|
+
if (directives.length === 0) {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
return directives.join(", ");
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
buildRobotsMetaContribution,
|
|
35
|
+
formatRobotsMetaContent
|
|
36
|
+
};
|
package/src/route-renderer/orchestration/route-pipeline/route-html-finalization.service.d.ts
CHANGED
|
@@ -12,5 +12,9 @@ export type RouteHtmlFinalizationContext<C> = {
|
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
14
|
* Builds the structural HTML finalization plan for one prepared route render.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Merges core robots meta contributions with integration-owned document
|
|
18
|
+
* contributions so page-level `metadata.robots` works across templates.
|
|
15
19
|
*/
|
|
16
20
|
export declare function buildRouteHtmlFinalization<C>(context: RouteHtmlFinalizationContext<C>): RouteHtmlFinalization;
|