@ecopages/core 0.2.0-beta.6 → 0.2.0-beta.8
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 +26 -2
- package/src/adapters/abstract/application-adapter.d.ts +12 -1
- package/src/adapters/abstract/application-adapter.js +13 -0
- package/src/adapters/abstract/server-adapter.d.ts +3 -0
- package/src/adapters/bun/client-bridge.d.ts +1 -0
- package/src/adapters/bun/client-bridge.js +3 -0
- package/src/adapters/bun/create-app.d.ts +2 -0
- package/src/adapters/bun/create-app.js +28 -9
- package/src/adapters/bun/server-adapter.d.ts +13 -0
- package/src/adapters/bun/server-adapter.js +29 -4
- package/src/adapters/create-app.d.ts +13 -0
- package/src/adapters/create-app.js +10 -1
- package/src/adapters/node/create-app.d.ts +1 -0
- package/src/adapters/node/create-app.js +17 -7
- package/src/adapters/node/node-client-bridge.d.ts +1 -0
- package/src/adapters/node/node-client-bridge.js +3 -0
- package/src/adapters/node/server-adapter.d.ts +10 -13
- package/src/adapters/node/server-adapter.js +37 -82
- package/src/adapters/node/static-content-server.d.ts +1 -0
- package/src/adapters/node/static-content-server.js +3 -0
- package/src/adapters/shared/copy-runtime-public-dir.d.ts +6 -0
- package/src/adapters/shared/copy-runtime-public-dir.js +24 -0
- package/src/adapters/shared/runtime-app-bootstrap.d.ts +0 -1
- package/src/adapters/shared/runtime-app-bootstrap.js +1 -6
- package/src/adapters/shared/server-static-builder.d.ts +4 -7
- package/src/adapters/shared/server-static-builder.js +89 -49
- package/src/adapters/shared/shared-hmr-manager.d.ts +1 -0
- package/src/adapters/shared/shared-hmr-manager.js +15 -2
- package/src/build/README.md +44 -4
- package/src/build/build-adapter.d.ts +8 -2
- package/src/build/build-adapter.js +71 -33
- package/src/build/build-input-fingerprint.d.ts +28 -0
- package/src/build/build-input-fingerprint.js +31 -0
- package/src/build/jsx-ownership-plugins.d.ts +10 -0
- package/src/build/jsx-ownership-plugins.js +17 -0
- package/src/build/pages-unified-graph-build.d.ts +31 -0
- package/src/build/pages-unified-graph-build.js +208 -0
- package/src/build/parallel-build-executor.d.ts +21 -0
- package/src/build/parallel-build-executor.js +52 -0
- package/src/build/rolldown-adapter-helpers.js +47 -3
- package/src/build/rolldown-build-adapter.js +2 -0
- package/src/build/rolldown-build-invocation-metrics.d.ts +12 -0
- package/src/build/rolldown-build-invocation-metrics.js +43 -0
- package/src/build/rolldown-dev-build-adapter.d.ts +2 -2
- package/src/build/rolldown-dev-build-adapter.js +30 -14
- package/src/build/runtime-build-executor.d.ts +7 -13
- package/src/build/runtime-build-executor.js +31 -7
- package/src/build/runtime-build-output-normalizer.d.ts +7 -0
- package/src/build/runtime-build-output-normalizer.js +85 -1
- package/src/build/server-entry-build-cache.d.ts +44 -0
- package/src/build/server-entry-build-cache.js +169 -0
- package/src/hmr/hmr.test.e2e.js +15 -4
- package/src/plugins/integration-plugin.d.ts +19 -8
- package/src/plugins/integration-plugin.js +7 -2
- package/src/plugins/processor.d.ts +5 -0
- package/src/plugins/processor.js +7 -0
- package/src/route-renderer/GRAPH.md +4 -4
- package/src/route-renderer/README.md +6 -5
- package/src/route-renderer/orchestration/foreign-subtree-execution.service.js +1 -3
- package/src/route-renderer/orchestration/integration-renderer.d.ts +10 -2
- package/src/route-renderer/orchestration/integration-renderer.js +32 -12
- package/src/route-renderer/orchestration/render-output.utils.d.ts +2 -0
- package/src/route-renderer/orchestration/render-output.utils.js +4 -0
- package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +2 -2
- package/src/route-renderer/orchestration/route-render-orchestrator.js +17 -16
- package/src/router/client/navigation-coordinator.js +10 -0
- package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.d.ts +1 -0
- package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.js +1 -0
- package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.d.ts +4 -2
- package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.js +27 -7
- package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +17 -3
- package/src/services/assets/browser-bundle.service.d.ts +1 -0
- package/src/services/assets/browser-bundle.service.js +9 -3
- package/src/services/html/html-transformer.service.d.ts +1 -1
- package/src/services/html/html-transformer.service.js +4 -4
- package/src/services/invalidation/development-invalidation.service.d.ts +10 -1
- package/src/services/invalidation/development-invalidation.service.js +29 -0
- package/src/services/module-loading/README.md +49 -0
- package/src/services/module-loading/app-server-module-transpiler.service.js +4 -17
- package/src/services/module-loading/page-module-import.service.d.ts +4 -1
- package/src/services/module-loading/page-module-import.service.js +52 -29
- package/src/services/module-loading/route-module-build-cache-registry.d.ts +24 -0
- package/src/services/module-loading/route-module-build-cache-registry.js +37 -0
- package/src/services/module-loading/route-module-build-cache.d.ts +4 -0
- package/src/services/module-loading/route-module-build-cache.js +38 -0
- package/src/services/module-loading/route-module-build-cache.store.d.ts +57 -0
- package/src/services/module-loading/route-module-build-cache.store.js +203 -0
- package/src/services/module-loading/route-module-build-manifest.d.ts +61 -0
- package/src/services/module-loading/route-module-build-manifest.js +124 -0
- package/src/services/module-loading/route-module-dependency-hasher.d.ts +81 -0
- package/src/services/module-loading/route-module-dependency-hasher.js +121 -0
- package/src/services/module-loading/server-module-transpiler.service.js +1 -1
- package/src/static-site-generator/README.md +33 -0
- package/src/static-site-generator/static-build-invalidation.d.ts +10 -0
- package/src/static-site-generator/static-build-invalidation.js +78 -0
- package/src/static-site-generator/static-export-context.d.ts +22 -0
- package/src/static-site-generator/static-export-context.js +0 -0
- package/src/static-site-generator/static-site-generator.d.ts +17 -20
- package/src/static-site-generator/static-site-generator.js +188 -85
- package/src/types/internal-types.d.ts +8 -0
- package/src/utils/parse-cli-args.d.ts +1 -0
- package/src/utils/parse-cli-args.js +3 -0
- package/src/watchers/project-watcher.d.ts +8 -0
- package/src/watchers/project-watcher.js +32 -12
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {} from "node:http";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileSystem } from "@ecopages/file-system";
|
|
4
4
|
import { getAppBrowserBuildPlugins, setupAppRuntimePlugins } from "../../build/build-adapter.js";
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isHtmlResponse,
|
|
21
21
|
shouldInjectHmrHtmlResponse
|
|
22
22
|
} from "../shared/hmr-html-response.js";
|
|
23
|
+
import { copyRuntimePublicDirIfChanged } from "../shared/copy-runtime-public-dir.js";
|
|
23
24
|
import { resolveServeRuntimeOrigin } from "../shared/runtime-app-bootstrap.js";
|
|
24
25
|
import { NodeClientAbortError, NodeHttpRequestBridge } from "./http-request-bridge.js";
|
|
25
26
|
import { NodeStaticPreviewHost } from "./static-preview-host.js";
|
|
@@ -32,6 +33,8 @@ class NodeServerAdapter extends SharedServerAdapter {
|
|
|
32
33
|
errorHandler;
|
|
33
34
|
bridge = null;
|
|
34
35
|
hmrManager = null;
|
|
36
|
+
projectWatcher = null;
|
|
37
|
+
adapterDisposed = false;
|
|
35
38
|
previewHost;
|
|
36
39
|
requestBridge;
|
|
37
40
|
devRuntimeFactory;
|
|
@@ -126,7 +129,7 @@ class NodeServerAdapter extends SharedServerAdapter {
|
|
|
126
129
|
prepareRuntimePublicDir() {
|
|
127
130
|
const srcPublicDir = path.join(this.appConfig.rootDir, this.appConfig.srcDir, this.appConfig.publicDir);
|
|
128
131
|
if (fileSystem.exists(srcPublicDir)) {
|
|
129
|
-
|
|
132
|
+
copyRuntimePublicDirIfChanged(srcPublicDir, path.join(this.appConfig.rootDir, this.appConfig.distDir));
|
|
130
133
|
}
|
|
131
134
|
fileSystem.ensureDir(path.join(this.appConfig.absolutePaths.distDir, RESOLVED_ASSETS_DIR));
|
|
132
135
|
}
|
|
@@ -139,20 +142,15 @@ class NodeServerAdapter extends SharedServerAdapter {
|
|
|
139
142
|
if (!this.initialized) {
|
|
140
143
|
await this.initialize();
|
|
141
144
|
}
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
);
|
|
153
|
-
} finally {
|
|
154
|
-
await this.stopBuildRuntimeServer(buildServer);
|
|
155
|
-
}
|
|
145
|
+
const baseUrl = resolveServeRuntimeOrigin(this.serveOptions);
|
|
146
|
+
await this.staticBuilder.build(
|
|
147
|
+
{ preview: false, baseUrl, force: options?.force },
|
|
148
|
+
{
|
|
149
|
+
router: this.router,
|
|
150
|
+
routeRendererFactory: this.routeRendererFactory,
|
|
151
|
+
staticRoutes: this.staticRoutes
|
|
152
|
+
}
|
|
153
|
+
);
|
|
156
154
|
if (!options?.preview) {
|
|
157
155
|
return;
|
|
158
156
|
}
|
|
@@ -165,71 +163,6 @@ class NodeServerAdapter extends SharedServerAdapter {
|
|
|
165
163
|
const previewPort = this.serveOptions.port || DEFAULT_ECOPAGES_PORT;
|
|
166
164
|
appLogger.info(`Preview running at http://${previewHostname}:${previewPort}`);
|
|
167
165
|
}
|
|
168
|
-
async startBuildRuntimeServer() {
|
|
169
|
-
const hostname = String(this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME);
|
|
170
|
-
const port = 0;
|
|
171
|
-
const server = createServer(async (req, res) => {
|
|
172
|
-
try {
|
|
173
|
-
const webRequest = this.requestBridge.createWebRequest(req, this.runtimeOrigin);
|
|
174
|
-
const response = await this.handleRequest(webRequest);
|
|
175
|
-
await this.requestBridge.sendNodeResponse(res, response);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
if (error instanceof NodeClientAbortError) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
appLogger.error("Node static build runtime request failed", error);
|
|
181
|
-
res.statusCode = 500;
|
|
182
|
-
res.end("Internal Server Error");
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
await new Promise((resolve, reject) => {
|
|
186
|
-
server.once("error", reject);
|
|
187
|
-
server.listen(port, hostname, () => {
|
|
188
|
-
server.off("error", reject);
|
|
189
|
-
resolve();
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
this.serverInstance = server;
|
|
193
|
-
appLogger.info(`Server running at ${this.getListeningServerOrigin(server)}`);
|
|
194
|
-
return server;
|
|
195
|
-
}
|
|
196
|
-
getListeningServerOrigin(server) {
|
|
197
|
-
const address = server.address();
|
|
198
|
-
const hostname = String(this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME);
|
|
199
|
-
if (!address || typeof address === "string") {
|
|
200
|
-
throw new Error("Build runtime server did not expose a numeric listening port");
|
|
201
|
-
}
|
|
202
|
-
return `http://${hostname}:${address.port}`;
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Gracefully shuts down the ephemeral build runtime server.
|
|
206
|
-
*
|
|
207
|
-
* `closeAllConnections()` is called *before* `close()` because `server.close()`
|
|
208
|
-
* only stops accepting new connections — it waits for existing keep-alive
|
|
209
|
-
* connections to finish naturally, which can stall the build indefinitely.
|
|
210
|
-
* `closeAllConnections()` force-closes any lingering sockets immediately so
|
|
211
|
-
* the `close()` callback fires promptly.
|
|
212
|
-
*
|
|
213
|
-
* The `NodeClientBridge` heartbeat is also destroyed here so its `setInterval`
|
|
214
|
-
* does not prevent the Node.js process from exiting cleanly after the build.
|
|
215
|
-
*/
|
|
216
|
-
async stopBuildRuntimeServer(server) {
|
|
217
|
-
await new Promise((resolve, reject) => {
|
|
218
|
-
server.close((error) => {
|
|
219
|
-
if (error) {
|
|
220
|
-
reject(error);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
resolve();
|
|
224
|
-
});
|
|
225
|
-
server.closeAllConnections();
|
|
226
|
-
});
|
|
227
|
-
if (this.serverInstance === server) {
|
|
228
|
-
this.serverInstance = null;
|
|
229
|
-
}
|
|
230
|
-
this.bridge?.destroy();
|
|
231
|
-
this.bridge = null;
|
|
232
|
-
}
|
|
233
166
|
async createAdapter() {
|
|
234
167
|
await this.initialize();
|
|
235
168
|
return {
|
|
@@ -237,9 +170,30 @@ class NodeServerAdapter extends SharedServerAdapter {
|
|
|
237
170
|
buildStatic: this.buildStatic.bind(this),
|
|
238
171
|
completeInitialization: this.completeInitialization.bind(this),
|
|
239
172
|
handleRequest: this.handleRequest.bind(this),
|
|
240
|
-
attachUserWebSocketUpgrades: this.attachUserWebSocketUpgrades.bind(this)
|
|
173
|
+
attachUserWebSocketUpgrades: this.attachUserWebSocketUpgrades.bind(this),
|
|
174
|
+
dispose: this.dispose.bind(this)
|
|
241
175
|
};
|
|
242
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Releases dev-time resources owned by the adapter.
|
|
179
|
+
*
|
|
180
|
+
* @remarks
|
|
181
|
+
* Safe to call multiple times. Does not stop the bound HTTP server — callers
|
|
182
|
+
* should shut down transport through the runtime host before disposing.
|
|
183
|
+
*/
|
|
184
|
+
async dispose() {
|
|
185
|
+
if (this.adapterDisposed) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.adapterDisposed = true;
|
|
189
|
+
await this.projectWatcher?.close();
|
|
190
|
+
this.projectWatcher = null;
|
|
191
|
+
this.hmrManager?.stop();
|
|
192
|
+
this.hmrManager = null;
|
|
193
|
+
this.bridge?.destroy();
|
|
194
|
+
this.bridge = null;
|
|
195
|
+
await this.previewHost.stop();
|
|
196
|
+
}
|
|
243
197
|
/**
|
|
244
198
|
* Handles a single incoming Web `Request` and returns a Web `Response`.
|
|
245
199
|
*
|
|
@@ -328,6 +282,7 @@ class NodeServerAdapter extends SharedServerAdapter {
|
|
|
328
282
|
hmrManager: this.hmrManager,
|
|
329
283
|
bridge: this.bridge
|
|
330
284
|
});
|
|
285
|
+
this.projectWatcher = watcher;
|
|
331
286
|
await watcher.createWatcherSubscription();
|
|
332
287
|
} else if (hasUserWs) {
|
|
333
288
|
this.wireUserWebSocketUpgrades(server);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copies a source public directory into the runtime dist dir, skipping unchanged files.
|
|
3
|
+
*
|
|
4
|
+
* Dev restarts call this on every boot; byte comparison avoids rewriting identical assets.
|
|
5
|
+
*/
|
|
6
|
+
export declare function copyRuntimePublicDirIfChanged(sourceDir: string, destinationDir: string): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileSystem } from "@ecopages/file-system";
|
|
4
|
+
function copyRuntimePublicDirIfChanged(sourceDir, destinationDir) {
|
|
5
|
+
fileSystem.ensureDir(destinationDir);
|
|
6
|
+
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
7
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
8
|
+
const destinationPath = path.join(destinationDir, entry.name);
|
|
9
|
+
if (entry.isDirectory()) {
|
|
10
|
+
copyRuntimePublicDirIfChanged(sourcePath, destinationPath);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (!entry.isFile()) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (fileSystem.exists(destinationPath) && Buffer.compare(fileSystem.readFileAsBuffer(sourcePath), fileSystem.readFileAsBuffer(destinationPath)) === 0) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
fileSystem.copyFile(sourcePath, destinationPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
copyRuntimePublicDirIfChanged
|
|
24
|
+
};
|
|
@@ -30,13 +30,8 @@ function resolveRuntimeBinding(options) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
function resolveStaticRuntimeMode(options) {
|
|
33
|
-
const requiresFetchRuntime = options.appConfig.integrations.some(
|
|
34
|
-
(integration) => integration.staticBuildStep === "fetch"
|
|
35
|
-
);
|
|
36
|
-
const canBuildWithoutRuntimeServer = (options.cliArgs.build || options.cliArgs.preview) && !requiresFetchRuntime;
|
|
37
33
|
return {
|
|
38
|
-
|
|
39
|
-
canBuildWithoutRuntimeServer
|
|
34
|
+
canBuildWithoutRuntimeServer: options.cliArgs.build || options.cliArgs.preview
|
|
40
35
|
};
|
|
41
36
|
}
|
|
42
37
|
export {
|
|
@@ -6,6 +6,7 @@ import type { StaticGenerationRendererResolver } from '../../route-renderer/rout
|
|
|
6
6
|
export interface StaticBuildOptions {
|
|
7
7
|
preview?: boolean;
|
|
8
8
|
baseUrl?: string;
|
|
9
|
+
force?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export interface ServeOptions {
|
|
11
12
|
hostname?: string;
|
|
@@ -55,24 +56,20 @@ export declare class ServerStaticBuilder {
|
|
|
55
56
|
private readonly previewServerFactory;
|
|
56
57
|
private readonly entryFile;
|
|
57
58
|
constructor({ appConfig, staticSiteGenerator, serveOptions, apiHandlers, logger, previewServerFactory, entryFile, }: ServerStaticBuilderParams);
|
|
58
|
-
private warnApiHandlersUnavailableInStaticMode;
|
|
59
59
|
private prepareExportDirectory;
|
|
60
60
|
private refreshRuntimeAssets;
|
|
61
61
|
/**
|
|
62
62
|
* Bundles the server entry file for production use.
|
|
63
63
|
*
|
|
64
64
|
* @remarks
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* `dist/{SERVER_BUNDLE_DIR}/{SERVER_BUNDLE_FILENAME}` and used by
|
|
69
|
-
* `ecopages start` in production.
|
|
65
|
+
* Every production build emits a runnable server entry for `ecopages start`,
|
|
66
|
+
* including static-only and websocket-only apps. Incremental builds may skip
|
|
67
|
+
* Rolldown when the `.eco/.server-entry` cache is still valid.
|
|
70
68
|
*
|
|
71
69
|
* Package imports remain external so native addons and runtime-owned
|
|
72
70
|
* dependencies continue to load through the app's installed
|
|
73
71
|
* `node_modules` tree. Only the app entry graph is bundled.
|
|
74
72
|
*
|
|
75
|
-
* Skips silently when no API endpoints are registered (static-only site).
|
|
76
73
|
* Throws if the build adapter is unavailable, is owned by a host
|
|
77
74
|
* runtime, or bundling fails.
|
|
78
75
|
*
|
|
@@ -3,8 +3,19 @@ import { fileSystem } from "@ecopages/file-system";
|
|
|
3
3
|
import { DEFAULT_ECOPAGES_HOSTNAME, DEFAULT_ECOPAGES_PORT } from "../../config/constants.js";
|
|
4
4
|
import { StaticContentServer } from "../../dev/sc-server.js";
|
|
5
5
|
import { appLogger } from "../../global/app-logger.js";
|
|
6
|
-
import { getAppBuildAdapter } from "../../build/build-adapter.js";
|
|
7
|
-
import {
|
|
6
|
+
import { build, getAppBuildAdapter } from "../../build/build-adapter.js";
|
|
7
|
+
import {
|
|
8
|
+
getServerBundleOutputPaths,
|
|
9
|
+
lookupServerEntryBuildCache,
|
|
10
|
+
recordServerEntryBuildCache,
|
|
11
|
+
writeServerBundleDeployManifest
|
|
12
|
+
} from "../../build/server-entry-build-cache.js";
|
|
13
|
+
import { getInstalledServerEntryBuildExecutor } from "../../build/runtime-build-executor.js";
|
|
14
|
+
import {
|
|
15
|
+
clearProductionBuildCaches,
|
|
16
|
+
shouldResetStaticExportDirectory
|
|
17
|
+
} from "../../static-site-generator/static-build-invalidation.js";
|
|
18
|
+
import { resolveEntryFile, SERVER_BUNDLE_FILENAME } from "../../utils/resolve-entry-file.js";
|
|
8
19
|
class ServerStaticBuilder {
|
|
9
20
|
appConfig;
|
|
10
21
|
staticSiteGenerator;
|
|
@@ -30,24 +41,19 @@ class ServerStaticBuilder {
|
|
|
30
41
|
this.previewServerFactory = previewServerFactory ?? StaticContentServer;
|
|
31
42
|
this.entryFile = resolveEntryFile({ entryFile });
|
|
32
43
|
}
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
44
|
+
prepareExportDirectory(force) {
|
|
45
|
+
if (force) {
|
|
46
|
+
clearProductionBuildCaches(this.appConfig);
|
|
36
47
|
}
|
|
37
|
-
const uniqueHandlers = Array.from(
|
|
38
|
-
new Set(this.apiHandlers.map((handler) => `${handler.method} ${handler.path}`))
|
|
39
|
-
);
|
|
40
|
-
const visibleHandlers = uniqueHandlers.slice(0, 5).join(", ");
|
|
41
|
-
const remainingCount = uniqueHandlers.length - Math.min(uniqueHandlers.length, 5);
|
|
42
|
-
const summary = remainingCount > 0 ? `${visibleHandlers}, +${remainingCount} more` : visibleHandlers;
|
|
43
|
-
this.logger.warn(
|
|
44
|
-
"Registered API endpoints are not available in static build or preview modes because no server runtime is started. They are excluded from the generated output.\n",
|
|
45
|
-
`\u27A4 ${summary}`
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
prepareExportDirectory() {
|
|
49
48
|
const exportDir = this.appConfig.absolutePaths?.distDir ?? path.join(this.appConfig.rootDir, this.appConfig.distDir);
|
|
50
|
-
|
|
49
|
+
const shouldCleanDist = shouldResetStaticExportDirectory(this.appConfig, force);
|
|
50
|
+
fileSystem.ensureDir(exportDir, shouldCleanDist);
|
|
51
|
+
if (shouldCleanDist) {
|
|
52
|
+
this.appConfig.runtime = {
|
|
53
|
+
...this.appConfig.runtime ?? {},
|
|
54
|
+
runtimeAssetsPrepared: false
|
|
55
|
+
};
|
|
56
|
+
}
|
|
51
57
|
const srcPublicDir = path.join(
|
|
52
58
|
this.appConfig.rootDir,
|
|
53
59
|
this.appConfig.srcDir ?? "src",
|
|
@@ -56,39 +62,47 @@ class ServerStaticBuilder {
|
|
|
56
62
|
if (fileSystem.exists(srcPublicDir)) {
|
|
57
63
|
fileSystem.copyDir(srcPublicDir, exportDir);
|
|
58
64
|
}
|
|
65
|
+
return !shouldCleanDist;
|
|
59
66
|
}
|
|
60
67
|
async refreshRuntimeAssets() {
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
if (this.appConfig.runtime?.runtimeAssetsPrepared) {
|
|
69
|
+
appLogger.debug("Skipped refreshRuntimeAssets: runtime assets already prepared");
|
|
70
|
+
return;
|
|
63
71
|
}
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
appLogger.debugTime("refreshRuntimeAssets");
|
|
73
|
+
try {
|
|
74
|
+
for (const processor of this.appConfig.processors.values()) {
|
|
75
|
+
await processor.setup();
|
|
76
|
+
}
|
|
77
|
+
for (const integration of this.appConfig.integrations) {
|
|
78
|
+
await integration.setup();
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
appLogger.debugTimeEnd("refreshRuntimeAssets");
|
|
66
82
|
}
|
|
83
|
+
this.appConfig.runtime = {
|
|
84
|
+
...this.appConfig.runtime ?? {},
|
|
85
|
+
runtimeAssetsPrepared: true
|
|
86
|
+
};
|
|
67
87
|
}
|
|
68
88
|
/**
|
|
69
89
|
* Bundles the server entry file for production use.
|
|
70
90
|
*
|
|
71
91
|
* @remarks
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* `dist/{SERVER_BUNDLE_DIR}/{SERVER_BUNDLE_FILENAME}` and used by
|
|
76
|
-
* `ecopages start` in production.
|
|
92
|
+
* Every production build emits a runnable server entry for `ecopages start`,
|
|
93
|
+
* including static-only and websocket-only apps. Incremental builds may skip
|
|
94
|
+
* Rolldown when the `.eco/.server-entry` cache is still valid.
|
|
77
95
|
*
|
|
78
96
|
* Package imports remain external so native addons and runtime-owned
|
|
79
97
|
* dependencies continue to load through the app's installed
|
|
80
98
|
* `node_modules` tree. Only the app entry graph is bundled.
|
|
81
99
|
*
|
|
82
|
-
* Skips silently when no API endpoints are registered (static-only site).
|
|
83
100
|
* Throws if the build adapter is unavailable, is owned by a host
|
|
84
101
|
* runtime, or bundling fails.
|
|
85
102
|
*
|
|
86
103
|
* @throws If the build adapter is unavailable, is host-owned, or bundling fails.
|
|
87
104
|
*/
|
|
88
|
-
async bundleServerEntry() {
|
|
89
|
-
if (this.apiHandlers.length === 0) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
105
|
+
async bundleServerEntry(options) {
|
|
92
106
|
const buildAdapter = getAppBuildAdapter(this.appConfig);
|
|
93
107
|
if (buildAdapter.ownership === "vite-host") {
|
|
94
108
|
throw new Error(
|
|
@@ -101,24 +115,49 @@ class ServerStaticBuilder {
|
|
|
101
115
|
`Cannot bundle server entry: file "${this.entryFile}" not found in "${this.appConfig.rootDir}".`
|
|
102
116
|
);
|
|
103
117
|
}
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
outdir: serverOutdir,
|
|
110
|
-
naming: SERVER_BUNDLE_FILENAME,
|
|
111
|
-
target: "node",
|
|
112
|
-
format: "esm",
|
|
113
|
-
sourcemap: "hidden",
|
|
114
|
-
externalPackages: true,
|
|
115
|
-
root: this.appConfig.rootDir
|
|
118
|
+
const { serverOutdir, serverEntryPath } = getServerBundleOutputPaths(this.appConfig);
|
|
119
|
+
const cached = lookupServerEntryBuildCache({
|
|
120
|
+
appConfig: this.appConfig,
|
|
121
|
+
entryPath,
|
|
122
|
+
force: options?.force
|
|
116
123
|
});
|
|
124
|
+
if (cached) {
|
|
125
|
+
const hasBundle = cached.outputPaths.some(
|
|
126
|
+
(outputPath) => path.resolve(outputPath) === path.resolve(serverEntryPath) && fileSystem.exists(outputPath)
|
|
127
|
+
);
|
|
128
|
+
if (hasBundle) {
|
|
129
|
+
this.logger.info("Reusing cached server entry bundle");
|
|
130
|
+
writeServerBundleDeployManifest(this.appConfig, serverEntryPath);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
this.logger.info("Bundling server entry file...");
|
|
135
|
+
const result = await build(
|
|
136
|
+
{
|
|
137
|
+
entrypoints: [entryPath],
|
|
138
|
+
outdir: serverOutdir,
|
|
139
|
+
naming: SERVER_BUNDLE_FILENAME,
|
|
140
|
+
target: "node",
|
|
141
|
+
format: "esm",
|
|
142
|
+
sourcemap: "hidden",
|
|
143
|
+
externalPackages: true,
|
|
144
|
+
root: this.appConfig.rootDir
|
|
145
|
+
},
|
|
146
|
+
getInstalledServerEntryBuildExecutor(this.appConfig)
|
|
147
|
+
);
|
|
117
148
|
if (!result.success) {
|
|
118
149
|
const errorMessages = result.logs.map((log) => log.message).join("\n");
|
|
119
150
|
throw new Error(`Failed to bundle server entry file:
|
|
120
151
|
${errorMessages}`);
|
|
121
152
|
}
|
|
153
|
+
const outputPaths = result.outputs.length > 0 ? result.outputs.map((output) => output.path) : fileSystem.exists(serverEntryPath) ? [serverEntryPath] : [];
|
|
154
|
+
recordServerEntryBuildCache({
|
|
155
|
+
appConfig: this.appConfig,
|
|
156
|
+
entryPath,
|
|
157
|
+
buildResult: result,
|
|
158
|
+
outputPaths
|
|
159
|
+
});
|
|
160
|
+
writeServerBundleDeployManifest(this.appConfig, serverEntryPath);
|
|
122
161
|
this.logger.info("Server entry file bundled successfully");
|
|
123
162
|
}
|
|
124
163
|
/**
|
|
@@ -129,17 +168,18 @@ ${errorMessages}`);
|
|
|
129
168
|
* @param dependencies.staticRoutes - Explicit static routes registered via app.static()
|
|
130
169
|
*/
|
|
131
170
|
async build(options, dependencies) {
|
|
132
|
-
const { preview = false, baseUrl: explicitBaseUrl } = options ?? {};
|
|
171
|
+
const { preview = false, baseUrl: explicitBaseUrl, force = false } = options ?? {};
|
|
133
172
|
const baseUrl = explicitBaseUrl ?? `http://${this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME}:${this.serveOptions.port || DEFAULT_ECOPAGES_PORT}`;
|
|
134
|
-
this.
|
|
135
|
-
this.prepareExportDirectory();
|
|
173
|
+
const preserveExportDirectory = this.prepareExportDirectory(force);
|
|
136
174
|
await this.refreshRuntimeAssets();
|
|
137
|
-
await this.bundleServerEntry();
|
|
175
|
+
await this.bundleServerEntry({ force });
|
|
138
176
|
await this.staticSiteGenerator.run({
|
|
139
177
|
router: dependencies.router,
|
|
140
178
|
baseUrl,
|
|
141
179
|
routeRendererFactory: dependencies.routeRendererFactory,
|
|
142
|
-
staticRoutes: dependencies.staticRoutes
|
|
180
|
+
staticRoutes: dependencies.staticRoutes,
|
|
181
|
+
force,
|
|
182
|
+
preserveExportDirectory
|
|
143
183
|
});
|
|
144
184
|
if (!preview) {
|
|
145
185
|
this.logger.info("Build completed");
|
|
@@ -56,5 +56,6 @@ export declare abstract class SharedHmrManager implements IHmrManager {
|
|
|
56
56
|
protected emitStrictEntrypoint(entrypointPath: string): Promise<void>;
|
|
57
57
|
protected emitScriptEntrypoint(entrypointPath: string, outputPath: string): Promise<void>;
|
|
58
58
|
stop(): void;
|
|
59
|
+
[Symbol.dispose](): void;
|
|
59
60
|
}
|
|
60
61
|
export {};
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
|
|
5
|
-
import {
|
|
5
|
+
import { getAppHmrBuildExecutor } from "../../build/build-adapter.js";
|
|
6
6
|
import { fileSystem } from "@ecopages/file-system";
|
|
7
7
|
import { HmrStrategyType } from "../../hmr/hmr-strategy.js";
|
|
8
8
|
import { DefaultHmrStrategy } from "../../hmr/strategies/default-hmr-strategy.js";
|
|
@@ -193,7 +193,7 @@ class SharedHmrManager {
|
|
|
193
193
|
getSrcDir: () => this.appConfig.absolutePaths.srcDir,
|
|
194
194
|
getLayoutsDir: () => this.appConfig.absolutePaths.layoutsDir,
|
|
195
195
|
getPagesDir: () => this.appConfig.absolutePaths.pagesDir,
|
|
196
|
-
getBuildExecutor: () =>
|
|
196
|
+
getBuildExecutor: () => getAppHmrBuildExecutor(this.appConfig),
|
|
197
197
|
getBrowserBundleService: () => this.browserBundleService,
|
|
198
198
|
getEntrypointDependencyGraph: () => this.entrypointDependencyGraph,
|
|
199
199
|
importServerModule: async (filePath) => await this.serverModuleTranspiler.importModule({
|
|
@@ -238,6 +238,16 @@ class SharedHmrManager {
|
|
|
238
238
|
appLogger.error(`[HMR] Generic script entrypoint build failed for ${entrypointPath}:`, buildResult.logs);
|
|
239
239
|
return;
|
|
240
240
|
}
|
|
241
|
+
if (!fileSystem.exists(outputPath) && buildResult.outputs.length > 0) {
|
|
242
|
+
const resolvedOutputPath = path.resolve(outputPath);
|
|
243
|
+
const emittedOutput = buildResult.outputs.find((output) => path.resolve(output.path) === resolvedOutputPath)?.path ?? buildResult.outputs.find((output) => path.basename(output.path) === path.basename(outputPath))?.path;
|
|
244
|
+
if (emittedOutput && fileSystem.exists(emittedOutput)) {
|
|
245
|
+
fileSystem.ensureDir(path.dirname(outputPath));
|
|
246
|
+
if (path.resolve(emittedOutput) !== resolvedOutputPath) {
|
|
247
|
+
fileSystem.copyFile(emittedOutput, outputPath);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
241
251
|
const entrypointDependencies = buildResult.dependencyGraph?.entrypoints?.[entrypointPath];
|
|
242
252
|
if (entrypointDependencies) {
|
|
243
253
|
this.entrypointDependencyGraph.setEntrypointDependencies(entrypointPath, entrypointDependencies);
|
|
@@ -253,6 +263,9 @@ class SharedHmrManager {
|
|
|
253
263
|
this.entrypointDependencyGraph.reset();
|
|
254
264
|
this.plugins = [];
|
|
255
265
|
}
|
|
266
|
+
[Symbol.dispose]() {
|
|
267
|
+
this.stop();
|
|
268
|
+
}
|
|
256
269
|
}
|
|
257
270
|
export {
|
|
258
271
|
SharedHmrManager,
|
package/src/build/README.md
CHANGED
|
@@ -10,7 +10,8 @@ Three concentric shapes, plus a serializer and a plugin injector:
|
|
|
10
10
|
| -------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
11
|
| `BuildAdapter` | `build-adapter.ts` | Low-level backend. Two implementations: the bundled adapter (the real bundler) and `ViteHostBuildAdapter` (a host-owned boundary marker that throws on direct use). |
|
|
12
12
|
| `BuildExecutor` | `build-adapter.ts` | Narrower runtime facade. Only `build` is exposed. Stored on `appConfig.runtime.buildExecutor`. |
|
|
13
|
-
| `SerializedBuildExecutor` | `serialized-build-executor.ts` | FIFO queue around any `BuildExecutor`. Used
|
|
13
|
+
| `SerializedBuildExecutor` | `serialized-build-executor.ts` | FIFO queue around any `BuildExecutor`. Used when strict single-flight ordering is required. |
|
|
14
|
+
| `ParallelBuildExecutor` | `parallel-build-executor.ts` | Concurrency-limited wrapper for independent route-module and HMR browser builds. |
|
|
14
15
|
| `withBuildExecutorPlugins` | `build-adapter.ts` | Merges app-owned plugins into every `build` call. The single point of plugin injection. |
|
|
15
16
|
|
|
16
17
|
Plus one translation bridge:
|
|
@@ -24,7 +25,8 @@ Plus one translation bridge:
|
|
|
24
25
|
- `rolldown-build-adapter.ts`: the production `BuildAdapter`. Wraps the bundler and exposes a normalized `BuildResult` (outputs, dependency graph, logs).
|
|
25
26
|
- `rolldown-plugin-bridge.ts`: `EcoBuildPlugin[]` → bundler-plugin translation.
|
|
26
27
|
- `serialized-build-executor.ts`: FIFO queue primitive.
|
|
27
|
-
- `runtime-build-executor.ts`:
|
|
28
|
+
- `runtime-build-executor.ts`: server-adapter entrypoint that wraps the app-owned adapter in `ParallelBuildExecutor` layers plus `withBuildExecutorPlugins`. Server-entry bundling uses `getInstalledServerEntryBuildExecutor()` (serialized, single-flight, cached on `appConfig.runtime.serverEntryBuildExecutor`).
|
|
29
|
+
- `server-entry-build-cache.ts`: production server-entry bundle cache (`.eco/.server-entry/.build-cache.json` + `dist/.server/manifest.json`).
|
|
28
30
|
- `*.test.ts`: regression coverage.
|
|
29
31
|
|
|
30
32
|
## Default Flow
|
|
@@ -35,8 +37,10 @@ When a server adapter initializes, it calls `installAppRuntimeBuildExecutor(appC
|
|
|
35
37
|
|
|
36
38
|
```
|
|
37
39
|
BuildExecutor
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
├─ ParallelBuildExecutor (route modules) // limited concurrency
|
|
41
|
+
├─ ParallelBuildExecutor (HMR browser) // separate pool, max 3
|
|
42
|
+
└─ SerializedBuildExecutor (server entry) // single-flight when used directly
|
|
43
|
+
└─ withBuildExecutorPlugins
|
|
40
44
|
└─ BuildAdapter (bundled adapter or Vite-host)
|
|
41
45
|
```
|
|
42
46
|
|
|
@@ -81,6 +85,42 @@ Plugin ordering: the bundler's `resolveId` and `load` are "first" hooks. The bri
|
|
|
81
85
|
|
|
82
86
|
These fields are kept in the type so existing call-sites compile. The proper fix is a more focused `BuildOptions` schema in a follow-up.
|
|
83
87
|
|
|
88
|
+
## Dev / watch path
|
|
89
|
+
|
|
90
|
+
`installAppRuntimeBuildExecutor` wraps the app-owned `BuildAdapter` in two `ParallelBuildExecutor` pools (route modules and HMR browser scripts). Both pools share one plugin-wrapped inner executor today.
|
|
91
|
+
|
|
92
|
+
`RolldownDevBuildAdapter` (DevEngine-backed incremental rebuilds) is available via `createBuildAdapter({ ownership: 'rolldown-dev' })` but is **not** yet selected by `installAppRuntimeBuildExecutor`. HMR browser bundles in `browser-bundle.service.ts` may construct a dedicated dev adapter where needed.
|
|
93
|
+
|
|
94
|
+
`ProjectWatcher` deduplicates watch roots, ignores `.eco/` and `dist/`, and coalesces duplicate chokidar events within 150ms.
|
|
95
|
+
|
|
96
|
+
## Production build caches
|
|
97
|
+
|
|
98
|
+
Two persisted cache layers accelerate production builds. Both use `.build-cache.json` manifests keyed by dependency hashes and a build-inputs fingerprint.
|
|
99
|
+
|
|
100
|
+
| Cache | On-disk location | Module |
|
|
101
|
+
| -------------------------------------- | --------------------------------------------------- | ----------------------------------- |
|
|
102
|
+
| Server-entry bundle | `.eco/.server-entry/.build-cache.json` | `server-entry-build-cache.ts` |
|
|
103
|
+
| Route-module transpile + static render | `<server-outdir>/.server-modules/.build-cache.json` | `route-module-build-cache.store.ts` |
|
|
104
|
+
|
|
105
|
+
`getInstalledServerEntryBuildExecutor()` returns one `SerializedBuildExecutor` per app instance (stored on `appConfig.runtime`). `clearProductionBuildCaches()` wipes both manifest trees and resets in-memory route-module state.
|
|
106
|
+
|
|
107
|
+
The route-module registry (`route-module-build-cache-registry.ts`) shares one `RouteModuleBuildCache` per `(app, outdir)` pair. Legacy `.server-route-modules` outdirs are still read for migration but new writes go to `.server-modules`.
|
|
108
|
+
|
|
109
|
+
## Unified pages graph
|
|
110
|
+
|
|
111
|
+
Production static exports compile all template pages in one Rolldown invocation when `shouldBuildPagesUnifiedGraph()` is true (default in production; opt out with `ECOPAGES_UNIFIED_PAGES_GRAPH=0`).
|
|
112
|
+
|
|
113
|
+
| Artifact | Location |
|
|
114
|
+
| -------------- | ----------------------------------------------------- |
|
|
115
|
+
| Graph manifest | `.eco/.server-pages-graph/.build-cache.json` |
|
|
116
|
+
| Chunk outputs | `.eco/.server-modules/` (shared with per-route cache) |
|
|
117
|
+
|
|
118
|
+
`StaticSiteGenerator` calls `ensurePagesUnifiedGraphBuilt()` before the export loop. `PageModuleImportService` imports prebuilt chunks via `importPagesUnifiedGraphModule()` and falls back to per-page Rolldown on miss.
|
|
119
|
+
|
|
120
|
+
`ECOPAGES_ROLLDOWN_BUILD_METRICS=1` enables `rolldown-build-invocation-metrics.ts` counters used by bench and parity tests.
|
|
121
|
+
|
|
122
|
+
Build-input fingerprinting lives in `build-input-fingerprint.ts` and is shared with server-entry cache, unified pages graph, and static-render invalidation.
|
|
123
|
+
|
|
84
124
|
## Testing Strategy
|
|
85
125
|
|
|
86
126
|
- `rolldown-build-adapter.test.ts` covers the adapter's `build`, `resolve`, `getTranspileOptions`, and dependency-graph extraction end-to-end.
|
|
@@ -476,12 +476,18 @@ export declare function getAppBrowserBuildPlugins(appConfig: EcoPagesAppConfig):
|
|
|
476
476
|
* @remarks
|
|
477
477
|
* Falls back to {@link getAppBuildAdapter} when no executor is set
|
|
478
478
|
* on the runtime yet. The dev-watch pipeline replaces this value with
|
|
479
|
-
* a
|
|
479
|
+
* a parallel route-module executor via
|
|
480
480
|
* {@link installAppRuntimeBuildExecutor}.
|
|
481
481
|
*/
|
|
482
482
|
export declare function getAppBuildExecutor(appConfig: EcoPagesAppConfig): BuildExecutor;
|
|
483
|
-
/**
|
|
483
|
+
/** Returns the HMR browser-bundle executor when installed. */
|
|
484
|
+
export declare function getAppHmrBuildExecutor(appConfig: EcoPagesAppConfig): BuildExecutor;
|
|
485
|
+
/** Route-module executor (page imports, transpile). Falls back to {@link getAppBuildExecutor}. */
|
|
486
|
+
export declare function getAppRouteModuleBuildExecutor(appConfig: EcoPagesAppConfig): BuildExecutor;
|
|
487
|
+
/** Installs the default executor for one app instance (ConfigBuilder / tests). */
|
|
484
488
|
export declare function setAppBuildExecutor(appConfig: EcoPagesAppConfig, buildExecutor: BuildExecutor): void;
|
|
489
|
+
export declare function setAppHmrBuildExecutor(appConfig: EcoPagesAppConfig, buildExecutor: BuildExecutor): void;
|
|
490
|
+
export declare function setAppRouteModuleBuildExecutor(appConfig: EcoPagesAppConfig, buildExecutor: BuildExecutor): void;
|
|
485
491
|
/**
|
|
486
492
|
* Runs a build through the active pipeline.
|
|
487
493
|
*
|