@ecopages/core 0.2.0-beta.7 → 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 -6
- package/src/adapters/shared/server-static-builder.js +90 -34
- 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 +5 -0
- 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/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 -8
- package/src/static-site-generator/static-site-generator.js +176 -62
- 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
|
@@ -10,15 +10,12 @@ import { OwnershipValidationService } from "./ownership-validation.service.js";
|
|
|
10
10
|
import {
|
|
11
11
|
RouteRenderOrchestrator
|
|
12
12
|
} from "./route-render-orchestrator.js";
|
|
13
|
-
import { normalizeUnresolvedMarkerArtifactHtml } from "./render-output.utils.js";
|
|
13
|
+
import { normalizeUnresolvedMarkerArtifactHtml, isMarkupNodeLike } from "./render-output.utils.js";
|
|
14
14
|
import {
|
|
15
15
|
ForeignSubtreeExecutionService
|
|
16
16
|
} from "./foreign-subtree-execution.service.js";
|
|
17
17
|
import {} from "./queued-foreign-subtree-resolution.service.js";
|
|
18
18
|
import { buildProcessedAssetDedupeKey } from "./processed-asset-dedupe.js";
|
|
19
|
-
function isMarkupNodeLike(value) {
|
|
20
|
-
return typeof value === "object" && value !== null && "nodeType" in value && typeof value.nodeType === "number" && "outerHTML" in value && typeof value.outerHTML === "string";
|
|
21
|
-
}
|
|
22
19
|
class IntegrationRenderer {
|
|
23
20
|
appConfig;
|
|
24
21
|
assetProcessingService;
|
|
@@ -31,7 +28,22 @@ class IntegrationRenderer {
|
|
|
31
28
|
pageModuleLoaderService;
|
|
32
29
|
routeRenderOrchestrator;
|
|
33
30
|
foreignSubtreeExecutionService = new ForeignSubtreeExecutionService();
|
|
31
|
+
/**
|
|
32
|
+
* Serializes route and view renders that mutate `htmlTransformer` state.
|
|
33
|
+
*
|
|
34
|
+
* Integration renderers are cached per integration, so concurrent static builds
|
|
35
|
+
* and overlapping SSR requests must not share one transformer page package.
|
|
36
|
+
*/
|
|
37
|
+
renderExclusiveChain = Promise.resolve();
|
|
34
38
|
DOC_TYPE = "<!DOCTYPE html>";
|
|
39
|
+
runRenderExclusive(operation) {
|
|
40
|
+
const run = this.renderExclusiveChain.then(operation, operation);
|
|
41
|
+
this.renderExclusiveChain = run.then(
|
|
42
|
+
() => void 0,
|
|
43
|
+
() => void 0
|
|
44
|
+
);
|
|
45
|
+
return run;
|
|
46
|
+
}
|
|
35
47
|
/**
|
|
36
48
|
* Loads one route module through the owning renderer's import path.
|
|
37
49
|
*
|
|
@@ -397,7 +409,7 @@ class IntegrationRenderer {
|
|
|
397
409
|
* @returns Structured component render result for orchestration paths.
|
|
398
410
|
*/
|
|
399
411
|
async renderStringComponentWithSerializedChildren(input, component) {
|
|
400
|
-
const serializedChildren = input.children === void 0 ? void 0 : typeof input.children === "string" ? input.children : isMarkupNodeLike(input.children) ? input.children.outerHTML : void 0;
|
|
412
|
+
const serializedChildren = input.children === void 0 ? void 0 : typeof input.children === "string" ? input.children : isMarkupNodeLike(input.children) && typeof input.children.outerHTML === "string" ? input.children.outerHTML : void 0;
|
|
401
413
|
if (input.children !== void 0 && serializedChildren === void 0) {
|
|
402
414
|
const componentFile = input.component.config?.__eco?.file ?? "unknown component";
|
|
403
415
|
const childTag = Object.prototype.toString.call(input.children);
|
|
@@ -533,7 +545,7 @@ class IntegrationRenderer {
|
|
|
533
545
|
* @returns The imported module.
|
|
534
546
|
*/
|
|
535
547
|
async importPageFile(file, options) {
|
|
536
|
-
const bypassCache = options?.bypassCache ??
|
|
548
|
+
const bypassCache = options?.bypassCache ?? false;
|
|
537
549
|
const pageModule = this.usesIntegrationPageImporter(file) ? await this.importIntegrationPageFile(file, {
|
|
538
550
|
bypassCache,
|
|
539
551
|
cacheScope: options?.cacheScope
|
|
@@ -633,7 +645,7 @@ class IntegrationRenderer {
|
|
|
633
645
|
resolveRoutePageComponentRender: (input) => this.resolveRoutePageComponentRender(input),
|
|
634
646
|
renderRouteBody: (renderOptions) => this.renderRouteBody(renderOptions),
|
|
635
647
|
getRouteHtmlFinalization: (renderOptions) => this.getRouteHtmlFinalization(renderOptions),
|
|
636
|
-
transformRouteResponse: (response, htmlContributions) => this.transformRouteResponse(response, htmlContributions)
|
|
648
|
+
transformRouteResponse: (response, htmlContributions, pagePackage) => this.transformRouteResponse(response, htmlContributions, pagePackage)
|
|
637
649
|
};
|
|
638
650
|
}
|
|
639
651
|
async resolveRouteRenderInputs(routeOptions) {
|
|
@@ -706,8 +718,13 @@ class IntegrationRenderer {
|
|
|
706
718
|
}
|
|
707
719
|
};
|
|
708
720
|
}
|
|
709
|
-
async transformRouteResponse(response, htmlContributions) {
|
|
710
|
-
const
|
|
721
|
+
async transformRouteResponse(response, htmlContributions, pagePackage) {
|
|
722
|
+
const resolvedPagePackage = this.htmlTransformer.getPagePackage() ?? pagePackage;
|
|
723
|
+
const transformedResponse = await this.htmlTransformer.transform(
|
|
724
|
+
response,
|
|
725
|
+
htmlContributions,
|
|
726
|
+
resolvedPagePackage
|
|
727
|
+
);
|
|
711
728
|
return transformedResponse.body ?? await transformedResponse.text();
|
|
712
729
|
}
|
|
713
730
|
/**
|
|
@@ -752,9 +769,12 @@ class IntegrationRenderer {
|
|
|
752
769
|
* @returns Rendered route body plus effective cache strategy.
|
|
753
770
|
*/
|
|
754
771
|
async execute(options) {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
772
|
+
return this.runRenderExclusive(async () => {
|
|
773
|
+
this.htmlTransformer.setProcessedDependencies([]);
|
|
774
|
+
const adapter = this.createRouteRenderOrchestratorAdapter();
|
|
775
|
+
const renderOptions = await this.prepareRenderOptions(options, adapter);
|
|
776
|
+
return this.routeRenderOrchestrator.executePrepared(renderOptions, adapter);
|
|
777
|
+
});
|
|
758
778
|
}
|
|
759
779
|
/**
|
|
760
780
|
* Finalizes already-resolved HTML for explicit renderer-owned paths.
|
|
@@ -21,6 +21,8 @@ type MarkupNodeLikeShape = {
|
|
|
21
21
|
* @typeParam T Expected resolved type of the thenable.
|
|
22
22
|
*/
|
|
23
23
|
export declare function isThenable<T>(value: unknown): value is PromiseLike<T>;
|
|
24
|
+
/** Returns whether a value looks like a DOM-like markup node from SSR integrations. */
|
|
25
|
+
export declare function isMarkupNodeLike(value: unknown): value is MarkupNodeLikeShape;
|
|
24
26
|
/**
|
|
25
27
|
* Injects `data-eco-trigger` into the first real HTML element opening tag of
|
|
26
28
|
* a component's rendered output string.
|
|
@@ -82,6 +82,9 @@ function isTemplateContentShape(value) {
|
|
|
82
82
|
function isMarkupNodeLikeShape(value) {
|
|
83
83
|
return typeof value === "object" && value !== null && typeof value.nodeType === "number";
|
|
84
84
|
}
|
|
85
|
+
function isMarkupNodeLike(value) {
|
|
86
|
+
return isMarkupNodeLikeShape(value);
|
|
87
|
+
}
|
|
85
88
|
function injectTriggerAttributeIntoString(content, triggerId) {
|
|
86
89
|
const str = content;
|
|
87
90
|
let i = 0;
|
|
@@ -165,6 +168,7 @@ export {
|
|
|
165
168
|
addTriggerAttribute,
|
|
166
169
|
decodeHtmlEntities,
|
|
167
170
|
inspectUnresolvedMarkerArtifactHtml,
|
|
171
|
+
isMarkupNodeLike,
|
|
168
172
|
isThenable,
|
|
169
173
|
normalizeUnresolvedMarkerArtifactHtml,
|
|
170
174
|
wrapWithScriptsInjector
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EcoPagesAppConfig } from '../../types/internal-types.js';
|
|
2
|
-
import type { ComponentRenderResult, EcoComponent, EcoPageComponent, EcoPageFile, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphResult, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
|
|
2
|
+
import type { ComponentRenderResult, EcoComponent, EcoPageComponent, EcoPageFile, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphResult, PageMetadataProps, PagePackageResult, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
|
|
3
3
|
import { type AssetProcessingService, type ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
|
|
4
4
|
import type { HtmlDocumentContribution } from '../../services/html/html-transformer.service.js';
|
|
5
5
|
import { OwnershipValidationService } from './ownership-validation.service.js';
|
|
@@ -64,7 +64,7 @@ export interface RouteRenderOrchestratorAdapter<C> {
|
|
|
64
64
|
/**
|
|
65
65
|
* Runs SSR-policy response transformation and returns the body value exposed to callers.
|
|
66
66
|
*/
|
|
67
|
-
transformRouteResponse(response: Response, htmlContributions?: HtmlDocumentContribution[]): Promise<RouteRendererBody>;
|
|
67
|
+
transformRouteResponse(response: Response, htmlContributions?: HtmlDocumentContribution[], pagePackage?: PagePackageResult): Promise<RouteRendererBody>;
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
70
|
* Captured route-render output in both replayable body and string HTML forms.
|
|
@@ -205,21 +205,20 @@ class RouteRenderOrchestrator {
|
|
|
205
205
|
return result.assetsByRoute;
|
|
206
206
|
}
|
|
207
207
|
const cacheKey = input.integrationName;
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
let pending = this.groupedPageBrowserGraphCache.get(cacheKey);
|
|
209
|
+
if (!pending) {
|
|
210
|
+
pending = this.buildGroupedPageBrowserAssets(input, currentContribution).then((result) => {
|
|
211
|
+
if (result.hasCollectionFailures) {
|
|
212
|
+
this.groupedPageBrowserGraphCache.delete(cacheKey);
|
|
213
|
+
}
|
|
214
|
+
return result.assetsByRoute;
|
|
215
|
+
}).catch((error) => {
|
|
214
216
|
this.groupedPageBrowserGraphCache.delete(cacheKey);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
});
|
|
221
|
-
this.groupedPageBrowserGraphCache.set(cacheKey, pendingGroupedAssets);
|
|
222
|
-
return await pendingGroupedAssets;
|
|
217
|
+
throw error;
|
|
218
|
+
});
|
|
219
|
+
this.groupedPageBrowserGraphCache.set(cacheKey, pending);
|
|
220
|
+
}
|
|
221
|
+
return await pending;
|
|
223
222
|
}
|
|
224
223
|
/**
|
|
225
224
|
* Builds the shared grouped page-browser asset map for one integration.
|
|
@@ -362,7 +361,8 @@ class RouteRenderOrchestrator {
|
|
|
362
361
|
"Content-Type": "text/html"
|
|
363
362
|
}
|
|
364
363
|
}),
|
|
365
|
-
htmlFinalization.htmlContributions
|
|
364
|
+
htmlFinalization.htmlContributions,
|
|
365
|
+
renderOptions.pagePackage
|
|
366
366
|
);
|
|
367
367
|
return {
|
|
368
368
|
body: body2,
|
|
@@ -376,7 +376,8 @@ class RouteRenderOrchestrator {
|
|
|
376
376
|
"Content-Type": "text/html"
|
|
377
377
|
}
|
|
378
378
|
}),
|
|
379
|
-
htmlFinalization.htmlContributions
|
|
379
|
+
htmlFinalization.htmlContributions,
|
|
380
|
+
renderOptions.pagePackage
|
|
380
381
|
);
|
|
381
382
|
return {
|
|
382
383
|
body,
|
|
@@ -198,6 +198,16 @@ function createEcoNavigationRuntime() {
|
|
|
198
198
|
await runtime.cleanupOwner(owner);
|
|
199
199
|
}
|
|
200
200
|
};
|
|
201
|
+
if (typeof window !== "undefined") {
|
|
202
|
+
window.addEventListener("pageshow", (event) => {
|
|
203
|
+
if (event.persisted) {
|
|
204
|
+
runtime.cancelCurrentNavigationTransaction();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
window.addEventListener("pagehide", () => {
|
|
208
|
+
runtime.cancelCurrentNavigationTransaction();
|
|
209
|
+
});
|
|
210
|
+
}
|
|
201
211
|
return runtime;
|
|
202
212
|
}
|
|
203
213
|
function getEcoNavigationRuntime(windowObject = window) {
|
|
@@ -31,6 +31,7 @@ function createBrowserRuntimeModuleAsset(options) {
|
|
|
31
31
|
modules: options.modules,
|
|
32
32
|
fileName: `${options.name}-entry.mjs`,
|
|
33
33
|
rootDir: options.rootDir,
|
|
34
|
+
workDir: options.workDir,
|
|
34
35
|
cacheDirName: options.cacheDirName
|
|
35
36
|
});
|
|
36
37
|
return createBrowserRuntimeScriptAsset({
|
|
@@ -9,12 +9,14 @@ export type BrowserRuntimeEntryModuleConfig = {
|
|
|
9
9
|
* @remarks
|
|
10
10
|
* Integrations use this helper when they need a browser runtime asset composed
|
|
11
11
|
* from multiple bare specifiers but do not want to own temporary file assembly
|
|
12
|
-
* logic themselves. The generated file lives under
|
|
13
|
-
*
|
|
12
|
+
* logic themselves. The generated file lives under the app work directory so
|
|
13
|
+
* repeated runs can reuse the same location without placing sources inside
|
|
14
|
+
* `node_modules`, which can cause bundlers to externalize bare imports.
|
|
14
15
|
*/
|
|
15
16
|
export declare function createBrowserRuntimeEntryModule(options: {
|
|
16
17
|
modules: BrowserRuntimeEntryModuleConfig[];
|
|
17
18
|
fileName: string;
|
|
18
19
|
rootDir?: string;
|
|
20
|
+
workDir?: string;
|
|
19
21
|
cacheDirName?: string;
|
|
20
22
|
}): string;
|
|
@@ -1,37 +1,57 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { DEFAULT_ECOPAGES_WORK_DIR } from "../../../config/constants.js";
|
|
4
5
|
function createBrowserRuntimeEntryModule(options) {
|
|
6
|
+
if (options.modules.some((module) => !module.specifier.startsWith("node:")) && !options.rootDir) {
|
|
7
|
+
throw new Error("createBrowserRuntimeEntryModule requires rootDir to resolve package specifiers");
|
|
8
|
+
}
|
|
5
9
|
const rootDir = options.rootDir ?? process.cwd();
|
|
10
|
+
const workDir = options.workDir ?? path.join(rootDir, DEFAULT_ECOPAGES_WORK_DIR);
|
|
6
11
|
const artifactsDir = path.join(
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
".cache",
|
|
12
|
+
workDir,
|
|
13
|
+
".browser-runtime-entries",
|
|
10
14
|
options.cacheDirName ?? "ecopages-browser-runtime"
|
|
11
15
|
);
|
|
12
16
|
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
13
17
|
const requireFromRoot = createRequire(path.join(rootDir, "package.json"));
|
|
14
18
|
const seenExports = /* @__PURE__ */ new Set();
|
|
15
19
|
const statements = [];
|
|
20
|
+
const filePath = path.join(artifactsDir, options.fileName);
|
|
21
|
+
const entryDir = path.dirname(filePath);
|
|
16
22
|
for (const module of options.modules) {
|
|
23
|
+
const importSpecifier = resolveEntryImportSpecifier(module.specifier, requireFromRoot, entryDir);
|
|
17
24
|
if (module.defaultExport) {
|
|
18
|
-
statements.push(`import __ecopages_default_export__ from '${
|
|
25
|
+
statements.push(`import __ecopages_default_export__ from '${importSpecifier}';`);
|
|
19
26
|
statements.push("export default __ecopages_default_export__;");
|
|
20
27
|
}
|
|
21
28
|
const exportNames = getModuleExportNames(module.specifier, requireFromRoot).filter(
|
|
22
29
|
(name) => !seenExports.has(name)
|
|
23
30
|
);
|
|
24
31
|
if (exportNames.length > 0) {
|
|
25
|
-
statements.push(`export { ${exportNames.join(", ")} } from '${
|
|
32
|
+
statements.push(`export { ${exportNames.join(", ")} } from '${importSpecifier}';`);
|
|
26
33
|
for (const exportName of exportNames) {
|
|
27
34
|
seenExports.add(exportName);
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
|
-
const
|
|
32
|
-
fs.
|
|
38
|
+
const content = statements.join("\n");
|
|
39
|
+
if (!fs.existsSync(filePath) || fs.readFileSync(filePath, "utf-8") !== content) {
|
|
40
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
41
|
+
}
|
|
33
42
|
return filePath;
|
|
34
43
|
}
|
|
44
|
+
function resolveEntryImportSpecifier(specifier, requireFromRoot, entryDir) {
|
|
45
|
+
if (specifier.startsWith("node:") || specifier.startsWith("file:")) {
|
|
46
|
+
return specifier;
|
|
47
|
+
}
|
|
48
|
+
const resolvedPath = requireFromRoot.resolve(specifier);
|
|
49
|
+
let relativePath = path.relative(entryDir, resolvedPath).replace(/\\/g, "/");
|
|
50
|
+
if (!relativePath.startsWith(".")) {
|
|
51
|
+
relativePath = `./${relativePath}`;
|
|
52
|
+
}
|
|
53
|
+
return relativePath;
|
|
54
|
+
}
|
|
35
55
|
function getModuleExportNames(specifier, requireFromRoot) {
|
|
36
56
|
const moduleExports = requireFromRoot(specifier);
|
|
37
57
|
return Object.keys(moduleExports).filter((name) => name !== "__esModule" && name !== "default").filter((name) => /^[$A-Z_a-z][$\w]*$/.test(name)).sort();
|
|
@@ -8,19 +8,33 @@ class NodeModuleScriptProcessor extends BaseScriptProcessor {
|
|
|
8
8
|
const modulePath = this.resolveModulePath(dep.importPath, this.appConfig.rootDir);
|
|
9
9
|
const moduleName = path.basename(modulePath);
|
|
10
10
|
const filename = dep.name ?? `nm-${moduleName}`;
|
|
11
|
+
const shouldBundle = this.shouldBundle(dep);
|
|
11
12
|
const configHash = this.generateHash(
|
|
12
|
-
JSON.stringify({
|
|
13
|
+
JSON.stringify({
|
|
14
|
+
bundle: shouldBundle,
|
|
15
|
+
inline: dep.inline,
|
|
16
|
+
minify: shouldBundle && !dep.inline && this.isProduction,
|
|
17
|
+
opts: dep.bundleOptions
|
|
18
|
+
})
|
|
13
19
|
);
|
|
14
20
|
const cachekey = `${this.buildCacheKey(filename, this.generateHash(modulePath), dep)}:${configHash}`;
|
|
15
21
|
return this.getOrProcess(cachekey, async () => {
|
|
16
|
-
if (dep.inline) {
|
|
22
|
+
if (dep.inline || !shouldBundle) {
|
|
17
23
|
const content = fileSystem.readFileAsBuffer(modulePath).toString();
|
|
24
|
+
const filepath = dep.inline ? void 0 : path.join(this.getAssetsDir(), "vendors", filename);
|
|
25
|
+
if (filepath) {
|
|
26
|
+
fileSystem.ensureDir(path.dirname(filepath));
|
|
27
|
+
if (!fileSystem.exists(filepath) || fileSystem.readFileAsBuffer(filepath).toString() !== content) {
|
|
28
|
+
fileSystem.copyFile(modulePath, filepath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
18
31
|
return {
|
|
32
|
+
filepath,
|
|
19
33
|
content,
|
|
20
34
|
kind: dep.kind,
|
|
21
35
|
position: dep.position,
|
|
22
36
|
attributes: dep.attributes,
|
|
23
|
-
inline:
|
|
37
|
+
inline: dep.inline,
|
|
24
38
|
excludeFromHtml: dep.excludeFromHtml,
|
|
25
39
|
packageRole: dep.packageRole,
|
|
26
40
|
bundledSourceFilepaths: dep.bundledSourceFilepaths
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getAppBrowserBuildPlugins,
|
|
3
|
+
getAppBuildExecutor,
|
|
4
|
+
getAppHmrBuildExecutor,
|
|
5
|
+
getAppTranspileOptions
|
|
6
|
+
} from "../../build/build-adapter.js";
|
|
2
7
|
import { mergeEcoBuildPlugins } from "../../build/build-manifest.js";
|
|
3
8
|
class BrowserBundleService {
|
|
4
9
|
appConfig;
|
|
@@ -17,7 +22,7 @@ class BrowserBundleService {
|
|
|
17
22
|
* site.
|
|
18
23
|
*/
|
|
19
24
|
async bundle(options) {
|
|
20
|
-
const { profile, excludeAppBuildPlugins, plugins, ...rawBuildOptions } = options;
|
|
25
|
+
const { profile, excludeAppBuildPlugins, plugins, executor = "hmr", ...rawBuildOptions } = options;
|
|
21
26
|
const appBrowserPlugins = getAppBrowserBuildPlugins(this.appConfig);
|
|
22
27
|
const filteredAppBrowserPlugins = excludeAppBuildPlugins && excludeAppBuildPlugins.length > 0 ? appBrowserPlugins.filter((plugin) => !excludeAppBuildPlugins.includes(plugin.name)) : appBrowserPlugins;
|
|
23
28
|
const request = {
|
|
@@ -26,7 +31,8 @@ class BrowserBundleService {
|
|
|
26
31
|
...getAppTranspileOptions(this.appConfig, profile),
|
|
27
32
|
plugins: mergeEcoBuildPlugins(plugins, filteredAppBrowserPlugins)
|
|
28
33
|
};
|
|
29
|
-
|
|
34
|
+
const buildExecutor = executor === "build" ? getAppBuildExecutor(this.appConfig) : getAppHmrBuildExecutor(this.appConfig);
|
|
35
|
+
return await buildExecutor.build(request);
|
|
30
36
|
}
|
|
31
37
|
async bundleGroupedEntries(entries, options) {
|
|
32
38
|
const request = {
|
|
@@ -83,7 +83,7 @@ export declare class HtmlTransformerService {
|
|
|
83
83
|
* string-based fallback remains in place for runtimes that cannot provide one
|
|
84
84
|
* of those rewriter implementations.
|
|
85
85
|
*/
|
|
86
|
-
transform(res: Response, contributions?: HtmlDocumentContribution[]): Promise<Response>;
|
|
86
|
+
transform(res: Response, contributions?: HtmlDocumentContribution[], pagePackage?: PagePackageResult): Promise<Response>;
|
|
87
87
|
/**
|
|
88
88
|
* Splits processed assets into head and body injection groups.
|
|
89
89
|
*/
|
|
@@ -188,8 +188,8 @@ class HtmlTransformerService {
|
|
|
188
188
|
* string-based fallback remains in place for runtimes that cannot provide one
|
|
189
189
|
* of those rewriter implementations.
|
|
190
190
|
*/
|
|
191
|
-
async transform(res, contributions = []) {
|
|
192
|
-
const { head, body } = this.groupDependenciesByPosition();
|
|
191
|
+
async transform(res, contributions = [], pagePackage) {
|
|
192
|
+
const { head, body } = this.groupDependenciesByPosition(pagePackage);
|
|
193
193
|
const { headPrepend, headAppend, bodyPrepend, bodyAppend } = this.groupContributionsByPlacement(contributions);
|
|
194
194
|
const htmlRewriter = await this.htmlRewriterProvider.createHtmlRewriter();
|
|
195
195
|
if (htmlRewriter) {
|
|
@@ -239,8 +239,8 @@ class HtmlTransformerService {
|
|
|
239
239
|
/**
|
|
240
240
|
* Splits processed assets into head and body injection groups.
|
|
241
241
|
*/
|
|
242
|
-
groupDependenciesByPosition() {
|
|
243
|
-
const dependencies = this.pagePackage ? this.resolvePagePackageHtmlDependencies(this.pagePackage) : this.processedDependencies;
|
|
242
|
+
groupDependenciesByPosition(pagePackageOverride) {
|
|
243
|
+
const dependencies = pagePackageOverride ? this.resolvePagePackageHtmlDependencies(pagePackageOverride) : this.pagePackage ? this.resolvePagePackageHtmlDependencies(this.pagePackage) : this.processedDependencies;
|
|
244
244
|
return dependencies.reduce(
|
|
245
245
|
(acc, dep) => {
|
|
246
246
|
if (dep.kind === "script") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EcoPagesAppConfig } from '../../types/internal-types.js';
|
|
2
|
-
export type DevelopmentInvalidationCategory = 'public-asset' | 'additional-watch' | 'include-source' | 'route-source' | 'processor-owned-asset' | 'server-source' | 'other';
|
|
2
|
+
export type DevelopmentInvalidationCategory = 'public-asset' | 'additional-watch' | 'include-source' | 'explicit-server-view' | 'route-source' | 'processor-owned-asset' | 'server-source' | 'other';
|
|
3
3
|
/**
|
|
4
4
|
* Framework-owned invalidation plan for one changed file.
|
|
5
5
|
*
|
|
@@ -62,6 +62,15 @@ export declare class DevelopmentInvalidationService {
|
|
|
62
62
|
* Returns whether the file is an include/template source file.
|
|
63
63
|
*/
|
|
64
64
|
isIncludeSourceFile(filePath: string): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Returns whether the file is an explicit server-rendered view module.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* These modules are typically registered through `renderServerModule` in
|
|
70
|
+
* `app.ts` rather than the filesystem router. They need a full browser reload
|
|
71
|
+
* because their HTML is produced on the server, not through client HMR entrypoints.
|
|
72
|
+
*/
|
|
73
|
+
isExplicitServerViewFile(filePath: string): boolean;
|
|
65
74
|
/**
|
|
66
75
|
* Returns whether the file is a server-executed source module outside the
|
|
67
76
|
* special route/include buckets.
|
|
@@ -60,6 +60,16 @@ class DevelopmentInvalidationService {
|
|
|
60
60
|
processorHandledAsset: false
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
if (this.isExplicitServerViewFile(filePath)) {
|
|
64
|
+
return {
|
|
65
|
+
category: "explicit-server-view",
|
|
66
|
+
invalidateServerModules: true,
|
|
67
|
+
refreshRoutes: false,
|
|
68
|
+
reloadBrowser: true,
|
|
69
|
+
delegateToHmr: false,
|
|
70
|
+
processorHandledAsset: false
|
|
71
|
+
};
|
|
72
|
+
}
|
|
63
73
|
if (this.isRouteSourceFile(filePath)) {
|
|
64
74
|
return {
|
|
65
75
|
category: "route-source",
|
|
@@ -148,6 +158,25 @@ class DevelopmentInvalidationService {
|
|
|
148
158
|
}
|
|
149
159
|
return /\.(?:[cm]?ts|[jt]sx?|mdx)$/u.test(resolvedPath);
|
|
150
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Returns whether the file is an explicit server-rendered view module.
|
|
163
|
+
*
|
|
164
|
+
* @remarks
|
|
165
|
+
* These modules are typically registered through `renderServerModule` in
|
|
166
|
+
* `app.ts` rather than the filesystem router. They need a full browser reload
|
|
167
|
+
* because their HTML is produced on the server, not through client HMR entrypoints.
|
|
168
|
+
*/
|
|
169
|
+
isExplicitServerViewFile(filePath) {
|
|
170
|
+
const resolvedPath = path.resolve(filePath);
|
|
171
|
+
const viewsDir = path.join(this.appConfig.absolutePaths.srcDir, "views");
|
|
172
|
+
if (resolvedPath !== viewsDir && !resolvedPath.startsWith(`${viewsDir}${path.sep}`)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (this.appConfig.templatesExt.some((extension) => resolvedPath.endsWith(extension))) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return /\.(?:[cm]?ts|[jt]sx?|mdx)$/u.test(resolvedPath);
|
|
179
|
+
}
|
|
151
180
|
/**
|
|
152
181
|
* Returns whether the file is a server-executed source module outside the
|
|
153
182
|
* special route/include buckets.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Module Loading
|
|
2
|
+
|
|
3
|
+
Server-side source loading for framework-owned page modules, includes, and related templates.
|
|
4
|
+
|
|
5
|
+
## Import stack
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Call site (route scan, renderer, SSG, API)
|
|
9
|
+
└─ AppModuleLoader.importModule() app-server-module-transpiler.service.ts
|
|
10
|
+
├─ merges server build plugins + JSX ownership plugins
|
|
11
|
+
├─ resolves route-module BuildExecutor
|
|
12
|
+
└─ PageModuleImportService.importModule()
|
|
13
|
+
├─ dev + host loader → host runtime (Node only, when configured)
|
|
14
|
+
├─ production disk cache → route-module build cache
|
|
15
|
+
├─ unified pages graph → prebuilt chunk import
|
|
16
|
+
└─ Rolldown per-page build → dynamic import of transpiled output
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`ServerModuleTranspiler` is a thin wrapper that injects `rootDir`, optional default plugins, and invalidation version into the same `PageModuleImportService` path. App code should prefer `getAppModuleLoader(appConfig)` or `getAppServerModuleTranspiler(appConfig)`.
|
|
20
|
+
|
|
21
|
+
## Host vs app ownership
|
|
22
|
+
|
|
23
|
+
| Path | When |
|
|
24
|
+
| ----------------------- | -------------------------------------------------------------------------------------------- |
|
|
25
|
+
| Host loader | `appConfig.runtime.hostModuleLoader` set (Vite host) and file supports direct source loading |
|
|
26
|
+
| Ecopages build pipeline | Framework-owned template files under `pages/`, `includes/`, etc. |
|
|
27
|
+
|
|
28
|
+
`shouldAppUseHostModuleLoader()` encodes the directory + extension rules. Bun always uses the build pipeline today.
|
|
29
|
+
|
|
30
|
+
## Caching layers
|
|
31
|
+
|
|
32
|
+
1. **In-memory promise cache** (`PageModuleImportService.importCache`) — keyed by file path, resolved `outdir`, JSX/plugin inputs, file hash, and invalidation version. Cleared by `invalidateDevelopmentGraph()`.
|
|
33
|
+
2. **Disk transpile cache** (`.eco/.server-modules/.build-cache.json`) — production only; see route-module build cache slice.
|
|
34
|
+
3. **Unified graph manifest** — production static export fast path; see build layer docs.
|
|
35
|
+
|
|
36
|
+
## Files
|
|
37
|
+
|
|
38
|
+
| File | Role |
|
|
39
|
+
| ----------------------------------------- | --------------------------------------------------------- |
|
|
40
|
+
| `page-module-import.service.ts` | Core import/cache/build orchestration |
|
|
41
|
+
| `app-module-loader.service.ts` | App-facing loader interface |
|
|
42
|
+
| `app-server-module-transpiler.service.ts` | Factory for app-scoped loader + transpiler |
|
|
43
|
+
| `server-module-transpiler.service.ts` | Injectable transpiler boundary for tests and bootstrap |
|
|
44
|
+
| `host-module-loader-registry.ts` | Process-global host loader fallback for embedded runtimes |
|
|
45
|
+
| `source-module-support.ts` | Extension allowlist for host direct loading |
|
|
46
|
+
|
|
47
|
+
## Development invalidation
|
|
48
|
+
|
|
49
|
+
`DevelopmentInvalidationService.invalidateServerModules()` bumps the app-owned invalidation version and calls `appModuleLoader.invalidateDevelopmentGraph()`, which clears the in-memory import cache and increments the per-service invalidation counter used in cache keys and dev `?update=` query params.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getAppRouteModuleBuildExecutor, getAppServerBuildPlugins } from "../../build/build-adapter.js";
|
|
2
|
+
import { getJsxOwnershipPlugins } from "../../build/jsx-ownership-plugins.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { DevelopmentInvalidationService } from "../invalidation/development-invalidation.service.js";
|
|
5
5
|
import {} from "./app-module-loader.service.js";
|
|
@@ -37,22 +37,9 @@ function setAppHostModuleLoader(appConfig, hostModuleLoader) {
|
|
|
37
37
|
hostModuleLoader
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
function getJsxOwnershipPlugins(appConfig) {
|
|
41
|
-
const jsxExtensions = (appConfig.integrations ?? []).filter((integration) => integration.jsxImportSource).flatMap(
|
|
42
|
-
(integration) => integration.extensions.filter((extension) => extension.endsWith(".tsx") || extension.endsWith(".jsx")).map((extension) => ({ integration, extension }))
|
|
43
|
-
).sort((left, right) => right.extension.length - left.extension.length);
|
|
44
|
-
return jsxExtensions.map(
|
|
45
|
-
({ integration, extension }) => createForeignJsxOverridePlugin({
|
|
46
|
-
hostJsxImportSource: integration.jsxImportSource,
|
|
47
|
-
foreignExtensions: [extension],
|
|
48
|
-
excludeExtensions: jsxExtensions.filter((candidate) => candidate.extension.length > extension.length).filter((candidate) => candidate.extension.endsWith(extension)).map((candidate) => candidate.extension),
|
|
49
|
-
name: `ecopages-jsx-ownership-${integration.name}-${extension.replace(/[^a-zA-Z0-9]+/g, "-")}`
|
|
50
|
-
})
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
40
|
function createAppModuleLoader(appConfig) {
|
|
54
41
|
const invalidationService = new DevelopmentInvalidationService(appConfig);
|
|
55
|
-
const pageModuleImportService = new PageModuleImportService({
|
|
42
|
+
const pageModuleImportService = new PageModuleImportService(appConfig, {
|
|
56
43
|
canLoadSourceModuleFromHost: (filePath) => shouldAppUseHostModuleLoader(appConfig, filePath),
|
|
57
44
|
getHostModuleLoader: () => getAppHostModuleLoader(appConfig)
|
|
58
45
|
});
|
|
@@ -69,7 +56,7 @@ function createAppModuleLoader(appConfig) {
|
|
|
69
56
|
return await pageModuleImportService.importModule({
|
|
70
57
|
...options,
|
|
71
58
|
...mergedPlugins.length > 0 ? { plugins: mergedPlugins } : {},
|
|
72
|
-
buildExecutor: options.buildExecutor ??
|
|
59
|
+
buildExecutor: options.buildExecutor ?? getAppRouteModuleBuildExecutor(appConfig),
|
|
73
60
|
invalidationVersion
|
|
74
61
|
});
|
|
75
62
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { build, type BuildExecutor, type BuildResult } from '../../build/build-adapter.js';
|
|
2
2
|
import type { EcoBuildPlugin } from '../../build/build-types.js';
|
|
3
|
+
import type { EcoPagesAppConfig } from '../../types/internal-types.js';
|
|
3
4
|
import type { SourceModuleLoaderFactory } from './module-loading-types.js';
|
|
4
5
|
interface PageModuleImportBaseOptions {
|
|
5
6
|
filePath: string;
|
|
@@ -59,10 +60,11 @@ export interface PageModuleImportDependencies {
|
|
|
59
60
|
* transpilation settings, and error semantics across the different callers.
|
|
60
61
|
*/
|
|
61
62
|
export declare class PageModuleImportService {
|
|
63
|
+
private readonly appConfig?;
|
|
62
64
|
private readonly dependencies;
|
|
63
65
|
private readonly importCache;
|
|
64
66
|
private developmentInvalidationVersion;
|
|
65
|
-
constructor(dependencies?: Partial<PageModuleImportDependencies>);
|
|
67
|
+
constructor(appConfig?: EcoPagesAppConfig, dependencies?: Partial<PageModuleImportDependencies>);
|
|
66
68
|
/**
|
|
67
69
|
* Clears the shared import cache used by framework-owned page-module loads.
|
|
68
70
|
*/
|
|
@@ -91,5 +93,6 @@ export declare class PageModuleImportService {
|
|
|
91
93
|
*/
|
|
92
94
|
importModule<T = unknown>(options: PageModuleBuildImportOptions): Promise<T>;
|
|
93
95
|
private loadModule;
|
|
96
|
+
private getRouteModuleBuildCache;
|
|
94
97
|
}
|
|
95
98
|
export {};
|