@ecopages/core 0.2.0-beta.41 → 0.2.0-beta.43
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 +11 -3
- package/src/adapters/shared/http/fs-server-response-matcher.js +3 -1
- package/src/build/browser/browser-runtime-manifest.d.ts +4 -0
- package/src/build/browser/browser-runtime-manifest.js +9 -2
- package/src/build/browser/browser-runtime-plugin-helpers.d.ts +3 -5
- package/src/build/browser/browser-runtime-plugin-helpers.js +4 -6
- package/src/build/browser/browser-runtime-plugin.d.ts +11 -12
- package/src/build/browser/browser-runtime-plugin.js +36 -34
- package/src/build/rolldown/rolldown-adapter-helpers.js +10 -5
- package/src/dev/transform-server/dev-transform-import-rewriter.js +2 -1
- package/src/dev/transform-server/dev-transform-vendor-registry.js +11 -3
- package/src/plugins/package-specifier.d.ts +12 -0
- package/src/plugins/package-specifier.js +33 -0
- package/src/plugins/tsconfig-import-resolver.d.ts +5 -3
- package/src/plugins/tsconfig-import-resolver.js +38 -11
- package/src/route-renderer/orchestration/route-pipeline/route-prepared-options.builder.js +3 -8
- package/src/services/assets/asset-processing-service/asset-processing.service.d.ts +5 -0
- package/src/services/assets/asset-processing-service/asset-processing.service.js +30 -3
- package/src/services/assets/browser-runtime-asset-generation.d.ts +17 -0
- package/src/services/assets/browser-runtime-asset-generation.js +28 -0
- package/src/services/cache/page-request-cache-coordinator.service.d.ts +2 -1
- package/src/services/cache/page-request-cache-coordinator.service.js +7 -1
- package/src/services/invalidation/development-invalidation.service.js +3 -1
- package/src/services/module-loading/README.md +2 -2
- package/src/services/module-loading/page-module-import.service.js +15 -4
- package/src/services/module-loading/route-module-build-cache-registry.d.ts +2 -0
- package/src/services/module-loading/route-module-build-cache-registry.js +12 -0
- package/src/services/module-loading/route-module-build-cache.store.d.ts +10 -0
- package/src/services/module-loading/route-module-build-cache.store.js +18 -0
- package/src/types/public-types.d.ts +5 -3
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.43",
|
|
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.43",
|
|
21
21
|
"@ecopages/logger": "^0.2.3",
|
|
22
22
|
"@ecopages/scripts-injector": "^0.1.5",
|
|
23
23
|
"@oxc-project/runtime": "0.141.0",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@standard-schema/utils": "^0.3.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@ecopages/dev-toolbar": "0.2.0-beta.
|
|
35
|
+
"@ecopages/dev-toolbar": "0.2.0-beta.43"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"@ecopages/dev-toolbar": {
|
|
@@ -283,6 +283,10 @@
|
|
|
283
283
|
"types": "./src/plugins/tsconfig-import-resolver.d.ts",
|
|
284
284
|
"default": "./src/plugins/tsconfig-import-resolver.js"
|
|
285
285
|
},
|
|
286
|
+
"./plugins/package-specifier": {
|
|
287
|
+
"types": "./src/plugins/package-specifier.d.ts",
|
|
288
|
+
"default": "./src/plugins/package-specifier.js"
|
|
289
|
+
},
|
|
286
290
|
"./plugins/jsx-import-source.utils": {
|
|
287
291
|
"types": "./src/plugins/jsx-import-source.utils.d.ts",
|
|
288
292
|
"default": "./src/plugins/jsx-import-source.utils.js"
|
|
@@ -576,6 +580,10 @@
|
|
|
576
580
|
"types": "./src/plugins/tsconfig-import-resolver.d.ts",
|
|
577
581
|
"default": "./src/plugins/tsconfig-import-resolver.js"
|
|
578
582
|
},
|
|
583
|
+
"./plugins/package-specifier.ts": {
|
|
584
|
+
"types": "./src/plugins/package-specifier.d.ts",
|
|
585
|
+
"default": "./src/plugins/package-specifier.js"
|
|
586
|
+
},
|
|
579
587
|
"./plugins/jsx-import-source.utils.ts": {
|
|
580
588
|
"types": "./src/plugins/jsx-import-source.utils.d.ts",
|
|
581
589
|
"default": "./src/plugins/jsx-import-source.utils.js"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { appLogger } from '../../../global/app-logger.js';
|
|
3
3
|
import { PageRequestCacheCoordinator } from '../../../services/cache/page-request-cache-coordinator.service.js';
|
|
4
|
+
import { getBrowserRuntimeAssetGeneration } from '../../../services/assets/browser-runtime-asset-generation.js';
|
|
4
5
|
import { ServerUtils } from '../../../utils/server-utils.module.js';
|
|
5
6
|
import { FileRouteMiddlewarePipeline } from './file-route-middleware-pipeline.js';
|
|
6
7
|
import { HttpError } from '../../../errors/http-error.js';
|
|
@@ -27,7 +28,7 @@ export class FileSystemResponseMatcher {
|
|
|
27
28
|
this.router = router;
|
|
28
29
|
this.routeRendererFactory = routeRendererFactory;
|
|
29
30
|
this.fileSystemResponseFactory = fileSystemResponseFactory;
|
|
30
|
-
this.pageRequestCacheCoordinator = new PageRequestCacheCoordinator(cacheService, defaultCacheStrategy);
|
|
31
|
+
this.pageRequestCacheCoordinator = new PageRequestCacheCoordinator(cacheService, defaultCacheStrategy, () => getBrowserRuntimeAssetGeneration(this.appConfig));
|
|
31
32
|
this.fileRouteMiddlewarePipeline = new FileRouteMiddlewarePipeline(cacheService);
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
@@ -169,6 +170,7 @@ export class FileSystemResponseMatcher {
|
|
|
169
170
|
const result = await routeRenderer.execute({
|
|
170
171
|
file: templatePath,
|
|
171
172
|
props,
|
|
173
|
+
locals: {},
|
|
172
174
|
});
|
|
173
175
|
return createHtmlResponse(result.body);
|
|
174
176
|
}
|
|
@@ -29,3 +29,7 @@ export declare class BrowserRuntimeManifestConflictError extends Error {
|
|
|
29
29
|
export declare function createBrowserRuntimeManifest(declarations?: readonly BrowserRuntimeAssetDeclaration[]): BrowserRuntimeManifest;
|
|
30
30
|
export declare function mergeBrowserRuntimeManifests(...manifests: Array<BrowserRuntimeManifest | undefined>): BrowserRuntimeManifest;
|
|
31
31
|
export declare function getBrowserRuntimeSpecifierMap(manifest: BrowserRuntimeManifest): ReadonlyMap<string, string>;
|
|
32
|
+
/** Resolves a runtime public URL for an exact specifier. */
|
|
33
|
+
export declare function resolveRuntimeSpecifierPublicPath(specifier: string, publicPathsBySpecifier: ReadonlyMap<string, string>): string | undefined;
|
|
34
|
+
/** Resolves a manifest-owned runtime public URL for an exact specifier. */
|
|
35
|
+
export declare function resolveBrowserRuntimePublicPath(specifier: string, manifest: BrowserRuntimeManifest): string | undefined;
|
|
@@ -18,8 +18,7 @@ function normalizeDeclaration(declaration) {
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
function hasCompatibleRuntimeAsset(existing, incoming) {
|
|
21
|
-
return (existing.
|
|
22
|
-
existing.importPath === incoming.importPath &&
|
|
21
|
+
return (existing.importPath === incoming.importPath &&
|
|
23
22
|
existing.publicPath === incoming.publicPath &&
|
|
24
23
|
existing.mode === incoming.mode &&
|
|
25
24
|
arrayEquals(existing.externals, incoming.externals));
|
|
@@ -56,3 +55,11 @@ export function mergeBrowserRuntimeManifests(...manifests) {
|
|
|
56
55
|
export function getBrowserRuntimeSpecifierMap(manifest) {
|
|
57
56
|
return new Map(manifest.assets.map((asset) => [asset.specifier, asset.publicPath]));
|
|
58
57
|
}
|
|
58
|
+
/** Resolves a runtime public URL for an exact specifier. */
|
|
59
|
+
export function resolveRuntimeSpecifierPublicPath(specifier, publicPathsBySpecifier) {
|
|
60
|
+
return publicPathsBySpecifier.get(specifier);
|
|
61
|
+
}
|
|
62
|
+
/** Resolves a manifest-owned runtime public URL for an exact specifier. */
|
|
63
|
+
export function resolveBrowserRuntimePublicPath(specifier, manifest) {
|
|
64
|
+
return manifest.bySpecifier.get(specifier)?.publicPath;
|
|
65
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `createBrowserRuntimePlugin` needs to:
|
|
6
6
|
*
|
|
7
7
|
* 1. Escape each specifier for inclusion in a regex filter.
|
|
8
|
-
* 2. Build a `RegExp` whose alternation matches
|
|
8
|
+
* 2. Build a `RegExp` whose alternation matches registered specifiers.
|
|
9
9
|
*
|
|
10
10
|
* These utilities are extracted here so the plugin does not duplicate
|
|
11
11
|
* the same code path.
|
|
@@ -17,10 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function escapeRegExp(value: string): string;
|
|
19
19
|
/**
|
|
20
|
-
* Builds a `RegExp`
|
|
21
|
-
* `specifierMap`.
|
|
20
|
+
* Builds a `RegExp` that matches registered specifiers exactly.
|
|
22
21
|
*
|
|
23
|
-
* Returns `null` for an empty map so callers can short-circuit
|
|
24
|
-
* avoid registering a no-op `onResolve` / `onLoad` filter.
|
|
22
|
+
* Returns `null` for an empty map so callers can short-circuit registration.
|
|
25
23
|
*/
|
|
26
24
|
export declare function buildSpecifierFilter(specifierMap: ReadonlyMap<string, string>): RegExp | null;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `createBrowserRuntimePlugin` needs to:
|
|
6
6
|
*
|
|
7
7
|
* 1. Escape each specifier for inclusion in a regex filter.
|
|
8
|
-
* 2. Build a `RegExp` whose alternation matches
|
|
8
|
+
* 2. Build a `RegExp` whose alternation matches registered specifiers.
|
|
9
9
|
*
|
|
10
10
|
* These utilities are extracted here so the plugin does not duplicate
|
|
11
11
|
* the same code path.
|
|
@@ -19,16 +19,14 @@ export function escapeRegExp(value) {
|
|
|
19
19
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Builds a `RegExp`
|
|
23
|
-
* `specifierMap`.
|
|
22
|
+
* Builds a `RegExp` that matches registered specifiers exactly.
|
|
24
23
|
*
|
|
25
|
-
* Returns `null` for an empty map so callers can short-circuit
|
|
26
|
-
* avoid registering a no-op `onResolve` / `onLoad` filter.
|
|
24
|
+
* Returns `null` for an empty map so callers can short-circuit registration.
|
|
27
25
|
*/
|
|
28
26
|
export function buildSpecifierFilter(specifierMap) {
|
|
29
27
|
if (specifierMap.size === 0) {
|
|
30
28
|
return null;
|
|
31
29
|
}
|
|
32
30
|
const alternation = Array.from(specifierMap.keys()).map(escapeRegExp).join('|');
|
|
33
|
-
return new RegExp(`^(
|
|
31
|
+
return new RegExp(`^(?:${alternation})$`);
|
|
34
32
|
}
|
|
@@ -11,22 +11,16 @@
|
|
|
11
11
|
* - `onLoad` for JS/TS files → AST-walking import/export rewrite against the
|
|
12
12
|
* manifest specifier set, with a `code.includes(specifier)` fast path
|
|
13
13
|
*
|
|
14
|
-
* The plugin object carries the manifest
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* read it without re-walking the manifest.
|
|
18
|
-
*
|
|
19
|
-
* This is the single source of truth for browser-runtime plugin behavior.
|
|
20
|
-
* Per-bundler bridges can treat it as a single plugin.
|
|
14
|
+
* The plugin object carries the manifest under `BROWSER_RUNTIME_MANIFEST` so
|
|
15
|
+
* post-build rewriters can resolve exact and subpath imports without rebuilding
|
|
16
|
+
* the map by hand.
|
|
21
17
|
*/
|
|
22
18
|
import type { EcoBuildPlugin } from '../contracts/build-types.js';
|
|
23
19
|
import { type BrowserRuntimeManifest } from './browser-runtime-manifest.js';
|
|
24
20
|
/**
|
|
25
|
-
* Symbol used to attach the manifest
|
|
26
|
-
* plugin instance. Consumers read it via `getBrowserRuntimeImportRewriteMap`
|
|
27
|
-
* or `collectBrowserRuntimeImportRewriteMap`.
|
|
21
|
+
* Symbol used to attach the browser-runtime manifest to a plugin instance.
|
|
28
22
|
*/
|
|
29
|
-
export declare const
|
|
23
|
+
export declare const BROWSER_RUNTIME_MANIFEST: unique symbol;
|
|
30
24
|
/**
|
|
31
25
|
* Default name used by `createBrowserRuntimePlugin` when the caller
|
|
32
26
|
* does not provide one.
|
|
@@ -63,8 +57,13 @@ export type CreateBrowserRuntimePluginOptions = {
|
|
|
63
57
|
*
|
|
64
58
|
* Exposed for the post-build rewriter and for tests.
|
|
65
59
|
*/
|
|
66
|
-
export declare function rewriteBrowserRuntimeImports(code: string,
|
|
60
|
+
export declare function rewriteBrowserRuntimeImports(code: string, manifest: BrowserRuntimeManifest, filePath?: string): string;
|
|
61
|
+
export declare function getBrowserRuntimeManifestFromPlugin(plugin: EcoBuildPlugin): BrowserRuntimeManifest | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Returns the exact specifier → publicPath map derived from the plugin manifest.
|
|
64
|
+
*/
|
|
67
65
|
export declare function getBrowserRuntimeImportRewriteMap(plugin: EcoBuildPlugin): ReadonlyMap<string, string> | undefined;
|
|
66
|
+
export declare function collectBrowserRuntimeManifests(plugins: EcoBuildPlugin[] | undefined): BrowserRuntimeManifest[];
|
|
68
67
|
export declare function collectBrowserRuntimeImportRewriteMap(plugins: EcoBuildPlugin[] | undefined): ReadonlyMap<string, string>;
|
|
69
68
|
/**
|
|
70
69
|
* Creates a build plugin that exposes the union of browser-runtime
|
|
@@ -11,25 +11,19 @@
|
|
|
11
11
|
* - `onLoad` for JS/TS files → AST-walking import/export rewrite against the
|
|
12
12
|
* manifest specifier set, with a `code.includes(specifier)` fast path
|
|
13
13
|
*
|
|
14
|
-
* The plugin object carries the manifest
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* read it without re-walking the manifest.
|
|
18
|
-
*
|
|
19
|
-
* This is the single source of truth for browser-runtime plugin behavior.
|
|
20
|
-
* Per-bundler bridges can treat it as a single plugin.
|
|
14
|
+
* The plugin object carries the manifest under `BROWSER_RUNTIME_MANIFEST` so
|
|
15
|
+
* post-build rewriters can resolve exact and subpath imports without rebuilding
|
|
16
|
+
* the map by hand.
|
|
21
17
|
*/
|
|
22
18
|
import path from 'node:path';
|
|
23
19
|
import { existsSync, readFileSync } from 'node:fs';
|
|
24
20
|
import { parseModuleSource } from '../../cache/module-parse-cache.js';
|
|
25
|
-
import { getBrowserRuntimeSpecifierMap } from './browser-runtime-manifest.js';
|
|
21
|
+
import { getBrowserRuntimeSpecifierMap, mergeBrowserRuntimeManifests, resolveBrowserRuntimePublicPath, } from './browser-runtime-manifest.js';
|
|
26
22
|
import { buildSpecifierFilter } from './browser-runtime-plugin-helpers.js';
|
|
27
23
|
/**
|
|
28
|
-
* Symbol used to attach the manifest
|
|
29
|
-
* plugin instance. Consumers read it via `getBrowserRuntimeImportRewriteMap`
|
|
30
|
-
* or `collectBrowserRuntimeImportRewriteMap`.
|
|
24
|
+
* Symbol used to attach the browser-runtime manifest to a plugin instance.
|
|
31
25
|
*/
|
|
32
|
-
export const
|
|
26
|
+
export const BROWSER_RUNTIME_MANIFEST = Symbol.for('ecopages.browserRuntimeManifest');
|
|
33
27
|
/**
|
|
34
28
|
* Default name used by `createBrowserRuntimePlugin` when the caller
|
|
35
29
|
* does not provide one.
|
|
@@ -57,7 +51,7 @@ function queueReplacement(options) {
|
|
|
57
51
|
if (!isRecord(options.source) || typeof options.source.value !== 'string') {
|
|
58
52
|
return;
|
|
59
53
|
}
|
|
60
|
-
const mappedPath =
|
|
54
|
+
const mappedPath = resolveBrowserRuntimePublicPath(options.source.value, options.manifest);
|
|
61
55
|
if (!mappedPath || typeof options.source.start !== 'number' || typeof options.source.end !== 'number') {
|
|
62
56
|
return;
|
|
63
57
|
}
|
|
@@ -74,8 +68,8 @@ function queueReplacement(options) {
|
|
|
74
68
|
*
|
|
75
69
|
* Exposed for the post-build rewriter and for tests.
|
|
76
70
|
*/
|
|
77
|
-
export function rewriteBrowserRuntimeImports(code,
|
|
78
|
-
if (
|
|
71
|
+
export function rewriteBrowserRuntimeImports(code, manifest, filePath = 'browser-runtime-imports.js') {
|
|
72
|
+
if (manifest.assets.length === 0) {
|
|
79
73
|
return code;
|
|
80
74
|
}
|
|
81
75
|
const edits = [];
|
|
@@ -88,11 +82,11 @@ export function rewriteBrowserRuntimeImports(code, specifierMap, filePath = 'bro
|
|
|
88
82
|
if (node.type === 'ImportDeclaration' ||
|
|
89
83
|
node.type === 'ExportNamedDeclaration' ||
|
|
90
84
|
node.type === 'ExportAllDeclaration') {
|
|
91
|
-
queueReplacement({ code, source: node.source,
|
|
85
|
+
queueReplacement({ code, source: node.source, manifest, edits });
|
|
92
86
|
}
|
|
93
87
|
if (node.type === 'ImportExpression' && isRecord(node.source)) {
|
|
94
88
|
if (node.source.type === 'StringLiteral' || node.source.type === 'Literal') {
|
|
95
|
-
queueReplacement({ code, source: node.source,
|
|
89
|
+
queueReplacement({ code, source: node.source, manifest, edits });
|
|
96
90
|
}
|
|
97
91
|
}
|
|
98
92
|
for (const value of Object.values(node)) {
|
|
@@ -121,23 +115,31 @@ export function rewriteBrowserRuntimeImports(code, specifierMap, filePath = 'bro
|
|
|
121
115
|
}
|
|
122
116
|
return rewritten;
|
|
123
117
|
}
|
|
118
|
+
function importMightReferenceRuntimeRoots(code, rootSpecifiers) {
|
|
119
|
+
return rootSpecifiers.some((specifier) => code.includes(specifier));
|
|
120
|
+
}
|
|
121
|
+
export function getBrowserRuntimeManifestFromPlugin(plugin) {
|
|
122
|
+
return plugin[BROWSER_RUNTIME_MANIFEST];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Returns the exact specifier → publicPath map derived from the plugin manifest.
|
|
126
|
+
*/
|
|
124
127
|
export function getBrowserRuntimeImportRewriteMap(plugin) {
|
|
125
|
-
|
|
128
|
+
const manifest = getBrowserRuntimeManifestFromPlugin(plugin);
|
|
129
|
+
return manifest ? getBrowserRuntimeSpecifierMap(manifest) : undefined;
|
|
126
130
|
}
|
|
127
|
-
export function
|
|
128
|
-
const
|
|
131
|
+
export function collectBrowserRuntimeManifests(plugins) {
|
|
132
|
+
const manifests = [];
|
|
129
133
|
for (const plugin of plugins ?? []) {
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
for (const [specifier, publicPath] of specifierMap.entries()) {
|
|
135
|
-
if (!merged.has(specifier)) {
|
|
136
|
-
merged.set(specifier, publicPath);
|
|
137
|
-
}
|
|
134
|
+
const manifest = getBrowserRuntimeManifestFromPlugin(plugin);
|
|
135
|
+
if (manifest) {
|
|
136
|
+
manifests.push(manifest);
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
|
-
return
|
|
139
|
+
return manifests;
|
|
140
|
+
}
|
|
141
|
+
export function collectBrowserRuntimeImportRewriteMap(plugins) {
|
|
142
|
+
return getBrowserRuntimeSpecifierMap(mergeBrowserRuntimeManifests(...collectBrowserRuntimeManifests(plugins)));
|
|
141
143
|
}
|
|
142
144
|
/**
|
|
143
145
|
* Creates a build plugin that exposes the union of browser-runtime
|
|
@@ -159,12 +161,12 @@ export function createBrowserRuntimePlugin(options) {
|
|
|
159
161
|
const rewriteImports = options.rewriteImports ?? true;
|
|
160
162
|
const matchPublicPaths = options.matchPublicPaths ?? true;
|
|
161
163
|
const publicPathSet = new Set(specifierMap.values());
|
|
162
|
-
const
|
|
164
|
+
const rootSpecifiers = manifest.assets.map((asset) => asset.specifier);
|
|
163
165
|
const plugin = {
|
|
164
166
|
name: options.name ?? DEFAULT_BROWSER_RUNTIME_PLUGIN_NAME,
|
|
165
167
|
setup(build) {
|
|
166
168
|
build.onResolve({ filter: specifierFilter }, (args) => {
|
|
167
|
-
const mappedPath =
|
|
169
|
+
const mappedPath = resolveBrowserRuntimePublicPath(args.path, manifest);
|
|
168
170
|
if (!mappedPath) {
|
|
169
171
|
return undefined;
|
|
170
172
|
}
|
|
@@ -195,10 +197,10 @@ export function createBrowserRuntimePlugin(options) {
|
|
|
195
197
|
* any manifest-owned specifiers. This avoids expensive oxc-parser calls
|
|
196
198
|
* on every JS/TS file in the dependency graph.
|
|
197
199
|
*/
|
|
198
|
-
if (!
|
|
200
|
+
if (!importMightReferenceRuntimeRoots(code, rootSpecifiers)) {
|
|
199
201
|
return undefined;
|
|
200
202
|
}
|
|
201
|
-
const rewritten = rewriteBrowserRuntimeImports(code,
|
|
203
|
+
const rewritten = rewriteBrowserRuntimeImports(code, manifest, args.path);
|
|
202
204
|
if (rewritten === code) {
|
|
203
205
|
return undefined;
|
|
204
206
|
}
|
|
@@ -211,6 +213,6 @@ export function createBrowserRuntimePlugin(options) {
|
|
|
211
213
|
}
|
|
212
214
|
},
|
|
213
215
|
};
|
|
214
|
-
plugin[
|
|
216
|
+
plugin[BROWSER_RUNTIME_MANIFEST] = manifest;
|
|
215
217
|
return plugin;
|
|
216
218
|
}
|
|
@@ -12,7 +12,8 @@ import { builtinModules, createRequire } from 'node:module';
|
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { isBarePackageImportSpecifier } from '../../plugins/tsconfig-import-resolver.js';
|
|
15
|
-
import {
|
|
15
|
+
import { collectBrowserRuntimeManifests, rewriteBrowserRuntimeImports } from '../browser/browser-runtime-plugin.js';
|
|
16
|
+
import { mergeBrowserRuntimeManifests } from '../browser/browser-runtime-manifest.js';
|
|
16
17
|
import { createServerSideCssShimPlugin } from './server-side-css-shim-plugin.js';
|
|
17
18
|
import { createRolldownPluginBridge } from './rolldown-plugin-bridge.js';
|
|
18
19
|
import { isDeclaredAppPackageImport, isWorkspacePackageImport, normalizeNodeRuntimeBuildOutputs, } from './runtime-build-output-normalizer.js';
|
|
@@ -374,13 +375,17 @@ export function rewriteBrowserRuntimeImportsInOutputs(result, contextRoot, plugi
|
|
|
374
375
|
if (!result.success || result.outputs.length === 0) {
|
|
375
376
|
return result;
|
|
376
377
|
}
|
|
377
|
-
const
|
|
378
|
-
|
|
378
|
+
const manifests = collectBrowserRuntimeManifests(plugins);
|
|
379
|
+
const manifest = mergeBrowserRuntimeManifests(...manifests);
|
|
380
|
+
if (manifest.assets.length === 0) {
|
|
379
381
|
return result;
|
|
380
382
|
}
|
|
381
383
|
const moduleRequireFromContext = createRequire(path.join(contextRoot, 'package.json'));
|
|
382
384
|
const fs = moduleRequireFromContext('node:fs');
|
|
383
|
-
const cacheFingerprint =
|
|
385
|
+
const cacheFingerprint = manifest.assets
|
|
386
|
+
.map((asset) => `${asset.specifier}->${asset.publicPath}`)
|
|
387
|
+
.sort()
|
|
388
|
+
.join('|');
|
|
384
389
|
for (const output of result.outputs) {
|
|
385
390
|
if (!/\.(?:[cm]?js)$/u.test(output.path)) {
|
|
386
391
|
continue;
|
|
@@ -390,7 +395,7 @@ export function rewriteBrowserRuntimeImportsInOutputs(result, contextRoot, plugi
|
|
|
390
395
|
if (rewriteCache.has(contentKey)) {
|
|
391
396
|
continue;
|
|
392
397
|
}
|
|
393
|
-
const rewritten = rewriteBrowserRuntimeImports(code,
|
|
398
|
+
const rewritten = rewriteBrowserRuntimeImports(code, manifest, output.path);
|
|
394
399
|
if (rewritten !== code) {
|
|
395
400
|
fs.writeFileSync(output.path, rewritten);
|
|
396
401
|
}
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { fileSystem } from '@ecopages/file-system';
|
|
3
3
|
import { cachedParseSync } from '../../cache/module-parse-cache.js';
|
|
4
4
|
import { isBarePackageImportSpecifier, resolveProjectModulePath } from '../../plugins/tsconfig-import-resolver.js';
|
|
5
|
+
import { resolveRuntimeSpecifierPublicPath } from '../../build/browser/browser-runtime-manifest.js';
|
|
5
6
|
import { resolveDevTransformModuleUrl } from './dev-transform-url.js';
|
|
6
7
|
/**
|
|
7
8
|
* Builds a browser-importable dev-transform URL that changes when the source changes.
|
|
@@ -106,7 +107,7 @@ export async function rewriteModuleImports(options) {
|
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
async function resolveImportSpecifier(options) {
|
|
109
|
-
const runtimeUrl = options.
|
|
110
|
+
const runtimeUrl = resolveRuntimeSpecifierPublicPath(options.specifier, options.runtimeSpecifierMap);
|
|
110
111
|
if (runtimeUrl) {
|
|
111
112
|
return runtimeUrl;
|
|
112
113
|
}
|
|
@@ -4,9 +4,17 @@ import { fileSystem } from '@ecopages/file-system';
|
|
|
4
4
|
import { RESOLVED_ASSETS_VENDORS_DIR } from '../../config/constants.js';
|
|
5
5
|
import { getAppBrowserBuildPlugins } from '../../build/build-adapter.js';
|
|
6
6
|
import { resolveBarePackageBrowserEntry } from '../../plugins/tsconfig-import-resolver.js';
|
|
7
|
+
import { toPackageRootSpecifier } from '../../plugins/package-specifier.js';
|
|
8
|
+
import { resolveRuntimeSpecifierPublicPath } from '../../build/browser/browser-runtime-manifest.js';
|
|
7
9
|
import { BrowserBundleService } from '../../services/assets/browser-bundle.service.js';
|
|
8
|
-
const
|
|
10
|
+
const BROWSER_FIRST_VENDOR_CONDITIONS = ['browser', 'module', 'import', 'default'];
|
|
11
|
+
const MODULE_FIRST_VENDOR_CONDITIONS = ['module', 'import', 'default'];
|
|
9
12
|
const BROWSER_NODE_BUILTIN_GUARD_MARKER = '[browser-build] Node builtin';
|
|
13
|
+
function getVendorBundleConditions(specifier) {
|
|
14
|
+
return toPackageRootSpecifier(specifier) === '@ecopages/core'
|
|
15
|
+
? BROWSER_FIRST_VENDOR_CONDITIONS
|
|
16
|
+
: MODULE_FIRST_VENDOR_CONDITIONS;
|
|
17
|
+
}
|
|
10
18
|
/**
|
|
11
19
|
* Lazily prebundles bare npm imports into cacheable `/assets/vendors/*` chunks.
|
|
12
20
|
*/
|
|
@@ -27,7 +35,7 @@ export class DevTransformVendorRegistry {
|
|
|
27
35
|
this.vendorsDir = path.join(distDir, RESOLVED_ASSETS_VENDORS_DIR);
|
|
28
36
|
}
|
|
29
37
|
resolveKnownVendorUrl(specifier) {
|
|
30
|
-
return this.getRuntimeSpecifierMap()
|
|
38
|
+
return resolveRuntimeSpecifierPublicPath(specifier, this.getRuntimeSpecifierMap());
|
|
31
39
|
}
|
|
32
40
|
async resolveVendorUrl(specifier) {
|
|
33
41
|
const known = this.resolveKnownVendorUrl(specifier);
|
|
@@ -114,7 +122,7 @@ export class DevTransformVendorRegistry {
|
|
|
114
122
|
externalPackages: false,
|
|
115
123
|
treeshaking: true,
|
|
116
124
|
splitting: false,
|
|
117
|
-
conditions: [...
|
|
125
|
+
conditions: [...getVendorBundleConditions(specifier)],
|
|
118
126
|
excludeAppBuildPlugins: getAppBrowserBuildPlugins(this.appConfig).map((plugin) => plugin.name),
|
|
119
127
|
plugins: [...vendorPlugins],
|
|
120
128
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes bare npm import specifiers to their package root.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* toPackageRootSpecifier('@scope/pkg/sub/file') -> '@scope/pkg'
|
|
6
|
+
* toPackageRootSpecifier('lodash/debounce') -> 'lodash'
|
|
7
|
+
*/
|
|
8
|
+
export declare function toPackageRootSpecifier(specifier: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Returns true when `specifier` is a subpath import of `packageRoot`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isSubpathOfPackageRoot(specifier: string, packageRoot: string): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes bare npm import specifiers to their package root.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* toPackageRootSpecifier('@scope/pkg/sub/file') -> '@scope/pkg'
|
|
6
|
+
* toPackageRootSpecifier('lodash/debounce') -> 'lodash'
|
|
7
|
+
*/
|
|
8
|
+
export function toPackageRootSpecifier(specifier) {
|
|
9
|
+
if (specifier.startsWith('.') ||
|
|
10
|
+
specifier.startsWith('/') ||
|
|
11
|
+
specifier.startsWith('node:') ||
|
|
12
|
+
specifier.includes('://')) {
|
|
13
|
+
return specifier;
|
|
14
|
+
}
|
|
15
|
+
if (specifier.startsWith('@')) {
|
|
16
|
+
const [scope, name, ...rest] = specifier.split('/');
|
|
17
|
+
if (!scope || !name) {
|
|
18
|
+
return specifier;
|
|
19
|
+
}
|
|
20
|
+
return rest.length > 0 ? `${scope}/${name}` : specifier;
|
|
21
|
+
}
|
|
22
|
+
const [name] = specifier.split('/');
|
|
23
|
+
return name ?? specifier;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns true when `specifier` is a subpath import of `packageRoot`.
|
|
27
|
+
*/
|
|
28
|
+
export function isSubpathOfPackageRoot(specifier, packageRoot) {
|
|
29
|
+
if (specifier === packageRoot) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return specifier.startsWith(`${packageRoot}/`);
|
|
33
|
+
}
|
|
@@ -12,9 +12,11 @@ export declare function isBarePackageImportSpecifier(specifier: string, projectR
|
|
|
12
12
|
* Resolves a bare npm package entry for browser vendor prebundles.
|
|
13
13
|
*
|
|
14
14
|
* @remarks
|
|
15
|
-
*
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
15
|
+
* Framework-owned dual packages such as `@ecopages/core` keep browser-first
|
|
16
|
+
* `mainFields` and `conditionNames` so vendor prebundles pick the browser facade.
|
|
17
|
+
* Third-party packages prefer ESM (`module` / `import`) and omit the `browser`
|
|
18
|
+
* export condition so legacy UMD `exports.browser` entries cannot win during
|
|
19
|
+
* Rolldown bundling.
|
|
18
20
|
*/
|
|
19
21
|
export declare function resolveBarePackageBrowserEntry(projectRoot: string, specifier: string): string | undefined;
|
|
20
22
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ResolverFactory } from 'oxc-resolver';
|
|
4
|
+
import { toPackageRootSpecifier } from './package-specifier.js';
|
|
4
5
|
const RESOLVABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mdx'];
|
|
5
6
|
const resolverCache = new Map();
|
|
6
7
|
const pathPrefixCache = new Map();
|
|
@@ -111,33 +112,59 @@ function getResolverFactory(projectRoot) {
|
|
|
111
112
|
resolverCache.set(projectRoot, resolver);
|
|
112
113
|
return resolver;
|
|
113
114
|
}
|
|
114
|
-
const
|
|
115
|
-
|
|
115
|
+
const BROWSER_FIRST_CONDITION_NAMES = ['browser', 'module', 'import', 'default'];
|
|
116
|
+
/**
|
|
117
|
+
* Omit `browser` so package `exports` cannot select a legacy UMD facade when both
|
|
118
|
+
* `browser` and `import` conditions are published. Legacy `mainFields.browser`
|
|
119
|
+
* remains available after `module` for packages without `exports`.
|
|
120
|
+
*/
|
|
121
|
+
const MODULE_FIRST_CONDITION_NAMES = ['module', 'import', 'default'];
|
|
122
|
+
const BROWSER_FIRST_MAIN_FIELDS = ['browser', 'module', 'main'];
|
|
123
|
+
const MODULE_FIRST_MAIN_FIELDS = ['module', 'browser', 'main'];
|
|
124
|
+
/**
|
|
125
|
+
* Package roots that ship an explicit browser facade and must keep browser-first
|
|
126
|
+
* resolution during vendor prebundles.
|
|
127
|
+
*
|
|
128
|
+
* @remarks
|
|
129
|
+
* Prefer expanding this set only for dual packages whose `"browser"` / `exports.browser`
|
|
130
|
+
* entry is the intentional client facade (for example `@ecopages/core`).
|
|
131
|
+
*/
|
|
132
|
+
const BROWSER_FIRST_PACKAGE_ROOTS = new Set(['@ecopages/core']);
|
|
116
133
|
const browserPackageResolvers = new Map();
|
|
117
|
-
function
|
|
134
|
+
function usesBrowserFirstPackageResolution(specifier) {
|
|
135
|
+
const packageRoot = toPackageRootSpecifier(specifier);
|
|
136
|
+
return BROWSER_FIRST_PACKAGE_ROOTS.has(packageRoot);
|
|
137
|
+
}
|
|
138
|
+
function getBrowserPackageResolver(projectRoot, mainFields, conditionNames) {
|
|
118
139
|
const normalizedRoot = path.resolve(projectRoot);
|
|
119
|
-
const
|
|
140
|
+
const cacheKey = `${normalizedRoot}\0${mainFields.join(',')}\0${conditionNames.join(',')}`;
|
|
141
|
+
const cached = browserPackageResolvers.get(cacheKey);
|
|
120
142
|
if (cached) {
|
|
121
143
|
return cached;
|
|
122
144
|
}
|
|
123
145
|
const resolver = new ResolverFactory({
|
|
124
|
-
conditionNames: [...
|
|
125
|
-
mainFields: [...
|
|
146
|
+
conditionNames: [...conditionNames],
|
|
147
|
+
mainFields: [...mainFields],
|
|
126
148
|
extensions: [...RESOLVABLE_EXTENSIONS],
|
|
127
149
|
});
|
|
128
|
-
browserPackageResolvers.set(
|
|
150
|
+
browserPackageResolvers.set(cacheKey, resolver);
|
|
129
151
|
return resolver;
|
|
130
152
|
}
|
|
131
153
|
/**
|
|
132
154
|
* Resolves a bare npm package entry for browser vendor prebundles.
|
|
133
155
|
*
|
|
134
156
|
* @remarks
|
|
135
|
-
*
|
|
136
|
-
* `
|
|
137
|
-
*
|
|
157
|
+
* Framework-owned dual packages such as `@ecopages/core` keep browser-first
|
|
158
|
+
* `mainFields` and `conditionNames` so vendor prebundles pick the browser facade.
|
|
159
|
+
* Third-party packages prefer ESM (`module` / `import`) and omit the `browser`
|
|
160
|
+
* export condition so legacy UMD `exports.browser` entries cannot win during
|
|
161
|
+
* Rolldown bundling.
|
|
138
162
|
*/
|
|
139
163
|
export function resolveBarePackageBrowserEntry(projectRoot, specifier) {
|
|
140
|
-
const
|
|
164
|
+
const browserFirst = usesBrowserFirstPackageResolution(specifier);
|
|
165
|
+
const mainFields = browserFirst ? BROWSER_FIRST_MAIN_FIELDS : MODULE_FIRST_MAIN_FIELDS;
|
|
166
|
+
const conditionNames = browserFirst ? BROWSER_FIRST_CONDITION_NAMES : MODULE_FIRST_CONDITION_NAMES;
|
|
167
|
+
const result = getBrowserPackageResolver(projectRoot, mainFields, conditionNames).sync(projectRoot, specifier);
|
|
141
168
|
if (result.error || !result.path) {
|
|
142
169
|
return undefined;
|
|
143
170
|
}
|
|
@@ -5,7 +5,7 @@ import { createPageLocalsProxy } from './route-prepared-options.utils.js';
|
|
|
5
5
|
* Assembles the final integration render options after route prep dependencies resolve.
|
|
6
6
|
*/
|
|
7
7
|
export function buildPreparedRenderOptions(input) {
|
|
8
|
-
const { routeOptions, resolvedInputs, resolvedPageDependencyComponents, resolvedDependencies, allDependencies, pageBrowserGraph,
|
|
8
|
+
const { routeOptions, resolvedInputs, resolvedPageDependencyComponents, resolvedDependencies, allDependencies, pageBrowserGraph, } = input;
|
|
9
9
|
const { Page, HtmlTemplate, Layouts, Layout, layoutEntries, props, metadata, integrationSpecificProps } = resolvedInputs;
|
|
10
10
|
const dedupedDependencies = dedupeProcessedAssets(allDependencies);
|
|
11
11
|
const pagePackage = createPagePackage(dedupedDependencies, { pageBrowserGraph });
|
|
@@ -16,13 +16,8 @@ export function buildPreparedRenderOptions(input) {
|
|
|
16
16
|
query: routeOptions.query || {},
|
|
17
17
|
};
|
|
18
18
|
const cacheStrategy = Page.cache;
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const localsAvailable = effectiveCacheStrategy === 'dynamic' && routeOptions.locals !== undefined;
|
|
22
|
-
const pageLocals = localsAvailable
|
|
23
|
-
? routeOptions.locals
|
|
24
|
-
: createPageLocalsProxy(routeOptions.file);
|
|
25
|
-
const locals = localsAvailable ? routeOptions.locals : undefined;
|
|
19
|
+
const pageLocals = routeOptions.locals ?? createPageLocalsProxy(routeOptions.file);
|
|
20
|
+
const locals = routeOptions.locals;
|
|
26
21
|
const preparedOptions = {
|
|
27
22
|
...routeOptions,
|
|
28
23
|
resolvedPageDependencyComponents,
|
|
@@ -89,6 +89,11 @@ export declare class AssetProcessingService {
|
|
|
89
89
|
* Stores one processed asset in the dependency cache.
|
|
90
90
|
*/
|
|
91
91
|
private setCachedAsset;
|
|
92
|
+
/**
|
|
93
|
+
* Removes a cached asset and advances the development HTML cache generation
|
|
94
|
+
* when the removed asset can determine browser runtime URLs.
|
|
95
|
+
*/
|
|
96
|
+
private invalidateCachedAsset;
|
|
92
97
|
private getFileSourceHash;
|
|
93
98
|
/**
|
|
94
99
|
* Clears all cached processed assets.
|
|
@@ -9,6 +9,7 @@ import { isHmrAware } from './processor.interface.js';
|
|
|
9
9
|
import { ProcessorRegistry } from './processor.registry.js';
|
|
10
10
|
import { processUngroupedDependency } from './ungrouped-dependency-processing.js';
|
|
11
11
|
import { materializeContentScriptAsset } from './materialize-content-script-asset.js';
|
|
12
|
+
import { bumpBrowserRuntimeAssetGeneration } from '../browser-runtime-asset-generation.js';
|
|
12
13
|
import { ContentScriptProcessor, ContentStylesheetProcessor, FileScriptProcessor, FileStylesheetProcessor, NodeModuleScriptProcessor, } from './processors/index.js';
|
|
13
14
|
/**
|
|
14
15
|
* Processes declared component and page asset dependencies for one app instance.
|
|
@@ -234,7 +235,7 @@ export class AssetProcessingService {
|
|
|
234
235
|
}
|
|
235
236
|
const sourceHash = this.getFileSourceHash(dep);
|
|
236
237
|
if (dep.source === 'file' && sourceHash === undefined) {
|
|
237
|
-
this.
|
|
238
|
+
this.invalidateCachedAsset(depKey);
|
|
238
239
|
return null;
|
|
239
240
|
}
|
|
240
241
|
const cached = this.cache.get(depKey);
|
|
@@ -242,11 +243,11 @@ export class AssetProcessingService {
|
|
|
242
243
|
return null;
|
|
243
244
|
}
|
|
244
245
|
if (cached.sourceHash !== sourceHash) {
|
|
245
|
-
this.
|
|
246
|
+
this.invalidateCachedAsset(depKey);
|
|
246
247
|
return null;
|
|
247
248
|
}
|
|
248
249
|
if (cached.asset.filepath && !fileSystem.exists(cached.asset.filepath)) {
|
|
249
|
-
this.
|
|
250
|
+
this.invalidateCachedAsset(depKey);
|
|
250
251
|
return null;
|
|
251
252
|
}
|
|
252
253
|
return cached.asset;
|
|
@@ -275,10 +276,26 @@ export class AssetProcessingService {
|
|
|
275
276
|
* Stores one processed asset in the dependency cache.
|
|
276
277
|
*/
|
|
277
278
|
setCachedAsset(dep, depKey, asset) {
|
|
279
|
+
const previous = this.cache.get(depKey)?.asset;
|
|
280
|
+
const tracksBrowserRuntimeGeneration = dep.packageRole === 'runtime' || dep.packageRole === 'page-script';
|
|
278
281
|
this.cache.set(depKey, {
|
|
279
282
|
asset,
|
|
280
283
|
sourceHash: this.getFileSourceHash(dep),
|
|
281
284
|
});
|
|
285
|
+
if (tracksBrowserRuntimeGeneration && previous !== undefined && previous.filepath !== asset.filepath) {
|
|
286
|
+
void bumpBrowserRuntimeAssetGeneration(this.config);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Removes a cached asset and advances the development HTML cache generation
|
|
291
|
+
* when the removed asset can determine browser runtime URLs.
|
|
292
|
+
*/
|
|
293
|
+
invalidateCachedAsset(depKey) {
|
|
294
|
+
const cached = this.cache.get(depKey);
|
|
295
|
+
this.cache.delete(depKey);
|
|
296
|
+
if (cached?.asset.packageRole === 'runtime' || cached?.asset.packageRole === 'page-script') {
|
|
297
|
+
void bumpBrowserRuntimeAssetGeneration(this.config);
|
|
298
|
+
}
|
|
282
299
|
}
|
|
283
300
|
getFileSourceHash(dep) {
|
|
284
301
|
if (dep.source !== 'file' || !('filepath' in dep) || !fileSystem.exists(dep.filepath)) {
|
|
@@ -290,19 +307,29 @@ export class AssetProcessingService {
|
|
|
290
307
|
* Clears all cached processed assets.
|
|
291
308
|
*/
|
|
292
309
|
clearCache() {
|
|
310
|
+
const invalidatesBrowserRuntime = Array.from(this.cache.values()).some(({ asset }) => asset.packageRole === 'runtime' || asset.packageRole === 'page-script');
|
|
293
311
|
this.cache.clear();
|
|
312
|
+
if (invalidatesBrowserRuntime) {
|
|
313
|
+
void bumpBrowserRuntimeAssetGeneration(this.config);
|
|
314
|
+
}
|
|
294
315
|
}
|
|
295
316
|
/**
|
|
296
317
|
* Removes cached assets that were produced from the given file path.
|
|
297
318
|
*/
|
|
298
319
|
invalidateCacheForFile(filepath) {
|
|
320
|
+
let invalidatesBrowserRuntime = false;
|
|
299
321
|
for (const [key, value] of this.cache.entries()) {
|
|
300
322
|
if (value.asset.filepath === filepath ||
|
|
301
323
|
value.asset.sourceFilepath === filepath ||
|
|
302
324
|
value.asset.bundledSourceFilepaths?.includes(filepath)) {
|
|
325
|
+
invalidatesBrowserRuntime ||=
|
|
326
|
+
value.asset.packageRole === 'runtime' || value.asset.packageRole === 'page-script';
|
|
303
327
|
this.cache.delete(key);
|
|
304
328
|
}
|
|
305
329
|
}
|
|
330
|
+
if (invalidatesBrowserRuntime) {
|
|
331
|
+
void bumpBrowserRuntimeAssetGeneration(this.config);
|
|
332
|
+
}
|
|
306
333
|
}
|
|
307
334
|
/**
|
|
308
335
|
* Creates a service prewired with the default core processors.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EcoPagesAppConfig } from '../../types/internal-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the current browser-runtime asset generation for one app.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Development HTML caches include this generation so rebuilt bootstrap and
|
|
7
|
+
* vendor URLs cannot leave stale script references behind.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getBrowserRuntimeAssetGeneration(appConfig: EcoPagesAppConfig): number;
|
|
10
|
+
/**
|
|
11
|
+
* Bumps browser-runtime asset generation and clears rendered HTML cache entries.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* Only call this when a previously cached runtime/page-script asset filepath changes.
|
|
15
|
+
* First inserts must not bump generation or cold start thrash-clears the HTML cache.
|
|
16
|
+
*/
|
|
17
|
+
export declare function bumpBrowserRuntimeAssetGeneration(appConfig: EcoPagesAppConfig): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { clearAppPageCache } from '../cache/page-cache-service.js';
|
|
2
|
+
import { isDevelopmentRuntime } from '../../utils/runtime.js';
|
|
3
|
+
const generationByAppConfig = new WeakMap();
|
|
4
|
+
/**
|
|
5
|
+
* Returns the current browser-runtime asset generation for one app.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Development HTML caches include this generation so rebuilt bootstrap and
|
|
9
|
+
* vendor URLs cannot leave stale script references behind.
|
|
10
|
+
*/
|
|
11
|
+
export function getBrowserRuntimeAssetGeneration(appConfig) {
|
|
12
|
+
return generationByAppConfig.get(appConfig) ?? 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Bumps browser-runtime asset generation and clears rendered HTML cache entries.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* Only call this when a previously cached runtime/page-script asset filepath changes.
|
|
19
|
+
* First inserts must not bump generation or cold start thrash-clears the HTML cache.
|
|
20
|
+
*/
|
|
21
|
+
export async function bumpBrowserRuntimeAssetGeneration(appConfig) {
|
|
22
|
+
if (!isDevelopmentRuntime()) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const nextGeneration = getBrowserRuntimeAssetGeneration(appConfig) + 1;
|
|
26
|
+
generationByAppConfig.set(appConfig, nextGeneration);
|
|
27
|
+
await clearAppPageCache(appConfig);
|
|
28
|
+
}
|
|
@@ -10,7 +10,8 @@ import type { CacheStrategy, RenderResult } from './cache.types.js';
|
|
|
10
10
|
export declare class PageRequestCacheCoordinator {
|
|
11
11
|
private cacheService;
|
|
12
12
|
private defaultCacheStrategy;
|
|
13
|
-
|
|
13
|
+
private readonly getRuntimeAssetGeneration?;
|
|
14
|
+
constructor(cacheService: PageCacheService | null, defaultCacheStrategy: CacheStrategy, getRuntimeAssetGeneration?: () => number);
|
|
14
15
|
/**
|
|
15
16
|
* Builds the cache key used for page lookups.
|
|
16
17
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCacheControlHeader } from './page-cache-service.js';
|
|
2
2
|
import { getRequestPipelineMetricsHeaderName, isRequestPipelineMetricsEnabled, serializeRequestPipelineMetricsHeader, } from '../../diagnostics/request-pipeline-metrics.js';
|
|
3
|
+
import { isDevelopmentRuntime } from '../../utils/runtime.js';
|
|
3
4
|
/**
|
|
4
5
|
* Coordinates request-time page caching concerns around one render invocation.
|
|
5
6
|
*
|
|
@@ -10,9 +11,11 @@ import { getRequestPipelineMetricsHeaderName, isRequestPipelineMetricsEnabled, s
|
|
|
10
11
|
export class PageRequestCacheCoordinator {
|
|
11
12
|
cacheService;
|
|
12
13
|
defaultCacheStrategy;
|
|
13
|
-
|
|
14
|
+
getRuntimeAssetGeneration;
|
|
15
|
+
constructor(cacheService, defaultCacheStrategy, getRuntimeAssetGeneration) {
|
|
14
16
|
this.cacheService = cacheService;
|
|
15
17
|
this.defaultCacheStrategy = defaultCacheStrategy;
|
|
18
|
+
this.getRuntimeAssetGeneration = getRuntimeAssetGeneration;
|
|
16
19
|
}
|
|
17
20
|
/**
|
|
18
21
|
* Builds the cache key used for page lookups.
|
|
@@ -29,6 +32,9 @@ export class PageRequestCacheCoordinator {
|
|
|
29
32
|
const queryString = new URLSearchParams(input.query).toString();
|
|
30
33
|
key += `?${queryString}`;
|
|
31
34
|
}
|
|
35
|
+
if (this.getRuntimeAssetGeneration && isDevelopmentRuntime()) {
|
|
36
|
+
key += `#__eco_rt=${this.getRuntimeAssetGeneration()}`;
|
|
37
|
+
}
|
|
32
38
|
return key;
|
|
33
39
|
}
|
|
34
40
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { getAppServerInvalidationState } from '../runtime-state/server-invalidation-state.service.js';
|
|
3
3
|
import { appLogger } from '../../global/app-logger.js';
|
|
4
|
+
import { clearAppDevelopmentRouteModuleBuildCaches } from '../module-loading/route-module-build-cache-registry.js';
|
|
4
5
|
/**
|
|
5
6
|
* Framework-owned development invalidation service.
|
|
6
7
|
*
|
|
@@ -29,6 +30,7 @@ export class DevelopmentInvalidationService {
|
|
|
29
30
|
invalidateServerModules(changedFiles) {
|
|
30
31
|
getAppServerInvalidationState(this.appConfig).invalidateServerModules(changedFiles);
|
|
31
32
|
this.appConfig.runtime?.appModuleLoader?.invalidateDevelopmentGraph();
|
|
33
|
+
clearAppDevelopmentRouteModuleBuildCaches(this.appConfig);
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Registers an integration-owned handler for registered script entrypoint edits.
|
|
@@ -57,8 +59,8 @@ export class DevelopmentInvalidationService {
|
|
|
57
59
|
* Resets runtime-owned graph state and invalidates server modules.
|
|
58
60
|
*/
|
|
59
61
|
resetRuntimeState(changedFiles) {
|
|
62
|
+
this.invalidateServerModules(changedFiles);
|
|
60
63
|
const serverInvalidationState = getAppServerInvalidationState(this.appConfig);
|
|
61
|
-
serverInvalidationState.invalidateServerModules(changedFiles);
|
|
62
64
|
serverInvalidationState.reset();
|
|
63
65
|
}
|
|
64
66
|
/**
|
|
@@ -33,7 +33,7 @@ Call site (route scan, renderer, SSG, API)
|
|
|
33
33
|
2. **Disk transpile cache** (`.eco/.server-modules/.build-cache.json`) — production and stable development graphs when dependency hashes match. Manifest field `corePackageVersion` invalidates entries when the framework package changes.
|
|
34
34
|
3. **Unified graph manifest** — production static export fast path only; see build layer docs.
|
|
35
35
|
|
|
36
|
-
Development
|
|
36
|
+
Development import URLs use `sourceHash` plus a per-service import generation counter. Node uses that value in its `?update=` query. Bun also receives a generation-specific compiled output filename because it retains a previously imported file when only its query changes. Both paths advance after `invalidateDevelopmentGraph()` without a process-wide invalidation version in reuse keys.
|
|
37
37
|
|
|
38
38
|
## Files
|
|
39
39
|
|
|
@@ -49,4 +49,4 @@ Development `?update=` query params use `sourceHash` plus a per-service import g
|
|
|
49
49
|
|
|
50
50
|
## Development invalidation
|
|
51
51
|
|
|
52
|
-
`DevelopmentInvalidationService.invalidateServerModules()` calls `appModuleLoader.invalidateDevelopmentGraph()`, which clears the in-memory import cache, bumps the per-service dev import generation used
|
|
52
|
+
`DevelopmentInvalidationService.invalidateServerModules()` calls `appModuleLoader.invalidateDevelopmentGraph()`, which clears the in-memory import cache, bumps the per-service dev import generation used for runtime URLs and Bun output filenames, and clears dependency-hash memoization. It also clears persisted route-module entries: externalized generated modules can change without appearing in a route bundle's dependency graph, so retaining those entries could reload stale server HTML.
|
|
@@ -127,10 +127,7 @@ export class PageModuleImportService {
|
|
|
127
127
|
async loadModule(options) {
|
|
128
128
|
const { filePath, fileHash, importCacheKey } = options;
|
|
129
129
|
const { rootDir, outdir, splitting, externalPackages, transpileErrorMessage = (details) => `Error transpiling page module: ${details}`, noOutputMessage = (targetFilePath) => `No transpiled output generated for page module: ${targetFilePath}`, } = options;
|
|
130
|
-
const outputFileName = resolvePageModuleOutputFileName({
|
|
131
|
-
filePath,
|
|
132
|
-
fileHash,
|
|
133
|
-
});
|
|
130
|
+
const outputFileName = createRuntimeBuildOutputFileName(resolvePageModuleOutputFileName({ filePath, fileHash }), this.developmentImportGeneration);
|
|
134
131
|
const outputNamingTemplate = outputFileName.replace(/\.mjs$/u, '.[ext]');
|
|
135
132
|
const preferredOutputPath = path.join(outdir, outputFileName);
|
|
136
133
|
const buildOptions = this.appConfig
|
|
@@ -229,3 +226,17 @@ function createRuntimeModuleUrl(filePath, fileHash, developmentImportGeneration)
|
|
|
229
226
|
function shouldAddRuntimeUpdateQuery() {
|
|
230
227
|
return process.env.NODE_ENV === 'development';
|
|
231
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Gives Bun a distinct compiled module path after development invalidation.
|
|
231
|
+
*
|
|
232
|
+
* @remarks
|
|
233
|
+
* Bun does not reload an already-imported module when only its URL query changes.
|
|
234
|
+
* A dependency edit can leave the entrypoint source hash unchanged, so the output
|
|
235
|
+
* filename must include the import generation as well as the runtime query.
|
|
236
|
+
*/
|
|
237
|
+
function createRuntimeBuildOutputFileName(outputFileName, developmentImportGeneration) {
|
|
238
|
+
if (typeof Bun === 'undefined' || !shouldAddRuntimeUpdateQuery()) {
|
|
239
|
+
return outputFileName;
|
|
240
|
+
}
|
|
241
|
+
return outputFileName.replace(/\.mjs$/u, `-${developmentImportGeneration}.mjs`);
|
|
242
|
+
}
|
|
@@ -22,3 +22,5 @@ export declare const getRouteModuleBuildCacheOutdir: typeof getServerModuleBuild
|
|
|
22
22
|
* generation during the same build process.
|
|
23
23
|
*/
|
|
24
24
|
export declare function getSharedRouteModuleBuildCache(outdir: string, appConfig?: EcoPagesAppConfig): RouteModuleBuildCache;
|
|
25
|
+
/** Clears route-module cache entries that can retain stale external server modules during development. */
|
|
26
|
+
export declare function clearAppDevelopmentRouteModuleBuildCaches(appConfig: EcoPagesAppConfig): void;
|
|
@@ -49,3 +49,15 @@ export function getSharedRouteModuleBuildCache(outdir, appConfig) {
|
|
|
49
49
|
caches.set(outdir, routeModuleBuildCache);
|
|
50
50
|
return routeModuleBuildCache;
|
|
51
51
|
}
|
|
52
|
+
/** Clears route-module cache entries that can retain stale external server modules during development. */
|
|
53
|
+
export function clearAppDevelopmentRouteModuleBuildCaches(appConfig) {
|
|
54
|
+
const serverModuleCacheOutdir = getServerModuleBuildCacheOutdir(appConfig);
|
|
55
|
+
const caches = appConfig.runtime?.routeModuleBuildCaches;
|
|
56
|
+
const serverModuleCache = caches?.get(serverModuleCacheOutdir) ?? new RouteModuleBuildCache(serverModuleCacheOutdir);
|
|
57
|
+
serverModuleCache.clearDevelopmentEntries();
|
|
58
|
+
for (const cache of caches?.values() ?? []) {
|
|
59
|
+
if (cache !== serverModuleCache) {
|
|
60
|
+
cache.clearDevelopmentEntries();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -51,6 +51,16 @@ export declare class RouteModuleBuildCache {
|
|
|
51
51
|
context: RouteModuleStaticRenderCacheContext;
|
|
52
52
|
}): void;
|
|
53
53
|
resetMemory(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Removes every persisted route-module entry after development invalidation.
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* Server bundles may externalize generated modules whose source dependencies
|
|
59
|
+
* are not present in the route bundle's dependency graph. Keeping an entry
|
|
60
|
+
* after its server graph changes can therefore reload a route that still
|
|
61
|
+
* imports an obsolete external bundle.
|
|
62
|
+
*/
|
|
63
|
+
clearDevelopmentEntries(): void;
|
|
54
64
|
pruneStaleRenderedOutputs(activePathnames: ReadonlySet<string>): string[];
|
|
55
65
|
private loadManifest;
|
|
56
66
|
private persistManifest;
|
|
@@ -164,6 +164,24 @@ export class RouteModuleBuildCache {
|
|
|
164
164
|
this.manifest = undefined;
|
|
165
165
|
this.manifestLoaded = false;
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Removes every persisted route-module entry after development invalidation.
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* Server bundles may externalize generated modules whose source dependencies
|
|
172
|
+
* are not present in the route bundle's dependency graph. Keeping an entry
|
|
173
|
+
* after its server graph changes can therefore reload a route that still
|
|
174
|
+
* imports an obsolete external bundle.
|
|
175
|
+
*/
|
|
176
|
+
clearDevelopmentEntries() {
|
|
177
|
+
if (!this.manifestLoaded && !this.dependencies.exists(this.manifestPath)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const manifest = createEmptyRouteModuleBuildCacheManifest();
|
|
181
|
+
this.manifest = manifest;
|
|
182
|
+
this.manifestLoaded = true;
|
|
183
|
+
this.persistManifest(manifest);
|
|
184
|
+
}
|
|
167
185
|
pruneStaleRenderedOutputs(activePathnames) {
|
|
168
186
|
if (process.env.NODE_ENV !== 'production') {
|
|
169
187
|
return [];
|
|
@@ -646,8 +646,9 @@ export type EcoLayoutComponent<T = EcoPagesElement> = EcoComponent<LayoutProps<T
|
|
|
646
646
|
export type EcoHtmlComponent<T = EcoPagesElement> = EcoComponent<HtmlTemplateProps, T>;
|
|
647
647
|
/**
|
|
648
648
|
* Props type for the semantic `404.*` page template.
|
|
649
|
-
* @remarks
|
|
650
|
-
* runtime does not currently pass
|
|
649
|
+
* @remarks Semantic error templates receive safe empty `pageLocals` rather than
|
|
650
|
+
* request-scoped locals. The runtime does not currently pass `message` / `stack`
|
|
651
|
+
* when rendering the custom 404 page.
|
|
651
652
|
*/
|
|
652
653
|
export interface Error404TemplateProps extends Omit<HtmlTemplateProps, 'children'> {
|
|
653
654
|
message: string;
|
|
@@ -658,7 +659,8 @@ export interface Error404TemplateProps extends Omit<HtmlTemplateProps, 'children
|
|
|
658
659
|
* @remarks In development, the page-pipeline passes `message` and `stack` from the
|
|
659
660
|
* thrown error when rendering this page after a failure. In production those
|
|
660
661
|
* fields are omitted so stacks are not serialized into HTML. Direct visits to
|
|
661
|
-
* `/500` also omit them.
|
|
662
|
+
* `/500` also omit them. Semantic error templates receive safe empty
|
|
663
|
+
* `pageLocals` rather than request-scoped locals.
|
|
662
664
|
*/
|
|
663
665
|
export interface Error500TemplateProps extends Omit<HtmlTemplateProps, 'children'> {
|
|
664
666
|
message?: string;
|