@ecopages/core 0.2.0-rc.6 → 0.2.0-rc.7
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 +2 -0
- package/package.json +3 -3
- package/src/declarations.d.ts +10 -1
- package/src/islands/README.md +5 -1
- package/src/route-renderer/README.md +2 -2
- package/src/route-renderer/orchestration/document-shell/document-shell-render.service.d.ts +7 -2
- package/src/route-renderer/orchestration/document-shell/document-shell-render.service.js +2 -0
- package/src/route-renderer/orchestration/foreign-child/foreign-subtree-execution.service.d.ts +3 -2
- package/src/route-renderer/orchestration/foreign-child/foreign-subtree-execution.service.js +1 -1
- package/src/route-renderer/orchestration/integration-renderer.d.ts +4 -3
- package/src/route-renderer/orchestration/integration-renderer.js +3 -3
- package/src/route-renderer/orchestration/ownership-graph/component-graph-collectors.d.ts +1 -1
- package/src/route-renderer/orchestration/ownership-graph/component-graph-collectors.js +2 -2
- package/src/route-renderer/orchestration/route-pipeline/route-render-orchestrator.js +3 -2
- package/src/route-renderer/orchestration/string-markup-renderer.d.ts +1 -1
- package/src/route-renderer/orchestration/string-markup-renderer.js +2 -1
- package/src/router/README.md +5 -1
- package/src/router/server/route-registry.d.ts +13 -0
- package/src/router/server/route-registry.js +46 -32
- package/src/services/README.md +2 -0
- package/src/services/assets/asset-processing-service/browser-runtime-entry-resolution.d.ts +6 -0
- package/src/services/assets/asset-processing-service/browser-runtime-entry-resolution.js +24 -5
- package/src/types/internal-types.d.ts +5 -1
package/README.md
CHANGED
|
@@ -77,6 +77,8 @@ The manager/orchestration layer is core-owned, but framework-specific strategies
|
|
|
77
77
|
- `ConfigBuilder` seeds one app-owned build ownership path, adapter, manifest, executor, dev graph, and runtime registry.
|
|
78
78
|
- `BrowserBundleService` is the shared browser build seam used by HMR and asset-oriented browser output paths.
|
|
79
79
|
- `ServerModuleTranspiler` is the shared server-side source loading seam used by runtime bootstrap and HMR metadata loading.
|
|
80
|
+
- `RouteRegistry` gives exact and dynamic Pages priority over catch-alls, then selects the most specific matching catch-all prefix.
|
|
81
|
+
- Browser runtime package entries resolve from the application root with ESM import conditions, so framework-local packages cannot shadow app dependencies.
|
|
80
82
|
- `createApp()` stays the universal runtime entrypoint, while Vite and Nitro hosts own their advanced dev and build workflows.
|
|
81
83
|
- One bundled adapter is the default bundler. Vite-based apps route through the `ViteHostBuildAdapter` boundary marker instead.
|
|
82
84
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/core",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.7",
|
|
4
4
|
"description": "Core package for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"directory": "packages/core"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@ecopages/file-system": "0.2.0-rc.
|
|
23
|
+
"@ecopages/file-system": "0.2.0-rc.7",
|
|
24
24
|
"@ecopages/logger": "^0.2.3",
|
|
25
25
|
"@ecopages/scripts-injector": "^0.1.5",
|
|
26
26
|
"@oxc-project/runtime": "0.141.0",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@standard-schema/utils": "^0.3.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@ecopages/dev-toolbar": "0.2.0-rc.
|
|
37
|
+
"@ecopages/dev-toolbar": "0.2.0-rc.7"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@ecopages/dev-toolbar": {
|
package/src/declarations.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import './css-imports.d.js';
|
|
2
2
|
|
|
3
3
|
import type { EcoPagesAppConfig } from './types/internal-types';
|
|
4
4
|
import type { EcoNavigationRuntime } from './router/client/navigation-coordinator';
|
|
@@ -6,6 +6,12 @@ import type { EcoNavigationRuntime } from './router/client/navigation-coordinato
|
|
|
6
6
|
type HMRHandler = (url: string) => Promise<void>;
|
|
7
7
|
type CleanupPageRootFunction = () => void;
|
|
8
8
|
type EcoPageRoot = { render: (node: unknown) => void; unmount: () => void };
|
|
9
|
+
type EcoIslandComponent = (props: Record<string, unknown>) => unknown;
|
|
10
|
+
/** Shared React island diagnostics and root lookup state exposed to devtools. */
|
|
11
|
+
type EcoIslandRuntime = {
|
|
12
|
+
islandRoots: Record<string, EcoPageRoot>;
|
|
13
|
+
islandComponents: Record<string, EcoIslandComponent>;
|
|
14
|
+
};
|
|
9
15
|
type EcoPageData = {
|
|
10
16
|
module: string;
|
|
11
17
|
props: Record<string, unknown>;
|
|
@@ -17,7 +23,10 @@ type EcoPagesWindowRuntime = {
|
|
|
17
23
|
cleanupPageRoot?: CleanupPageRootFunction;
|
|
18
24
|
pageRoot?: EcoPageRoot | null;
|
|
19
25
|
};
|
|
26
|
+
/** Roots keyed by SSR instance ID; component keys are never used for lookup. */
|
|
20
27
|
islandRoots?: Record<string, EcoPageRoot>;
|
|
28
|
+
islandComponents?: Record<string, EcoIslandComponent>;
|
|
29
|
+
__ecoIslandRuntime?: EcoIslandRuntime;
|
|
21
30
|
page?: EcoPageData;
|
|
22
31
|
};
|
|
23
32
|
|
package/src/islands/README.md
CHANGED
|
@@ -12,7 +12,11 @@ Hydratable component instances stamp a small, integration-agnostic attribute set
|
|
|
12
12
|
| `data-eco-component-key` | Optional module key used by client hydration (React) |
|
|
13
13
|
| `data-eco-props` | Optional base64 JSON props snapshot for hydration |
|
|
14
14
|
|
|
15
|
-
React may
|
|
15
|
+
React integrations may emit `<eco-island>` as the SSR host. The host uses
|
|
16
|
+
`display: contents` so it does not introduce a layout box, while the host and
|
|
17
|
+
its children remain in place as the client calls `hydrateRoot()` on that host.
|
|
18
|
+
React island integrations may add `data-eco-hydrated` after the initial client
|
|
19
|
+
commit for development diagnostics.
|
|
16
20
|
|
|
17
21
|
## Where stamping happens
|
|
18
22
|
|
|
@@ -95,12 +95,12 @@ Important:
|
|
|
95
95
|
|
|
96
96
|
- route-level fallback resolution is gone; unresolved artifacts are now a hard failure
|
|
97
97
|
- ownership is declared from canonical component identity, not inferred from final HTML
|
|
98
|
-
- declared page dependencies are resolved from final render inputs and carried to the owning integration;
|
|
98
|
+
- declared page dependencies are resolved from final render inputs and carried as dependency roots to the owning integration; those roots participate in asset, integration, ownership, and renderer-specific lifecycle collection
|
|
99
99
|
- same-integration children stay renderer-local and do not need to pass through a universal transport
|
|
100
100
|
|
|
101
101
|
## Declared Foreign Child Contract
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
Static mixed-integration component configs must declare every possible foreign child in `config.dependencies.components`. Page dependency resolvers can additionally provide request-time component roots when a route loads a component dynamically.
|
|
104
104
|
|
|
105
105
|
`OwnershipValidationService` surfaces missing metadata or unknown integrations during `prepareRenderOptions()` and throws before render execution starts.
|
|
106
106
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BaseIntegrationContext,
|
|
1
|
+
import type { BaseIntegrationContext, ComponentRenderResult, EcoComponent, PageMetadataProps } from '../../../types/public-types.js';
|
|
2
|
+
import type { InternalComponentRenderInput } from '../../../types/internal-types.js';
|
|
2
3
|
import type { ProcessedAsset } from '../../../services/assets/asset-processing-service/index.js';
|
|
3
4
|
import { HtmlTransformerService, type HtmlDocumentContribution } from '../../../services/html/html-transformer.service.js';
|
|
4
5
|
export type DocumentShellLayoutInput = {
|
|
@@ -36,6 +37,8 @@ export type DocumentShellComposeChildrenHook = (context: DocumentShellComposeChi
|
|
|
36
37
|
export type DocumentShellComposeInput = {
|
|
37
38
|
primaryComponent: EcoComponent;
|
|
38
39
|
primaryProps: Record<string, unknown>;
|
|
40
|
+
/** Components whose ownership graphs must join the foreign-child decision. */
|
|
41
|
+
foreignChildRoots?: ReadonlyArray<EcoComponent | Partial<EcoComponent>>;
|
|
39
42
|
layout?: DocumentShellLayoutInput;
|
|
40
43
|
layouts?: DocumentShellLayoutInput[];
|
|
41
44
|
composeChildren?: DocumentShellComposeChildrenHook;
|
|
@@ -47,6 +50,8 @@ export type DocumentShellPageRenderInput = {
|
|
|
47
50
|
component: EcoComponent;
|
|
48
51
|
props: Record<string, unknown>;
|
|
49
52
|
};
|
|
53
|
+
/** Components whose ownership graphs must join the foreign-child decision. */
|
|
54
|
+
foreignChildRoots?: ReadonlyArray<EcoComponent | Partial<EcoComponent>>;
|
|
50
55
|
layout?: DocumentShellLayoutInput;
|
|
51
56
|
layouts?: DocumentShellLayoutInput[];
|
|
52
57
|
composeChildren?: DocumentShellComposeChildrenHook;
|
|
@@ -57,7 +62,7 @@ export type DocumentShellPageRenderInput = {
|
|
|
57
62
|
transformDocumentHtml?: (html: string) => string;
|
|
58
63
|
};
|
|
59
64
|
export type DocumentShellRenderDependencies = {
|
|
60
|
-
renderComponentWithForeignChildren(input:
|
|
65
|
+
renderComponentWithForeignChildren(input: InternalComponentRenderInput): Promise<ComponentRenderResult>;
|
|
61
66
|
appendProcessedDependencies(...assetGroups: Array<readonly ProcessedAsset[] | undefined>): ProcessedAsset[];
|
|
62
67
|
};
|
|
63
68
|
export type DocumentShellAttributeStamping = {
|
|
@@ -68,6 +68,7 @@ export async function composeDocumentShell(dependencies, input) {
|
|
|
68
68
|
primaryRender = await dependencies.renderComponentWithForeignChildren({
|
|
69
69
|
component: input.primaryComponent,
|
|
70
70
|
props: input.primaryProps,
|
|
71
|
+
foreignChildRoots: input.foreignChildRoots,
|
|
71
72
|
integrationContext: { rendererCache },
|
|
72
73
|
});
|
|
73
74
|
const composed = await composeChildren({
|
|
@@ -113,6 +114,7 @@ export async function renderPageDocumentShell(dependencies, input, docType) {
|
|
|
113
114
|
const { documentHtml: composedDocumentHtml } = await composeDocumentShell(dependencies, {
|
|
114
115
|
primaryComponent: input.page.component,
|
|
115
116
|
primaryProps: input.page.props,
|
|
117
|
+
foreignChildRoots: input.foreignChildRoots,
|
|
116
118
|
layout: input.layout,
|
|
117
119
|
layouts: input.layouts,
|
|
118
120
|
composeChildren: input.composeChildren,
|
package/src/route-renderer/orchestration/foreign-child/foreign-subtree-execution.service.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ProcessedAsset } from '../../../services/assets/asset-processing-service/index.js';
|
|
2
2
|
import type { BaseIntegrationContext, ComponentRenderInput, ComponentRenderResult, EcoComponent, ForeignSubtreeRenderPayload } from '../../../types/public-types.js';
|
|
3
|
+
import type { InternalComponentRenderInput } from '../../../types/internal-types.js';
|
|
3
4
|
import { type ForeignChildRuntime } from './component-render-context.js';
|
|
4
5
|
export type QueuedForeignChildDecisionInput = {
|
|
5
6
|
currentIntegration: string;
|
|
@@ -38,10 +39,10 @@ export interface ForeignSubtreeExecutionDecisionInput {
|
|
|
38
39
|
}
|
|
39
40
|
export interface ForeignSubtreeExecutionRenderOptions {
|
|
40
41
|
currentIntegrationName: string;
|
|
41
|
-
input:
|
|
42
|
+
input: InternalComponentRenderInput;
|
|
42
43
|
renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
|
|
43
44
|
normalizeComponentRenderOutput(result: ComponentRenderResult): ComponentRenderResult;
|
|
44
|
-
hasForeignChildDescendants(component: EcoComponent): boolean;
|
|
45
|
+
hasForeignChildDescendants(component: EcoComponent, foreignChildRoots?: ReadonlyArray<EcoComponent | Partial<EcoComponent>>): boolean;
|
|
45
46
|
createForeignChildRuntime(options: {
|
|
46
47
|
renderInput: ComponentRenderInput;
|
|
47
48
|
rendererCache: Map<string, ForeignSubtreeExecutionOwningRenderer>;
|
|
@@ -241,7 +241,7 @@ export class ForeignSubtreeExecutionService {
|
|
|
241
241
|
if (delegatedForeignChildRender) {
|
|
242
242
|
return delegatedForeignChildRender;
|
|
243
243
|
}
|
|
244
|
-
const hasForeignChildren = options.hasForeignChildDescendants(options.input.component) ||
|
|
244
|
+
const hasForeignChildren = options.hasForeignChildDescendants(options.input.component, options.input.foreignChildRoots) ||
|
|
245
245
|
this.requiresForeignChildRuntime(options.input);
|
|
246
246
|
const activeRenderContext = getComponentRenderContext();
|
|
247
247
|
if (!hasForeignChildren) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Every integration renderer should extend this class
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
-
import type { EcoPagesAppConfig, IHmrManager } from '../../types/internal-types.js';
|
|
6
|
+
import type { EcoPagesAppConfig, IHmrManager, InternalComponentRenderInput } from '../../types/internal-types.js';
|
|
7
7
|
import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPageFile, EcoPagesElement, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphContributionContext, PageBrowserGraphResult, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
|
|
8
8
|
import { type AssetProcessingService, type ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
|
|
9
9
|
import { HtmlTransformerService } from '../../services/html/html-transformer.service.js';
|
|
@@ -217,6 +217,7 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
|
|
|
217
217
|
pageProps: Record<string, unknown>;
|
|
218
218
|
documentProps?: Record<string, unknown>;
|
|
219
219
|
transformDocumentHtml?: (html: string) => string;
|
|
220
|
+
foreignChildRoots?: ReadonlyArray<EcoComponent | Partial<EcoComponent>>;
|
|
220
221
|
}): Promise<string>;
|
|
221
222
|
protected renderStringComponentWithSerializedChildren(input: ComponentRenderInput, component: (props: Record<string, unknown>) => Promise<EcoPagesElement> | EcoPagesElement): Promise<ComponentRenderResult>;
|
|
222
223
|
/**
|
|
@@ -349,7 +350,7 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
|
|
|
349
350
|
* render them with no active foreign-child runtime, which bypasses the owning
|
|
350
351
|
* renderer's nested foreign-child handoff.
|
|
351
352
|
*/
|
|
352
|
-
renderComponentWithForeignChildren(input:
|
|
353
|
+
renderComponentWithForeignChildren(input: InternalComponentRenderInput): Promise<ComponentRenderResult>;
|
|
353
354
|
protected finalizeIslandComponentRender(input: ComponentRenderInput, result: ComponentRenderResult): ComponentRenderResult;
|
|
354
355
|
private normalizeComponentRenderOutput;
|
|
355
356
|
protected normalizeUnresolvedMarkerArtifactHtml(html: string): string;
|
|
@@ -360,7 +361,7 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
|
|
|
360
361
|
* This keeps foreign-child runtime setup narrow: same-integration trees can render
|
|
361
362
|
* directly without paying the queue orchestration cost.
|
|
362
363
|
*/
|
|
363
|
-
protected hasForeignChildDescendants(component: EcoComponent): boolean;
|
|
364
|
+
protected hasForeignChildDescendants(component: EcoComponent, foreignChildRoots?: ReadonlyArray<EcoComponent | Partial<EcoComponent>>): boolean;
|
|
364
365
|
/**
|
|
365
366
|
* Render a view directly to a Response object.
|
|
366
367
|
* Used for explicit routing where views are rendered from route handlers.
|
|
@@ -688,7 +688,7 @@ export class IntegrationRenderer {
|
|
|
688
688
|
input,
|
|
689
689
|
renderComponent: (renderInput) => this.renderComponent(renderInput),
|
|
690
690
|
normalizeComponentRenderOutput: (result) => this.normalizeComponentRenderOutput(result),
|
|
691
|
-
hasForeignChildDescendants: (component) => this.hasForeignChildDescendants(component),
|
|
691
|
+
hasForeignChildDescendants: (component, foreignChildRoots) => this.hasForeignChildDescendants(component, foreignChildRoots),
|
|
692
692
|
createForeignChildRuntime: ({ renderInput, rendererCache }) => this.createForeignChildRuntime({
|
|
693
693
|
renderInput,
|
|
694
694
|
rendererCache: rendererCache,
|
|
@@ -725,8 +725,8 @@ export class IntegrationRenderer {
|
|
|
725
725
|
* This keeps foreign-child runtime setup narrow: same-integration trees can render
|
|
726
726
|
* directly without paying the queue orchestration cost.
|
|
727
727
|
*/
|
|
728
|
-
hasForeignChildDescendants(component) {
|
|
729
|
-
return hasForeignChildDescendantsInGraph(component, this.name);
|
|
728
|
+
hasForeignChildDescendants(component, foreignChildRoots) {
|
|
729
|
+
return hasForeignChildDescendantsInGraph(component, this.name, foreignChildRoots);
|
|
730
730
|
}
|
|
731
731
|
/**
|
|
732
732
|
* Render a single component and return structured output for orchestration paths.
|
|
@@ -3,5 +3,5 @@ import type { EcoComponent, ResolvedLazyTrigger } from '../../../types/public-ty
|
|
|
3
3
|
import type { ProcessedAsset } from '../../../services/assets/asset-processing-service/index.js';
|
|
4
4
|
export declare function collectIntegrationNamesFromGraph(components: (EcoComponent | Partial<EcoComponent>)[], currentIntegrationName: string): Set<string>;
|
|
5
5
|
export declare function collectResolvedLazyTriggersFromGraph(components: (EcoComponent | Partial<EcoComponent>)[], currentIntegrationName: string): ResolvedLazyTrigger[];
|
|
6
|
-
export declare function hasForeignChildDescendantsInGraph(component: EcoComponent, currentIntegrationName: string): boolean;
|
|
6
|
+
export declare function hasForeignChildDescendantsInGraph(component: EcoComponent, currentIntegrationName: string, foreignChildRoots?: ReadonlyArray<EcoComponent | Partial<EcoComponent>>): boolean;
|
|
7
7
|
export declare function collectUsedIntegrationDependenciesFromGraph(appConfig: EcoPagesAppConfig, components: (EcoComponent | Partial<EcoComponent>)[], currentIntegrationName: string): ProcessedAsset[];
|
|
@@ -31,10 +31,10 @@ export function collectResolvedLazyTriggersFromGraph(components, currentIntegrat
|
|
|
31
31
|
});
|
|
32
32
|
return triggers;
|
|
33
33
|
}
|
|
34
|
-
export function hasForeignChildDescendantsInGraph(component, currentIntegrationName) {
|
|
34
|
+
export function hasForeignChildDescendantsInGraph(component, currentIntegrationName, foreignChildRoots) {
|
|
35
35
|
let foundForeign = false;
|
|
36
36
|
walkComponentGraph({
|
|
37
|
-
roots: [
|
|
37
|
+
roots: toGraphRoots([component, ...(foreignChildRoots ?? [])]),
|
|
38
38
|
currentIntegrationName,
|
|
39
39
|
onComponent: ({ component: currentComponent }) => {
|
|
40
40
|
if (foundForeign) {
|
|
@@ -88,11 +88,12 @@ export class RouteRenderOrchestrator {
|
|
|
88
88
|
dependencyInstanceKey,
|
|
89
89
|
});
|
|
90
90
|
const resolvedPageDependencyComponents = resolvedPageDependencies?.components ?? [];
|
|
91
|
+
const dependencyRoots = [...componentsToResolve, ...resolvedPageDependencyComponents];
|
|
91
92
|
const allDependencies = [
|
|
92
93
|
...resolvedDependencies,
|
|
93
|
-
...collectUsedIntegrationDependenciesFromGraph(this.appConfig,
|
|
94
|
+
...collectUsedIntegrationDependenciesFromGraph(this.appConfig, dependencyRoots, adapter.name),
|
|
94
95
|
];
|
|
95
|
-
const triggers = collectResolvedLazyTriggersFromGraph(
|
|
96
|
+
const triggers = collectResolvedLazyTriggersFromGraph(dependencyRoots, adapter.name);
|
|
96
97
|
const globalAssets = triggers.length > 0
|
|
97
98
|
? await buildGlobalInjectorAssets(this.appConfig, this.assetProcessingService, triggers, adapter.name)
|
|
98
99
|
: [];
|
|
@@ -9,6 +9,6 @@ import { IntegrationRenderer, type RenderToResponseContext } from './integration
|
|
|
9
9
|
*/
|
|
10
10
|
export declare abstract class StringMarkupRenderer extends IntegrationRenderer<EcoPagesElement> {
|
|
11
11
|
renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
|
|
12
|
-
render({ params, query, props, locals, pageLocals, metadata, Page, Layout, layoutEntries, HtmlTemplate, pageProps, }: IntegrationRendererRenderOptions): Promise<RouteRendererBody>;
|
|
12
|
+
render({ params, query, props, locals, pageLocals, metadata, Page, Layout, layoutEntries, HtmlTemplate, pageProps, resolvedPageDependencyComponents, }: IntegrationRendererRenderOptions): Promise<RouteRendererBody>;
|
|
13
13
|
renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
|
|
14
14
|
}
|
|
@@ -14,7 +14,7 @@ export class StringMarkupRenderer extends IntegrationRenderer {
|
|
|
14
14
|
}
|
|
15
15
|
return this.renderStringComponentWithQueuedForeignSubtrees(input, input.component);
|
|
16
16
|
}
|
|
17
|
-
async render({ params, query, props, locals, pageLocals, metadata, Page, Layout, layoutEntries, HtmlTemplate, pageProps, }) {
|
|
17
|
+
async render({ params, query, props, locals, pageLocals, metadata, Page, Layout, layoutEntries, HtmlTemplate, pageProps, resolvedPageDependencyComponents, }) {
|
|
18
18
|
try {
|
|
19
19
|
return await this.renderPageWithDocumentShell({
|
|
20
20
|
page: {
|
|
@@ -31,6 +31,7 @@ export class StringMarkupRenderer extends IntegrationRenderer {
|
|
|
31
31
|
htmlTemplate: HtmlTemplate,
|
|
32
32
|
metadata,
|
|
33
33
|
pageProps: pageProps ?? props ?? {},
|
|
34
|
+
foreignChildRoots: resolvedPageDependencyComponents,
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
37
|
catch (error) {
|
package/src/router/README.md
CHANGED
|
@@ -56,7 +56,11 @@ Match priority:
|
|
|
56
56
|
|
|
57
57
|
1. `exact` — the pathname must equal the route pathname exactly.
|
|
58
58
|
2. `dynamic` — the clean (bracket-stripped) prefix must appear in the pathname, and the segment counts must match.
|
|
59
|
-
3. `catch-all` — the
|
|
59
|
+
3. `catch-all` — the prefix segments must match the pathname. When multiple
|
|
60
|
+
catch-all routes match, shared prefix segments are compared from left to right
|
|
61
|
+
so an earlier static segment wins over a dynamic one; when those patterns tie,
|
|
62
|
+
the route with the longer prefix wins, then pathname order provides a
|
|
63
|
+
deterministic tie-breaker.
|
|
60
64
|
|
|
61
65
|
## `client/`
|
|
62
66
|
|
|
@@ -62,6 +62,19 @@ export declare class RouteRegistry {
|
|
|
62
62
|
reload(): Promise<void>;
|
|
63
63
|
onReload(listener: () => void): () => void;
|
|
64
64
|
matchRequest(requestUrl: string): RouteMatch | null;
|
|
65
|
+
/**
|
|
66
|
+
* Orders overlapping catch-all routes by the specificity of their prefix.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* Prefix segments are compared from left to right because an earlier static
|
|
70
|
+
* segment constrains a request more strongly than a later one. A longer
|
|
71
|
+
* prefix wins only after the shared segment pattern ties. The pathname tie
|
|
72
|
+
* breaker keeps equal patterns deterministic without depending on discovery
|
|
73
|
+
* order.
|
|
74
|
+
*/
|
|
75
|
+
private compareCatchAllSpecificity;
|
|
76
|
+
private getCatchAllPrefix;
|
|
77
|
+
private isDynamicRouteSegment;
|
|
65
78
|
listStaticPathExpansions(input: {
|
|
66
79
|
runtimeOrigin: string;
|
|
67
80
|
}): Promise<readonly StaticPathExpansion[]>;
|
|
@@ -49,49 +49,60 @@ export class RouteRegistry {
|
|
|
49
49
|
const requestedPathname = normalizePathname(url.pathname);
|
|
50
50
|
const query = this.getSearchParams(url);
|
|
51
51
|
for (const route of this.templateRouteList) {
|
|
52
|
-
if (route.kind
|
|
52
|
+
if (route.kind === 'exact') {
|
|
53
|
+
if (requestedPathname === route.pathname || requestedPathname === `${route.pathname}/`) {
|
|
54
|
+
return {
|
|
55
|
+
requestedPathname,
|
|
56
|
+
templateRoute: route,
|
|
57
|
+
params: {},
|
|
58
|
+
query,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
53
61
|
continue;
|
|
54
62
|
}
|
|
55
|
-
|
|
63
|
+
const params = this.tryExtractParams(route, requestedPathname);
|
|
64
|
+
if (params) {
|
|
56
65
|
return {
|
|
57
66
|
requestedPathname,
|
|
58
67
|
templateRoute: route,
|
|
59
|
-
params
|
|
68
|
+
params,
|
|
60
69
|
query,
|
|
61
70
|
};
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
continue;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Orders overlapping catch-all routes by the specificity of their prefix.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* Prefix segments are compared from left to right because an earlier static
|
|
80
|
+
* segment constrains a request more strongly than a later one. A longer
|
|
81
|
+
* prefix wins only after the shared segment pattern ties. The pathname tie
|
|
82
|
+
* breaker keeps equal patterns deterministic without depending on discovery
|
|
83
|
+
* order.
|
|
84
|
+
*/
|
|
85
|
+
compareCatchAllSpecificity(left, right) {
|
|
86
|
+
const leftPrefix = this.getCatchAllPrefix(left.pathname);
|
|
87
|
+
const rightPrefix = this.getCatchAllPrefix(right.pathname);
|
|
88
|
+
const sharedLength = Math.min(leftPrefix.length, rightPrefix.length);
|
|
89
|
+
for (let index = 0; index < sharedLength; index += 1) {
|
|
90
|
+
const leftIsDynamic = this.isDynamicRouteSegment(leftPrefix[index]);
|
|
91
|
+
const rightIsDynamic = this.isDynamicRouteSegment(rightPrefix[index]);
|
|
92
|
+
if (leftIsDynamic !== rightIsDynamic) {
|
|
93
|
+
return leftIsDynamic ? 1 : -1;
|
|
86
94
|
}
|
|
87
|
-
return {
|
|
88
|
-
requestedPathname,
|
|
89
|
-
templateRoute: route,
|
|
90
|
-
params,
|
|
91
|
-
query,
|
|
92
|
-
};
|
|
93
95
|
}
|
|
94
|
-
|
|
96
|
+
const prefixLengthDifference = rightPrefix.length - leftPrefix.length;
|
|
97
|
+
return prefixLengthDifference !== 0 ? prefixLengthDifference : left.pathname.localeCompare(right.pathname);
|
|
98
|
+
}
|
|
99
|
+
getCatchAllPrefix(pathname) {
|
|
100
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
101
|
+
const catchAllIndex = segments.findIndex((segment) => segment.startsWith('[...') && segment.endsWith(']'));
|
|
102
|
+
return catchAllIndex === -1 ? segments : segments.slice(0, catchAllIndex);
|
|
103
|
+
}
|
|
104
|
+
isDynamicRouteSegment(segment) {
|
|
105
|
+
return segment.startsWith('[') && segment.endsWith(']');
|
|
95
106
|
}
|
|
96
107
|
async listStaticPathExpansions(input) {
|
|
97
108
|
const expansions = [];
|
|
@@ -168,6 +179,9 @@ export class RouteRegistry {
|
|
|
168
179
|
if (priorityDifference !== 0) {
|
|
169
180
|
return priorityDifference;
|
|
170
181
|
}
|
|
182
|
+
if (left.kind === 'catch-all' && right.kind === 'catch-all') {
|
|
183
|
+
return this.compareCatchAllSpecificity(left, right);
|
|
184
|
+
}
|
|
171
185
|
if (left.pathname === '/') {
|
|
172
186
|
return -1;
|
|
173
187
|
}
|
package/src/services/README.md
CHANGED
|
@@ -30,6 +30,8 @@ so shared layout styles do not need to be rebuilt for every navigated page while
|
|
|
30
30
|
|
|
31
31
|
Browser runtime module assets resolve bare package roots through their ESM import target. For legacy packages without an `exports` map, they prefer `package.json#module` over CJS `main`; CJS resolution is only a compatibility fallback. Generated entries use `export *` for ESM files and explicit named re-exports for CJS files (so bindings such as React `jsx` exist on the vendor). A default binding is added only when the selected entry exposes one. Runtime vendors are package-root contracts: subpath imports need their own vendor declaration or remain in the consuming bundle.
|
|
32
32
|
|
|
33
|
+
Package entry lookup is rooted at the application directory, so a framework-local dependency with the same name cannot shadow the application's installed package. ESM export conditions are evaluated in the same order as the generated vendor import (`node`, `import`, then `default`).
|
|
34
|
+
|
|
33
35
|
## Design Rule
|
|
34
36
|
|
|
35
37
|
If a concern affects more than one integration or more than one runtime adapter, it usually belongs here instead of in a package-specific implementation.
|
|
@@ -3,6 +3,12 @@ export type BrowserRuntimeDefaultExportPolicy = 'emit-default' | 'skip-default';
|
|
|
3
3
|
type RequireFromRoot = ReturnType<typeof createRequire>;
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a package specifier through Node's ESM import conditions from the app root.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The resolver is rooted at the application directory so package lookup cannot
|
|
9
|
+
* accidentally use a dependency with the same name from the framework's own
|
|
10
|
+
* installation. The condition order matches the generated vendor import: Node
|
|
11
|
+
* compatibility first, then ESM import, then the package default.
|
|
6
12
|
*/
|
|
7
13
|
export declare function resolvePackageEsmEntryPath(specifier: string, rootDir: string): string | undefined;
|
|
8
14
|
/**
|
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ResolverFactory } from 'oxc-resolver';
|
|
4
5
|
import { isBarePackageImportSpecifier } from '../../../plugins/tsconfig-import-resolver.js';
|
|
5
6
|
import { toPackageRootSpecifier } from '../../../plugins/package-specifier.js';
|
|
7
|
+
const esmEntryResolvers = new Map();
|
|
8
|
+
function getEsmEntryResolver(rootDir) {
|
|
9
|
+
const normalizedRoot = path.resolve(rootDir);
|
|
10
|
+
const cached = esmEntryResolvers.get(normalizedRoot);
|
|
11
|
+
if (cached) {
|
|
12
|
+
return cached;
|
|
13
|
+
}
|
|
14
|
+
const resolver = new ResolverFactory({ conditionNames: ['node', 'import', 'default'] });
|
|
15
|
+
esmEntryResolvers.set(normalizedRoot, resolver);
|
|
16
|
+
return resolver;
|
|
17
|
+
}
|
|
6
18
|
/**
|
|
7
19
|
* Resolves a package specifier through Node's ESM import conditions from the app root.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* The resolver is rooted at the application directory so package lookup cannot
|
|
23
|
+
* accidentally use a dependency with the same name from the framework's own
|
|
24
|
+
* installation. The condition order matches the generated vendor import: Node
|
|
25
|
+
* compatibility first, then ESM import, then the package default.
|
|
8
26
|
*/
|
|
9
27
|
export function resolvePackageEsmEntryPath(specifier, rootDir) {
|
|
10
28
|
try {
|
|
11
|
-
|
|
29
|
+
const result = getEsmEntryResolver(rootDir).sync(path.resolve(rootDir), specifier);
|
|
30
|
+
return result.path;
|
|
12
31
|
}
|
|
13
32
|
catch {
|
|
14
33
|
return undefined;
|
|
@@ -56,13 +75,13 @@ export function resolveBrowserRuntimeEntryPath(options) {
|
|
|
56
75
|
if (specifier.startsWith('.')) {
|
|
57
76
|
return requireFromRoot.resolve(specifier);
|
|
58
77
|
}
|
|
59
|
-
const esmResolvedPath = isBarePackageImportSpecifier(specifier, rootDir)
|
|
60
|
-
? resolvePackageEsmEntryPath(specifier, rootDir)
|
|
61
|
-
: undefined;
|
|
62
78
|
const legacyModulePath = resolveLegacyPackageModuleEntry({ specifier, requireFromRoot, rootDir });
|
|
63
79
|
if (legacyModulePath) {
|
|
64
80
|
return legacyModulePath;
|
|
65
81
|
}
|
|
82
|
+
const esmResolvedPath = isBarePackageImportSpecifier(specifier, rootDir)
|
|
83
|
+
? resolvePackageEsmEntryPath(specifier, rootDir)
|
|
84
|
+
: undefined;
|
|
66
85
|
if (esmResolvedPath) {
|
|
67
86
|
return esmResolvedPath;
|
|
68
87
|
}
|
|
@@ -5,7 +5,11 @@ import type { BuildRuntime } from '../build/runtime/build-runtime.js';
|
|
|
5
5
|
import type { AnyIntegrationPlugin } from '../plugins/integration-plugin.js';
|
|
6
6
|
import type { Processor } from '../plugins/processor.js';
|
|
7
7
|
import type { EcoSourceTransform } from '../plugins/source-transform.js';
|
|
8
|
-
import type { PageMetadataProps, SitemapConfig } from './public-types.js';
|
|
8
|
+
import type { ComponentRenderInput, EcoComponent, PageMetadataProps, SitemapConfig } from './public-types.js';
|
|
9
|
+
/** Internal render input carrying route-resolved dependency roots. */
|
|
10
|
+
export type InternalComponentRenderInput = ComponentRenderInput & {
|
|
11
|
+
foreignChildRoots?: ReadonlyArray<EcoComponent | Partial<EcoComponent>>;
|
|
12
|
+
};
|
|
9
13
|
import type { RouteRegistry } from '../router/server/route-registry.js';
|
|
10
14
|
import type { CacheConfig } from '../services/cache/cache.types.js';
|
|
11
15
|
import type { DevGraphService } from '../services/runtime-state/dev-graph.service.js';
|