@ecopages/core 0.2.0-beta.38 → 0.2.0-beta.39
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/README.md +1 -0
- package/package.json +27 -3
- package/src/adapters/bun/server-adapter.js +1 -0
- package/src/adapters/node/server-adapter.js +1 -0
- package/src/adapters/shared/http/explicit-static-render-preparation.js +1 -1
- package/src/adapters/shared/http/explicit-static-route-matcher.js +1 -1
- package/src/adapters/shared/http/fs-server-response-matcher.js +15 -7
- package/src/adapters/shared/runtime/collect-dev-prewarm-plan.d.ts +11 -0
- package/src/adapters/shared/runtime/collect-dev-prewarm-plan.js +29 -0
- package/src/adapters/shared/runtime/dev-static-route-prewarm.d.ts +16 -0
- package/src/adapters/shared/runtime/dev-static-route-prewarm.js +66 -0
- package/src/adapters/shared/runtime/render-context.js +1 -1
- package/src/adapters/shared/runtime/server-adapter.d.ts +7 -1
- package/src/adapters/shared/runtime/server-adapter.js +64 -5
- package/src/build/README.md +1 -1
- package/src/build/browser/browser-runtime-plugin.js +2 -5
- package/src/build/cache/production-build-cache.d.ts +3 -2
- package/src/build/cache/production-build-cache.js +2 -2
- package/src/build/contracts/content-virtual-modules.d.ts +12 -0
- package/src/build/contracts/content-virtual-modules.js +29 -0
- package/src/build/rolldown/rolldown-adapter-helpers.js +1 -1
- package/src/build/runtime/build-request-policy.d.ts +3 -4
- package/src/build/runtime/build-request-policy.js +4 -4
- package/src/cache/index.d.ts +2 -1
- package/src/cache/index.js +2 -1
- package/src/cache/module-parse-cache.d.ts +5 -0
- package/src/cache/module-parse-cache.js +27 -3
- package/src/cache/module-transform-profiler.d.ts +8 -0
- package/src/cache/module-transform-profiler.js +8 -0
- package/src/client/view-transitions.d.ts +17 -1
- package/src/client/view-transitions.js +45 -1
- package/src/client/view-transitions.test.browser.d.ts +1 -0
- package/src/client/view-transitions.test.browser.js +56 -0
- package/src/config/config-builder.d.ts +16 -9
- package/src/config/config-builder.js +24 -14
- package/src/dev/transform-server/dev-transform-vendor-registry.d.ts +1 -0
- package/src/dev/transform-server/dev-transform-vendor-registry.js +15 -2
- package/src/diagnostics/request-pipeline-metrics.d.ts +38 -0
- package/src/diagnostics/request-pipeline-metrics.js +121 -0
- package/src/eco/component-identity.d.ts +13 -0
- package/src/eco/component-identity.js +18 -0
- package/src/eco/eco-declared-component.d.ts +3 -2
- package/src/eco/eco-declared-component.js +6 -4
- package/src/eco/eco.browser.js +2 -2
- package/src/eco/eco.js +7 -6
- package/src/eco/eco.types.d.ts +6 -5
- package/src/errors/http-error.d.ts +8 -0
- package/src/errors/http-error.js +16 -0
- package/src/errors/undeclared-component-dependency-error.d.ts +1 -1
- package/src/errors/undeclared-component-dependency-error.js +2 -2
- package/src/index.browser.d.ts +1 -0
- package/src/index.browser.js +1 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/plugins/README.md +2 -1
- package/src/plugins/eco-component-meta-plugin.d.ts +6 -102
- package/src/plugins/eco-component-meta-plugin.js +110 -391
- package/src/plugins/processor.d.ts +10 -0
- package/src/plugins/processor.js +9 -0
- package/src/route-renderer/README.md +10 -8
- package/src/route-renderer/orchestration/document-shell/document-shell-render.service.js +1 -1
- package/src/route-renderer/orchestration/foreign-child/foreign-subtree-execution.service.js +1 -1
- package/src/route-renderer/orchestration/integration-renderer.d.ts +0 -8
- package/src/route-renderer/orchestration/integration-renderer.js +6 -19
- package/src/route-renderer/orchestration/ownership-graph/component-graph-collectors.js +5 -4
- package/src/route-renderer/orchestration/ownership-graph/component-graph.js +6 -4
- package/src/route-renderer/orchestration/ownership-graph/ownership-validation.service.d.ts +0 -1
- package/src/route-renderer/orchestration/ownership-graph/ownership-validation.service.js +10 -19
- package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.d.ts +1 -0
- package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.js +12 -0
- package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.d.ts +9 -0
- package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.js +7 -0
- package/src/route-renderer/orchestration/route-pipeline/route-render-orchestrator.js +65 -33
- package/src/route-renderer/page-loading/component-dependency-collection.js +3 -2
- package/src/route-renderer/page-loading/ecopages-virtual-imports.d.ts +0 -5
- package/src/route-renderer/page-loading/ecopages-virtual-imports.js +4 -10
- package/src/route-renderer/page-loading/file-scoped-dependency-components.d.ts +2 -2
- package/src/route-renderer/page-loading/file-scoped-dependency-components.js +12 -11
- package/src/route-renderer/page-loading/lazy-trigger-planning.js +2 -1
- package/src/route-renderer/page-loading/page-module-loader.d.ts +1 -1
- package/src/route-renderer/page-loading/page-module-loader.js +8 -3
- package/src/router/README.md +8 -0
- package/src/services/README.md +5 -0
- package/src/services/assets/asset-processing-service/asset-processing.service.d.ts +1 -0
- package/src/services/assets/asset-processing-service/asset-processing.service.js +22 -5
- package/src/services/cache/cache.types.d.ts +4 -2
- package/src/services/cache/html-page-cache-dependency-index.d.ts +34 -0
- package/src/services/cache/html-page-cache-dependency-index.js +88 -0
- package/src/services/cache/index.d.ts +2 -0
- package/src/services/cache/index.js +2 -0
- package/src/services/cache/page-cache-service.d.ts +31 -1
- package/src/services/cache/page-cache-service.js +79 -3
- package/src/services/cache/page-request-cache-coordinator.service.js +4 -0
- package/src/services/module-loading/README.md +9 -6
- package/src/services/module-loading/app-server-module-transpiler.service.js +0 -3
- package/src/services/module-loading/collection-server-module-build.service.d.ts +19 -0
- package/src/services/module-loading/collection-server-module-build.service.js +83 -0
- package/src/services/module-loading/page-module-import.service.d.ts +5 -11
- package/src/services/module-loading/page-module-import.service.js +56 -73
- package/src/services/module-loading/route-module-build-cache.store.js +6 -6
- package/src/services/module-loading/route-module-build-manifest.d.ts +8 -5
- package/src/services/module-loading/route-module-build-manifest.js +20 -32
- package/src/services/module-loading/server-module-transpiler.service.d.ts +0 -2
- package/src/services/module-loading/server-module-transpiler.service.js +0 -3
- package/src/static-site-generator/static-site-generator.js +2 -2
- package/src/types/internal-types.d.ts +4 -0
- package/src/types/public-types.d.ts +18 -18
- package/src/watchers/project-watcher.js +14 -0
package/src/cache/index.js
CHANGED
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module @ecopages/core/cache
|
|
5
5
|
*/
|
|
6
|
-
export { ModuleParseCache, moduleParseCache, cachedParseSync } from './module-parse-cache.js';
|
|
6
|
+
export { ModuleParseCache, moduleParseCache, cachedParseSync, parseModuleSource, parserLanguageForFile, } from './module-parse-cache.js';
|
|
7
|
+
export { recordModuleTransformProfile } from './module-transform-profiler.js';
|
|
@@ -22,6 +22,11 @@ export type ModuleParseOptions = ParserOptions & {
|
|
|
22
22
|
*/
|
|
23
23
|
lang?: ParserOptions['lang'];
|
|
24
24
|
};
|
|
25
|
+
export type ParserLanguage = 'js' | 'jsx' | 'ts' | 'tsx';
|
|
26
|
+
/** Resolves the Oxc dialect for a source module from its file extension. */
|
|
27
|
+
export declare function parserLanguageForFile(filePath: string): ParserLanguage;
|
|
28
|
+
/** Parses an ECMAScript module with the shared, normalized cache contract. */
|
|
29
|
+
export declare function parseModuleSource(filePath: string, source: string, options?: ModuleParseOptions): ParseResult;
|
|
25
30
|
/**
|
|
26
31
|
* LRU-bounded module parse cache.
|
|
27
32
|
*
|
|
@@ -13,9 +13,25 @@
|
|
|
13
13
|
* mtime-hashed so that `touch`/`utimes` does not invalidate a
|
|
14
14
|
* still-valid parse.
|
|
15
15
|
*/
|
|
16
|
+
import { extname } from 'node:path';
|
|
16
17
|
import { parseSync } from 'oxc-parser';
|
|
17
18
|
import { rapidhash } from '../utils/hash.js';
|
|
18
19
|
const DEFAULT_MAX_ENTRIES = 10_000;
|
|
20
|
+
/** Resolves the Oxc dialect for a source module from its file extension. */
|
|
21
|
+
export function parserLanguageForFile(filePath) {
|
|
22
|
+
const extension = extname(filePath).toLowerCase();
|
|
23
|
+
if (extension === '.tsx')
|
|
24
|
+
return 'tsx';
|
|
25
|
+
if (extension === '.ts' || extension === '.mts' || extension === '.cts')
|
|
26
|
+
return 'ts';
|
|
27
|
+
if (extension === '.jsx')
|
|
28
|
+
return 'jsx';
|
|
29
|
+
return 'js';
|
|
30
|
+
}
|
|
31
|
+
/** Parses an ECMAScript module with the shared, normalized cache contract. */
|
|
32
|
+
export function parseModuleSource(filePath, source, options = {}) {
|
|
33
|
+
return moduleParseCache.getOrParse(filePath, source, normalizeModuleParseOptions(filePath, options));
|
|
34
|
+
}
|
|
19
35
|
/**
|
|
20
36
|
* LRU-bounded module parse cache.
|
|
21
37
|
*
|
|
@@ -39,8 +55,9 @@ export class ModuleParseCache {
|
|
|
39
55
|
* @returns the {@link ParseResult} from `oxc-parser.parseSync`.
|
|
40
56
|
*/
|
|
41
57
|
getOrParse(filePath, source, options = {}) {
|
|
58
|
+
const normalizedOptions = normalizeModuleParseOptions(filePath, options);
|
|
42
59
|
const hash = rapidhash(source);
|
|
43
|
-
const key = makeKey(filePath, hash,
|
|
60
|
+
const key = makeKey(filePath, hash, normalizedOptions);
|
|
44
61
|
const existing = this.entries.get(key);
|
|
45
62
|
if (existing && existing.hash === hash) {
|
|
46
63
|
this.hits += 1;
|
|
@@ -50,7 +67,7 @@ export class ModuleParseCache {
|
|
|
50
67
|
return existing.result;
|
|
51
68
|
}
|
|
52
69
|
this.misses += 1;
|
|
53
|
-
const result = parseSync(filePath, source,
|
|
70
|
+
const result = parseSync(filePath, source, normalizedOptions);
|
|
54
71
|
this.entries.set(key, { hash, result });
|
|
55
72
|
if (this.entries.size > this.maxEntries) {
|
|
56
73
|
// Map iteration order is insertion order; the first key is the
|
|
@@ -95,7 +112,14 @@ export const moduleParseCache = new ModuleParseCache();
|
|
|
95
112
|
* on user/source files during a build.
|
|
96
113
|
*/
|
|
97
114
|
export function cachedParseSync(filePath, source, options = {}) {
|
|
98
|
-
return
|
|
115
|
+
return parseModuleSource(filePath, source, options);
|
|
116
|
+
}
|
|
117
|
+
function normalizeModuleParseOptions(filePath, options) {
|
|
118
|
+
return {
|
|
119
|
+
...options,
|
|
120
|
+
lang: options.lang ?? parserLanguageForFile(filePath),
|
|
121
|
+
sourceType: options.sourceType ?? 'module',
|
|
122
|
+
};
|
|
99
123
|
}
|
|
100
124
|
function makeKey(filePath, sourceHash, options) {
|
|
101
125
|
// Stringify only the option keys we actually use; `parseSync` does
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* View Transition API helpers for `data-view-transition` markup.
|
|
2
|
+
* View Transition API helpers for `data-view-transition` markup and safe root defaults.
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Whether a document tree contains elements that participate in named view transitions.
|
|
7
|
+
*/
|
|
8
|
+
export declare function documentHasNamedViewTransitions(doc: Document): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Whether SPA navigation should use `startViewTransition` (old or incoming page).
|
|
11
|
+
*/
|
|
12
|
+
export declare function navigationHasNamedViewTransitions(current: Document, incoming: Document): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Injects persisted CSS that opts the document out of the root view-transition group.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Uses `data-eco-persist` so head morphing does not drop the stylesheet between navigations.
|
|
18
|
+
* Named `data-view-transition` morph / fade rules live in a separate dynamic style tag.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensureRootViewTransitionStyles(): void;
|
|
5
21
|
/**
|
|
6
22
|
* Assigns `view-transition-name` from `data-view-transition` and injects morph styles.
|
|
7
23
|
*
|
|
@@ -1,11 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* View Transition API helpers for `data-view-transition` markup.
|
|
2
|
+
* View Transition API helpers for `data-view-transition` markup and safe root defaults.
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
5
|
const VIEW_TRANSITION_ATTR = 'data-view-transition';
|
|
6
6
|
const VIEW_TRANSITION_ANIMATE_ATTR = 'data-view-transition-animate';
|
|
7
7
|
const VIEW_TRANSITION_DURATION_ATTR = 'data-view-transition-duration';
|
|
8
8
|
const DYNAMIC_STYLES_ID = 'eco-vt-dynamic-styles';
|
|
9
|
+
const ROOT_STYLES_ID = 'eco-vt-root-styles';
|
|
10
|
+
/**
|
|
11
|
+
* @remarks
|
|
12
|
+
* UA default is `html { view-transition-name: root }`, which snapshots the whole
|
|
13
|
+
* document and crossfades with plus-lighter (flashes lighter on dark UIs). Routers
|
|
14
|
+
* inject this opt-out when `viewTransitions` is enabled. Named `data-view-transition`
|
|
15
|
+
* morphs still run. Full-page fades are not a supported default — use named elements
|
|
16
|
+
* or app-owned motion outside the View Transitions API.
|
|
17
|
+
*/
|
|
18
|
+
const ROOT_STYLES_CSS = `html {
|
|
19
|
+
view-transition-name: none;
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a document tree contains elements that participate in named view transitions.
|
|
24
|
+
*/
|
|
25
|
+
export function documentHasNamedViewTransitions(doc) {
|
|
26
|
+
return doc.querySelector(`[${VIEW_TRANSITION_ATTR}]`) !== null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether SPA navigation should use `startViewTransition` (old or incoming page).
|
|
30
|
+
*/
|
|
31
|
+
export function navigationHasNamedViewTransitions(current, incoming) {
|
|
32
|
+
return documentHasNamedViewTransitions(current) || documentHasNamedViewTransitions(incoming);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Injects persisted CSS that opts the document out of the root view-transition group.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Uses `data-eco-persist` so head morphing does not drop the stylesheet between navigations.
|
|
39
|
+
* Named `data-view-transition` morph / fade rules live in a separate dynamic style tag.
|
|
40
|
+
*/
|
|
41
|
+
export function ensureRootViewTransitionStyles() {
|
|
42
|
+
let styleEl = document.getElementById(ROOT_STYLES_ID);
|
|
43
|
+
if (!styleEl) {
|
|
44
|
+
styleEl = document.createElement('style');
|
|
45
|
+
styleEl.id = ROOT_STYLES_ID;
|
|
46
|
+
styleEl.setAttribute('data-eco-persist', '');
|
|
47
|
+
document.head.append(styleEl);
|
|
48
|
+
}
|
|
49
|
+
if (styleEl.textContent !== ROOT_STYLES_CSS) {
|
|
50
|
+
styleEl.textContent = ROOT_STYLES_CSS;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
9
53
|
/**
|
|
10
54
|
* Assigns `view-transition-name` from `data-view-transition` and injects morph styles.
|
|
11
55
|
*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { applyViewTransitionNames, clearViewTransitionNames, documentHasNamedViewTransitions, ensureRootViewTransitionStyles, navigationHasNamedViewTransitions, } from './view-transitions.js';
|
|
3
|
+
describe('documentHasNamedViewTransitions', () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
document.body.replaceChildren();
|
|
6
|
+
});
|
|
7
|
+
it('returns false when no markup is present', () => {
|
|
8
|
+
expect(documentHasNamedViewTransitions(document)).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it('returns true when data-view-transition is present', () => {
|
|
11
|
+
document.body.innerHTML = '<div data-view-transition="x"></div>';
|
|
12
|
+
expect(documentHasNamedViewTransitions(document)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
it('navigation checks current or incoming document', () => {
|
|
15
|
+
const incoming = new DOMParser().parseFromString('<html><body><div data-view-transition="hero"></div></body></html>', 'text/html');
|
|
16
|
+
expect(navigationHasNamedViewTransitions(document, incoming)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('ensureRootViewTransitionStyles', () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
document.getElementById('eco-vt-root-styles')?.remove();
|
|
22
|
+
document.getElementById('eco-vt-dynamic-styles')?.remove();
|
|
23
|
+
document.body.replaceChildren();
|
|
24
|
+
});
|
|
25
|
+
it('opts the document out of the root view-transition group without !important', () => {
|
|
26
|
+
ensureRootViewTransitionStyles();
|
|
27
|
+
const style = document.getElementById('eco-vt-root-styles');
|
|
28
|
+
expect(style).not.toBeNull();
|
|
29
|
+
expect(style?.hasAttribute('data-eco-persist')).toBe(true);
|
|
30
|
+
expect(style?.textContent).toContain('view-transition-name: none');
|
|
31
|
+
expect(style?.textContent).not.toContain('!important');
|
|
32
|
+
expect(getComputedStyle(document.documentElement).viewTransitionName).toBe('none');
|
|
33
|
+
});
|
|
34
|
+
it('reuses a single style element', () => {
|
|
35
|
+
ensureRootViewTransitionStyles();
|
|
36
|
+
ensureRootViewTransitionStyles();
|
|
37
|
+
expect(document.querySelectorAll('#eco-vt-root-styles')).toHaveLength(1);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('applyViewTransitionNames with root styles', () => {
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
document.getElementById('eco-vt-root-styles')?.remove();
|
|
43
|
+
document.getElementById('eco-vt-dynamic-styles')?.remove();
|
|
44
|
+
document.body.replaceChildren();
|
|
45
|
+
});
|
|
46
|
+
it('keeps root styles when clearing named morph styles', () => {
|
|
47
|
+
ensureRootViewTransitionStyles();
|
|
48
|
+
document.body.innerHTML = '<div data-view-transition="hero"></div>';
|
|
49
|
+
applyViewTransitionNames();
|
|
50
|
+
expect(document.getElementById('eco-vt-dynamic-styles')?.textContent).toContain('view-transition-old(hero)');
|
|
51
|
+
expect(document.getElementById('eco-vt-root-styles')?.textContent).toContain('view-transition-name: none');
|
|
52
|
+
clearViewTransitionNames();
|
|
53
|
+
expect(document.getElementById('eco-vt-dynamic-styles')?.textContent).toBe('');
|
|
54
|
+
expect(document.getElementById('eco-vt-root-styles')?.textContent).toContain('view-transition-name: none');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -198,6 +198,20 @@ export declare class ConfigBuilder {
|
|
|
198
198
|
* @returns The ConfigBuilder instance for method chaining
|
|
199
199
|
*/
|
|
200
200
|
setAdditionalWatchPaths(additionalWatchPaths: string[]): this;
|
|
201
|
+
/**
|
|
202
|
+
* Sets static paths to SSR-prewarm in development.
|
|
203
|
+
*
|
|
204
|
+
* @param devPrewarmPaths - Absolute URL pathnames such as `/` or `/docs`.
|
|
205
|
+
* @returns The ConfigBuilder instance for method chaining.
|
|
206
|
+
*/
|
|
207
|
+
setDevPrewarmPaths(devPrewarmPaths: string[]): this;
|
|
208
|
+
/**
|
|
209
|
+
* Sets static paths to SSR-prewarm before the development server reports ready.
|
|
210
|
+
*
|
|
211
|
+
* @param devPrewarmBeforeReadyPaths - Critical absolute URL pathnames such as `/`.
|
|
212
|
+
* @returns The ConfigBuilder instance for method chaining.
|
|
213
|
+
*/
|
|
214
|
+
setDevPrewarmBeforeReadyPaths(devPrewarmBeforeReadyPaths: string[]): this;
|
|
201
215
|
/**
|
|
202
216
|
* Sets the processors to use for the application.
|
|
203
217
|
* This replaces any existing processors.
|
|
@@ -265,15 +279,8 @@ export declare class ConfigBuilder {
|
|
|
265
279
|
* Initializes default loaders that are required for EcoPages to function.
|
|
266
280
|
*
|
|
267
281
|
* @remarks
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* - `sourceTransforms` is the canonical browser/HMR path. The Rolldown bridge
|
|
271
|
-
* runs these after first-wins `onLoad` plugins rewrite module source.
|
|
272
|
-
* - `loaders` keeps the same transform available to server-oriented builds
|
|
273
|
-
* that still use competing `onLoad` handlers directly.
|
|
274
|
-
*
|
|
275
|
-
* Browser builds exclude the loader copy in {@link getAppBrowserBuildPlugins}
|
|
276
|
-
* when the transform name is already present in `sourceTransforms`.
|
|
282
|
+
* Component identity attribution is a source transform so browser, HMR, and
|
|
283
|
+
* server module paths share one component-identity attribution mechanism.
|
|
277
284
|
*/
|
|
278
285
|
private initializeDefaultLoaders;
|
|
279
286
|
private reviewBaseUrl;
|
|
@@ -7,7 +7,6 @@ import { DEFAULT_ECOPAGES_DIST_DIR, DEFAULT_ECOPAGES_HOSTNAME, DEFAULT_ECOPAGES_
|
|
|
7
7
|
import { collectConfiguredAppBuildManifestContributions, createBuildAdapter, setAppBuildAdapter, updateAppBuildManifest, } from '../build/build-adapter.js';
|
|
8
8
|
import { GHTML_PLUGIN_NAME } from '../integrations/ghtml/ghtml.constants.js';
|
|
9
9
|
import { ghtmlPlugin } from '../integrations/ghtml/ghtml.plugin.js';
|
|
10
|
-
import { createEcoComponentMetaPlugin } from '../plugins/eco-component-meta-plugin.js';
|
|
11
10
|
import { createEcoComponentMetaTransform } from '../plugins/eco-component-meta-plugin.js';
|
|
12
11
|
import { NoopEntrypointDependencyGraph, setAppEntrypointDependencyGraph, } from '../services/runtime-state/entrypoint-dependency-graph.service.js';
|
|
13
12
|
import { CounterServerInvalidationState, setAppServerInvalidationState, } from '../services/runtime-state/server-invalidation-state.service.js';
|
|
@@ -76,6 +75,8 @@ export class ConfigBuilder {
|
|
|
76
75
|
description: 'This is a static site generated with Ecopages',
|
|
77
76
|
},
|
|
78
77
|
additionalWatchPaths: [],
|
|
78
|
+
devPrewarmPaths: [],
|
|
79
|
+
devPrewarmBeforeReadyPaths: [],
|
|
79
80
|
templatesExt: [],
|
|
80
81
|
absolutePaths: {
|
|
81
82
|
config: '',
|
|
@@ -316,6 +317,26 @@ export class ConfigBuilder {
|
|
|
316
317
|
this.config.additionalWatchPaths = additionalWatchPaths;
|
|
317
318
|
return this;
|
|
318
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Sets static paths to SSR-prewarm in development.
|
|
322
|
+
*
|
|
323
|
+
* @param devPrewarmPaths - Absolute URL pathnames such as `/` or `/docs`.
|
|
324
|
+
* @returns The ConfigBuilder instance for method chaining.
|
|
325
|
+
*/
|
|
326
|
+
setDevPrewarmPaths(devPrewarmPaths) {
|
|
327
|
+
this.config.devPrewarmPaths = devPrewarmPaths;
|
|
328
|
+
return this;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Sets static paths to SSR-prewarm before the development server reports ready.
|
|
332
|
+
*
|
|
333
|
+
* @param devPrewarmBeforeReadyPaths - Critical absolute URL pathnames such as `/`.
|
|
334
|
+
* @returns The ConfigBuilder instance for method chaining.
|
|
335
|
+
*/
|
|
336
|
+
setDevPrewarmBeforeReadyPaths(devPrewarmBeforeReadyPaths) {
|
|
337
|
+
this.config.devPrewarmBeforeReadyPaths = devPrewarmBeforeReadyPaths;
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
319
340
|
/**
|
|
320
341
|
* Sets the processors to use for the application.
|
|
321
342
|
* This replaces any existing processors.
|
|
@@ -580,25 +601,14 @@ export class ConfigBuilder {
|
|
|
580
601
|
* Initializes default loaders that are required for EcoPages to function.
|
|
581
602
|
*
|
|
582
603
|
* @remarks
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
* - `sourceTransforms` is the canonical browser/HMR path. The Rolldown bridge
|
|
586
|
-
* runs these after first-wins `onLoad` plugins rewrite module source.
|
|
587
|
-
* - `loaders` keeps the same transform available to server-oriented builds
|
|
588
|
-
* that still use competing `onLoad` handlers directly.
|
|
589
|
-
*
|
|
590
|
-
* Browser builds exclude the loader copy in {@link getAppBrowserBuildPlugins}
|
|
591
|
-
* when the transform name is already present in `sourceTransforms`.
|
|
604
|
+
* Component identity attribution is a source transform so browser, HMR, and
|
|
605
|
+
* server module paths share one component-identity attribution mechanism.
|
|
592
606
|
*/
|
|
593
607
|
async initializeDefaultLoaders() {
|
|
594
608
|
const componentMetaTransform = createEcoComponentMetaTransform({ config: this.config });
|
|
595
609
|
if (!this.config.sourceTransforms.has(componentMetaTransform.name)) {
|
|
596
610
|
this.config.sourceTransforms.set(componentMetaTransform.name, componentMetaTransform);
|
|
597
611
|
}
|
|
598
|
-
const componentMetaPlugin = createEcoComponentMetaPlugin({ config: this.config });
|
|
599
|
-
if (!this.config.loaders.has(componentMetaPlugin.name)) {
|
|
600
|
-
this.config.loaders.set(componentMetaPlugin.name, componentMetaPlugin);
|
|
601
|
-
}
|
|
602
612
|
}
|
|
603
613
|
reviewBaseUrl(baseUrl) {
|
|
604
614
|
if (baseUrl) {
|
|
@@ -22,6 +22,7 @@ export declare class DevTransformVendorRegistry {
|
|
|
22
22
|
tryHandleVendorRequest(pathname: string): Response | null;
|
|
23
23
|
invalidateAll(): void;
|
|
24
24
|
private createVendorResponse;
|
|
25
|
+
private resolveExistingVendorPath;
|
|
25
26
|
private prebundleSpecifier;
|
|
26
27
|
private resolvePackageEntry;
|
|
27
28
|
}
|
|
@@ -31,7 +31,7 @@ export class DevTransformVendorRegistry {
|
|
|
31
31
|
}
|
|
32
32
|
async resolveVendorUrl(specifier) {
|
|
33
33
|
const known = this.resolveKnownVendorUrl(specifier);
|
|
34
|
-
if (known) {
|
|
34
|
+
if (known && this.resolveExistingVendorPath(known)) {
|
|
35
35
|
return known;
|
|
36
36
|
}
|
|
37
37
|
const cached = this.cache.get(specifier);
|
|
@@ -60,7 +60,7 @@ export class DevTransformVendorRegistry {
|
|
|
60
60
|
const filePath = path.join(this.vendorsDir, fileName);
|
|
61
61
|
if (!fileSystem.exists(filePath)) {
|
|
62
62
|
for (const entry of this.cache.values()) {
|
|
63
|
-
if (entry.url === pathname) {
|
|
63
|
+
if (entry.url === pathname && fileSystem.exists(entry.filePath)) {
|
|
64
64
|
return this.createVendorResponse(entry.filePath);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -80,6 +80,19 @@ export class DevTransformVendorRegistry {
|
|
|
80
80
|
},
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
|
+
resolveExistingVendorPath(url) {
|
|
84
|
+
const prefix = `/${RESOLVED_ASSETS_VENDORS_DIR}/`;
|
|
85
|
+
const pathname = new URL(url, 'http://ecopages.local').pathname;
|
|
86
|
+
if (!pathname.startsWith(prefix)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const fileName = pathname.slice(prefix.length);
|
|
90
|
+
if (!fileName || fileName.includes('..')) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const filePath = path.join(this.vendorsDir, fileName);
|
|
94
|
+
return fileSystem.exists(filePath) ? filePath : undefined;
|
|
95
|
+
}
|
|
83
96
|
async prebundleSpecifier(specifier) {
|
|
84
97
|
const resolvedEntry = this.resolvePackageEntry(specifier);
|
|
85
98
|
fileSystem.ensureDir(this.vendorsDir);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request and process-wide counters for the development render pipeline.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Gated behind {@link isRequestPipelineMetricsEnabled} so production paths stay quiet.
|
|
6
|
+
* Benchmarks and structural invariant tests read the snapshot via
|
|
7
|
+
* {@link getRequestPipelineMetricsSnapshot}.
|
|
8
|
+
*/
|
|
9
|
+
export type PageModuleLoadOutcome = 'import-cache-hit' | 'disk-cache-hit' | 'cold-build' | 'host-loader' | 'unified-graph';
|
|
10
|
+
export type RouteRenderPhase = 'resolve-route-inputs' | 'ownership-validation' | 'resolve-dependencies' | 'page-browser-graph' | 'render-body' | 'transform-response';
|
|
11
|
+
export type RouteRenderPhaseRecord = {
|
|
12
|
+
count: number;
|
|
13
|
+
totalDurationMs: number;
|
|
14
|
+
lastDurationMs: number;
|
|
15
|
+
};
|
|
16
|
+
export type RequestPipelineMetricsSnapshot = {
|
|
17
|
+
runtime: 'node' | 'bun';
|
|
18
|
+
pageModuleLoads: number;
|
|
19
|
+
pageModuleBuilds: number;
|
|
20
|
+
pageModuleCacheHits: number;
|
|
21
|
+
hostLoaderLoads: number;
|
|
22
|
+
diskCacheHits: number;
|
|
23
|
+
routeModuleArtifacts: number;
|
|
24
|
+
mdxTransforms: number;
|
|
25
|
+
collectionBuilds: number;
|
|
26
|
+
routeRenderPhases: Partial<Record<RouteRenderPhase, RouteRenderPhaseRecord>>;
|
|
27
|
+
};
|
|
28
|
+
export declare function isRequestPipelineMetricsEnabled(): boolean;
|
|
29
|
+
export declare function getRequestPipelineMetricsHeaderName(): string;
|
|
30
|
+
export declare function resolveRequestPipelineRuntime(): 'node' | 'bun';
|
|
31
|
+
export declare function recordPageModuleLoad(outcome: PageModuleLoadOutcome, artifactCount?: number): void;
|
|
32
|
+
export declare function recordMdxTransform(): void;
|
|
33
|
+
export declare function recordCollectionBuild(): void;
|
|
34
|
+
export declare function recordRouteRenderPhase(phase: RouteRenderPhase, durationMs: number): void;
|
|
35
|
+
export declare function measureRouteRenderPhase<T>(phase: RouteRenderPhase, fn: () => Promise<T>): Promise<T>;
|
|
36
|
+
export declare function getRequestPipelineMetricsSnapshot(): RequestPipelineMetricsSnapshot;
|
|
37
|
+
export declare function serializeRequestPipelineMetricsHeader(): string;
|
|
38
|
+
export declare function resetRequestPipelineMetrics(): void;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request and process-wide counters for the development render pipeline.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Gated behind {@link isRequestPipelineMetricsEnabled} so production paths stay quiet.
|
|
6
|
+
* Benchmarks and structural invariant tests read the snapshot via
|
|
7
|
+
* {@link getRequestPipelineMetricsSnapshot}.
|
|
8
|
+
*/
|
|
9
|
+
const METRICS_HEADER = 'x-ecopages-pipeline-metrics';
|
|
10
|
+
let pageModuleLoads = 0;
|
|
11
|
+
let pageModuleBuilds = 0;
|
|
12
|
+
let pageModuleCacheHits = 0;
|
|
13
|
+
let hostLoaderLoads = 0;
|
|
14
|
+
let diskCacheHits = 0;
|
|
15
|
+
let routeModuleArtifacts = 0;
|
|
16
|
+
let mdxTransforms = 0;
|
|
17
|
+
let collectionBuilds = 0;
|
|
18
|
+
const routeRenderPhases = new Map();
|
|
19
|
+
export function isRequestPipelineMetricsEnabled() {
|
|
20
|
+
return (process.env.ECOPAGES_REQUEST_PIPELINE_METRICS === '1' ||
|
|
21
|
+
process.env.ECOPAGES_ROLLDOWN_BUILD_METRICS === '1' ||
|
|
22
|
+
process.env.ECOPAGES_LOGGER_DEBUG === 'true');
|
|
23
|
+
}
|
|
24
|
+
export function getRequestPipelineMetricsHeaderName() {
|
|
25
|
+
return METRICS_HEADER;
|
|
26
|
+
}
|
|
27
|
+
export function resolveRequestPipelineRuntime() {
|
|
28
|
+
return typeof Bun !== 'undefined' ? 'bun' : 'node';
|
|
29
|
+
}
|
|
30
|
+
export function recordPageModuleLoad(outcome, artifactCount = 0) {
|
|
31
|
+
if (!isRequestPipelineMetricsEnabled()) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
pageModuleLoads += 1;
|
|
35
|
+
switch (outcome) {
|
|
36
|
+
case 'import-cache-hit':
|
|
37
|
+
pageModuleCacheHits += 1;
|
|
38
|
+
break;
|
|
39
|
+
case 'disk-cache-hit':
|
|
40
|
+
diskCacheHits += 1;
|
|
41
|
+
break;
|
|
42
|
+
case 'cold-build':
|
|
43
|
+
pageModuleBuilds += 1;
|
|
44
|
+
routeModuleArtifacts += artifactCount;
|
|
45
|
+
break;
|
|
46
|
+
case 'host-loader':
|
|
47
|
+
hostLoaderLoads += 1;
|
|
48
|
+
break;
|
|
49
|
+
case 'unified-graph':
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function recordMdxTransform() {
|
|
54
|
+
if (!isRequestPipelineMetricsEnabled()) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
mdxTransforms += 1;
|
|
58
|
+
}
|
|
59
|
+
export function recordCollectionBuild() {
|
|
60
|
+
if (!isRequestPipelineMetricsEnabled()) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
collectionBuilds += 1;
|
|
64
|
+
}
|
|
65
|
+
export function recordRouteRenderPhase(phase, durationMs) {
|
|
66
|
+
if (!isRequestPipelineMetricsEnabled()) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const existing = routeRenderPhases.get(phase);
|
|
70
|
+
if (existing) {
|
|
71
|
+
existing.count += 1;
|
|
72
|
+
existing.totalDurationMs += durationMs;
|
|
73
|
+
existing.lastDurationMs = durationMs;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
routeRenderPhases.set(phase, {
|
|
77
|
+
count: 1,
|
|
78
|
+
totalDurationMs: durationMs,
|
|
79
|
+
lastDurationMs: durationMs,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export async function measureRouteRenderPhase(phase, fn) {
|
|
83
|
+
if (!isRequestPipelineMetricsEnabled()) {
|
|
84
|
+
return fn();
|
|
85
|
+
}
|
|
86
|
+
const startedAt = performance.now();
|
|
87
|
+
try {
|
|
88
|
+
return await fn();
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
recordRouteRenderPhase(phase, performance.now() - startedAt);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function getRequestPipelineMetricsSnapshot() {
|
|
95
|
+
return {
|
|
96
|
+
runtime: resolveRequestPipelineRuntime(),
|
|
97
|
+
pageModuleLoads,
|
|
98
|
+
pageModuleBuilds,
|
|
99
|
+
pageModuleCacheHits,
|
|
100
|
+
hostLoaderLoads,
|
|
101
|
+
diskCacheHits,
|
|
102
|
+
routeModuleArtifacts,
|
|
103
|
+
mdxTransforms,
|
|
104
|
+
collectionBuilds,
|
|
105
|
+
routeRenderPhases: Object.fromEntries(routeRenderPhases),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function serializeRequestPipelineMetricsHeader() {
|
|
109
|
+
return JSON.stringify(getRequestPipelineMetricsSnapshot());
|
|
110
|
+
}
|
|
111
|
+
export function resetRequestPipelineMetrics() {
|
|
112
|
+
pageModuleLoads = 0;
|
|
113
|
+
pageModuleBuilds = 0;
|
|
114
|
+
pageModuleCacheHits = 0;
|
|
115
|
+
hostLoaderLoads = 0;
|
|
116
|
+
diskCacheHits = 0;
|
|
117
|
+
routeModuleArtifacts = 0;
|
|
118
|
+
mdxTransforms = 0;
|
|
119
|
+
collectionBuilds = 0;
|
|
120
|
+
routeRenderPhases.clear();
|
|
121
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EcoComponent, EcoComponentConfig } from '../types/public-types.js';
|
|
2
|
+
/** Stable attribution for a Component created by an Integration-owned module. */
|
|
3
|
+
export type ComponentIdentity = {
|
|
4
|
+
id: string;
|
|
5
|
+
file: string;
|
|
6
|
+
integration: string;
|
|
7
|
+
};
|
|
8
|
+
/** Gets canonical component attribution. */
|
|
9
|
+
export declare function getComponentIdentity(value: EcoComponent | EcoComponentConfig | undefined): ComponentIdentity | undefined;
|
|
10
|
+
/** Merges attribution into factory options. */
|
|
11
|
+
export declare function bindComponentIdentity<T extends object>(identity: ComponentIdentity, options: T): T & {
|
|
12
|
+
identity: ComponentIdentity;
|
|
13
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function asConfig(value) {
|
|
2
|
+
if (!value)
|
|
3
|
+
return undefined;
|
|
4
|
+
if (typeof value === 'function')
|
|
5
|
+
return value.config;
|
|
6
|
+
if ('config' in value)
|
|
7
|
+
return value.config;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
/** Gets canonical component attribution. */
|
|
11
|
+
export function getComponentIdentity(value) {
|
|
12
|
+
const config = asConfig(value);
|
|
13
|
+
return config?.identity;
|
|
14
|
+
}
|
|
15
|
+
/** Merges attribution into factory options. */
|
|
16
|
+
export function bindComponentIdentity(identity, options) {
|
|
17
|
+
return { ...options, identity };
|
|
18
|
+
}
|
|
@@ -3,11 +3,12 @@ export { getUndeclaredComponentDependencyMessage } from '../errors/undeclared-co
|
|
|
3
3
|
/**
|
|
4
4
|
* @remarks
|
|
5
5
|
* Declared components are produced by `eco.component()`, `eco.layout()`, and
|
|
6
|
-
* `eco.html()` after the component
|
|
6
|
+
* `eco.html()` after the component identity transform binds identity via
|
|
7
|
+
* `bindComponentIdentity`.
|
|
7
8
|
*/
|
|
8
9
|
export declare function isEcoDeclaredComponent(component: unknown): component is EcoDeclaredComponent;
|
|
9
10
|
/**
|
|
10
|
-
* @throws {UndeclaredComponentDependencyError} When the value lacks
|
|
11
|
+
* @throws {UndeclaredComponentDependencyError} When the value lacks component identity.
|
|
11
12
|
*/
|
|
12
13
|
export declare function assertEcoDeclaredComponent(component: unknown, context?: {
|
|
13
14
|
parentComponentFile?: string;
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { UndeclaredComponentDependencyError } from '../errors/undeclared-component-dependency-error.js';
|
|
2
|
+
import { getComponentIdentity } from './component-identity.js';
|
|
2
3
|
export { getUndeclaredComponentDependencyMessage } from '../errors/undeclared-component-dependency-error.js';
|
|
3
4
|
/**
|
|
4
5
|
* @remarks
|
|
5
6
|
* Declared components are produced by `eco.component()`, `eco.layout()`, and
|
|
6
|
-
* `eco.html()` after the component
|
|
7
|
+
* `eco.html()` after the component identity transform binds identity via
|
|
8
|
+
* `bindComponentIdentity`.
|
|
7
9
|
*/
|
|
8
10
|
export function isEcoDeclaredComponent(component) {
|
|
9
11
|
return (typeof component === 'function' &&
|
|
10
|
-
typeof component
|
|
11
|
-
typeof component
|
|
12
|
+
typeof getComponentIdentity(component)?.file === 'string' &&
|
|
13
|
+
typeof getComponentIdentity(component)?.integration === 'string');
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
|
-
* @throws {UndeclaredComponentDependencyError} When the value lacks
|
|
16
|
+
* @throws {UndeclaredComponentDependencyError} When the value lacks component identity.
|
|
15
17
|
*/
|
|
16
18
|
export function assertEcoDeclaredComponent(component, context = {}) {
|
|
17
19
|
if (isEcoDeclaredComponent(component)) {
|
package/src/eco/eco.browser.js
CHANGED
|
@@ -2,7 +2,7 @@ import { applyPageLayoutConfig, mergeLayoutDependencies, normalizePageLayouts }
|
|
|
2
2
|
function createComponentFactory(options) {
|
|
3
3
|
const component = ((props) => options.render(props));
|
|
4
4
|
component.config = {
|
|
5
|
-
|
|
5
|
+
identity: options.identity,
|
|
6
6
|
integration: options.integration,
|
|
7
7
|
dependencies: options.dependencies,
|
|
8
8
|
};
|
|
@@ -29,7 +29,7 @@ function page(options) {
|
|
|
29
29
|
? mergeLayoutDependencies(undefined, layoutEntries)
|
|
30
30
|
: mergeLayoutDependencies(dependenciesInput, layoutEntries);
|
|
31
31
|
const pageComponent = createComponentFactory({
|
|
32
|
-
|
|
32
|
+
identity: options.identity,
|
|
33
33
|
integration: options.integration,
|
|
34
34
|
dependencies: staticDependencies,
|
|
35
35
|
render,
|