@ecopages/core 0.2.0-alpha.25 → 0.2.0-alpha.27
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 +63 -7
- package/package.json +4 -47
- package/src/adapters/bun/create-app.ts +54 -2
- package/src/adapters/bun/hmr-manager.test.ts +0 -2
- package/src/adapters/bun/hmr-manager.ts +1 -24
- package/src/adapters/bun/server-adapter.ts +30 -4
- package/src/adapters/node/node-hmr-manager.test.ts +0 -2
- package/src/adapters/node/node-hmr-manager.ts +2 -25
- package/src/adapters/shared/explicit-static-render-preparation.ts +58 -0
- package/src/adapters/shared/explicit-static-route-matcher.test.ts +6 -6
- package/src/adapters/shared/explicit-static-route-matcher.ts +22 -31
- package/src/adapters/shared/file-route-middleware-pipeline.test.ts +5 -10
- package/src/adapters/shared/file-route-middleware-pipeline.ts +8 -17
- package/src/adapters/shared/fs-server-response-factory.test.ts +32 -43
- package/src/adapters/shared/fs-server-response-factory.ts +15 -37
- package/src/adapters/shared/fs-server-response-matcher.test.ts +65 -39
- package/src/adapters/shared/fs-server-response-matcher.ts +94 -43
- package/src/adapters/shared/hmr-manager.contract.test.ts +0 -4
- package/src/adapters/shared/render-context.ts +3 -3
- package/src/adapters/shared/server-adapter.test.ts +53 -0
- package/src/adapters/shared/server-adapter.ts +228 -159
- package/src/adapters/shared/server-route-handler.test.ts +6 -5
- package/src/adapters/shared/server-route-handler.ts +4 -4
- package/src/adapters/shared/server-static-builder.test.ts +4 -4
- package/src/adapters/shared/server-static-builder.ts +4 -4
- package/src/config/README.md +1 -1
- package/src/config/config-builder.test.ts +0 -1
- package/src/config/config-builder.ts +2 -7
- package/src/dev/host-runtime.ts +34 -0
- package/src/eco/eco.browser.test.ts +2 -2
- package/src/eco/eco.browser.ts +2 -2
- package/src/eco/eco.test.ts +6 -6
- package/src/eco/eco.ts +12 -12
- package/src/eco/eco.types.ts +3 -3
- package/src/errors/index.ts +1 -0
- package/src/hmr/client/hmr-runtime.ts +4 -2
- package/src/hmr/strategies/js-hmr-strategy.test.ts +0 -1
- package/src/hmr/strategies/js-hmr-strategy.ts +0 -6
- package/src/integrations/ghtml/ghtml-renderer.test.ts +7 -7
- package/src/integrations/ghtml/ghtml-renderer.ts +1 -11
- package/src/plugins/eco-component-meta-plugin.ts +0 -1
- package/src/plugins/integration-plugin.test.ts +9 -14
- package/src/plugins/integration-plugin.ts +34 -22
- package/src/plugins/processor.ts +17 -0
- package/src/route-renderer/GRAPH.md +81 -289
- package/src/route-renderer/README.md +67 -105
- package/src/route-renderer/orchestration/component-render-context.ts +45 -38
- package/src/route-renderer/orchestration/declared-ownership-graph.ts +62 -0
- package/src/route-renderer/orchestration/foreign-subtree-execution.service.ts +383 -0
- package/src/route-renderer/orchestration/integration-renderer.test.ts +118 -121
- package/src/route-renderer/orchestration/integration-renderer.ts +362 -403
- package/src/route-renderer/orchestration/ownership-planning.service.ts +97 -0
- package/src/route-renderer/orchestration/ownership-validation.service.ts +76 -0
- package/src/route-renderer/orchestration/processed-asset-dedupe.ts +1 -1
- package/src/route-renderer/orchestration/{queued-boundary-runtime.service.test.ts → queued-foreign-subtree-resolution.service.test.ts} +76 -71
- package/src/route-renderer/orchestration/{queued-boundary-runtime.service.ts → queued-foreign-subtree-resolution.service.ts} +68 -63
- package/src/route-renderer/orchestration/render-output.utils.ts +21 -13
- package/src/route-renderer/orchestration/{render-preparation.service.test.ts → route-render-orchestrator.prepare-render-options.test.ts} +160 -85
- package/src/route-renderer/orchestration/route-render-orchestrator.test.ts +265 -0
- package/src/route-renderer/orchestration/{render-preparation.service.ts → route-render-orchestrator.ts} +244 -160
- package/src/route-renderer/page-loading/component-dependency-collection.ts +9 -3
- package/src/route-renderer/page-loading/declared-asset-collection.ts +2 -5
- package/src/route-renderer/page-loading/dependency-resolver.test.ts +107 -11
- package/src/route-renderer/page-loading/dependency-resolver.ts +6 -12
- package/src/route-renderer/page-loading/ecopages-virtual-imports.ts +1 -1
- package/src/route-renderer/page-loading/lazy-entry-collection.ts +1 -1
- package/src/route-renderer/page-loading/lazy-trigger-planning.ts +1 -1
- package/src/route-renderer/page-loading/module-declaration-aggregation.ts +1 -1
- package/src/route-renderer/page-loading/module-declaration-scripts.ts +1 -1
- package/src/route-renderer/page-loading/page-dependency-bundling.ts +105 -66
- package/src/route-renderer/route-renderer.ts +28 -31
- package/src/router/README.md +16 -19
- package/src/router/server/route-registry.test.ts +176 -0
- package/src/router/server/route-registry.ts +382 -0
- package/src/services/README.md +1 -2
- package/src/services/assets/asset-processing-service/asset-dependency-keys.ts +1 -1
- package/src/services/assets/asset-processing-service/asset-processing.service.test.ts +1 -4
- package/src/services/assets/asset-processing-service/asset-processing.service.ts +1 -2
- package/src/services/assets/asset-processing-service/assets.types.ts +3 -0
- package/src/services/assets/asset-processing-service/grouped-content-bundles.ts +1 -1
- package/src/services/assets/asset-processing-service/index.ts +1 -0
- package/src/{route-renderer/orchestration/page-packaging.service.test.ts → services/assets/asset-processing-service/page-package.test.ts} +38 -14
- package/src/services/assets/asset-processing-service/page-package.ts +93 -0
- package/src/services/assets/asset-processing-service/processors/base/base-script-processor.ts +4 -5
- package/src/services/assets/asset-processing-service/processors/script/content-script.processor.test.ts +13 -10
- package/src/services/assets/asset-processing-service/processors/script/content-script.processor.ts +3 -0
- package/src/services/assets/asset-processing-service/processors/script/file-script.processor.ts +6 -0
- package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.ts +2 -0
- package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.ts +1 -0
- package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.ts +2 -0
- package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.ts +1 -1
- package/src/services/html/html-transformer.service.test.ts +1 -4
- package/src/services/module-loading/app-server-module-transpiler.service.ts +1 -3
- package/src/services/module-loading/node-bootstrap-plugin.ts +17 -3
- package/src/services/module-loading/page-module-import.service.ts +0 -1
- package/src/services/module-loading/source-module-support.ts +1 -1
- package/src/static-site-generator/static-site-generator.test.ts +124 -32
- package/src/static-site-generator/static-site-generator.ts +168 -185
- package/src/types/internal-types.ts +13 -12
- package/src/types/public-types.ts +55 -39
- package/src/watchers/project-watcher.test-helpers.ts +4 -3
- package/src/route-renderer/orchestration/boundary-planning.service.ts +0 -146
- package/src/route-renderer/orchestration/page-packaging.service.ts +0 -85
- package/src/route-renderer/orchestration/render-execution.service.test.ts +0 -196
- package/src/route-renderer/orchestration/render-execution.service.ts +0 -182
- package/src/route-renderer/orchestration/route-shell-composer.service.ts +0 -162
- package/src/router/server/fs-router-scanner.test.ts +0 -83
- package/src/router/server/fs-router-scanner.ts +0 -224
- package/src/router/server/fs-router.test.ts +0 -214
- package/src/router/server/fs-router.ts +0 -122
- package/src/services/runtime-state/runtime-specifier-registry.service.ts +0 -96
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { FIXTURE_APP_PROJECT_DIR } from '../../../__fixtures__/constants.js';
|
|
3
|
+
import { ConfigBuilder } from '../../config/config-builder.js';
|
|
4
|
+
import type { EcoPagesAppConfig } from '../../types/internal-types.js';
|
|
5
|
+
import { RouteRegistry, type RouteRegistryPageModuleAdapter, type TemplateRoute } from './route-registry.ts';
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
templatesExt,
|
|
9
|
+
absolutePaths: { pagesDir },
|
|
10
|
+
...appConfig
|
|
11
|
+
} = await new ConfigBuilder().setRootDir(FIXTURE_APP_PROJECT_DIR).build();
|
|
12
|
+
|
|
13
|
+
const createRegistry = (overrides?: Partial<ConstructorParameters<typeof RouteRegistry>[0]>) =>
|
|
14
|
+
new RouteRegistry({
|
|
15
|
+
pagesDir,
|
|
16
|
+
appConfig: appConfig as EcoPagesAppConfig,
|
|
17
|
+
origin: 'http://localhost:3000',
|
|
18
|
+
templatesExt,
|
|
19
|
+
buildMode: false,
|
|
20
|
+
pageModuleAdapter: {
|
|
21
|
+
loadPageModule: vi.fn(async () => ({})),
|
|
22
|
+
},
|
|
23
|
+
...overrides,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('RouteRegistry', () => {
|
|
27
|
+
test('discovers canonical template routes', async () => {
|
|
28
|
+
const registry = createRegistry();
|
|
29
|
+
|
|
30
|
+
await registry.init();
|
|
31
|
+
|
|
32
|
+
expect(registry.templateRoutes.map((route) => route.pathname)).toEqual([
|
|
33
|
+
'/',
|
|
34
|
+
'/404',
|
|
35
|
+
'/postcss-hmr',
|
|
36
|
+
'/dynamic/[slug]',
|
|
37
|
+
'/catch-all/[...path]',
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test.each([
|
|
42
|
+
['/products/[id]', '/products/123', { id: '123' }],
|
|
43
|
+
['/products/[...id]', '/products/123/456/789', { id: ['123', '456', '789'] }],
|
|
44
|
+
])('matches template route %p against request %p', async (pathname, requestPathname, expectedParams) => {
|
|
45
|
+
const registry = createRegistry();
|
|
46
|
+
await registry.init();
|
|
47
|
+
|
|
48
|
+
(registry as unknown as { templateRouteList: TemplateRoute[] }).templateRouteList = [
|
|
49
|
+
{
|
|
50
|
+
pathname,
|
|
51
|
+
kind: pathname.includes('[...') ? 'catch-all' : 'dynamic',
|
|
52
|
+
filePath: '/pages/example.ts',
|
|
53
|
+
paramNames: [],
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const result = registry.matchRequest(`http://localhost:3000${requestPathname}`);
|
|
58
|
+
|
|
59
|
+
expect(result?.params).toEqual(expectedParams);
|
|
60
|
+
expect(result?.templateRoute.pathname).toBe(pathname);
|
|
61
|
+
expect(result?.requestedPathname).toBe(requestPathname);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('includes query parameters in match results', async () => {
|
|
65
|
+
const registry = createRegistry();
|
|
66
|
+
await registry.init();
|
|
67
|
+
|
|
68
|
+
(registry as unknown as { templateRouteList: TemplateRoute[] }).templateRouteList = [
|
|
69
|
+
{ pathname: '/page', kind: 'exact', filePath: '/pages/page.ts', paramNames: [] },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const result = registry.matchRequest('http://localhost:3000/page?sort=asc&page=2');
|
|
73
|
+
|
|
74
|
+
expect(result?.query).toEqual({ sort: 'asc', page: '2' });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('prefers exact routes over dynamic routes', async () => {
|
|
78
|
+
const registry = createRegistry();
|
|
79
|
+
await registry.init();
|
|
80
|
+
|
|
81
|
+
(registry as unknown as { templateRouteList: TemplateRoute[] }).templateRouteList = [
|
|
82
|
+
{ pathname: '/blog/latest', kind: 'exact', filePath: '/pages/blog/latest.ts', paramNames: [] },
|
|
83
|
+
{ pathname: '/blog/[slug]', kind: 'dynamic', filePath: '/pages/blog/[slug].ts', paramNames: ['slug'] },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const result = registry.matchRequest('http://localhost:3000/blog/latest');
|
|
87
|
+
|
|
88
|
+
expect(result?.templateRoute.kind).toBe('exact');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('fires reload listeners after reload', async () => {
|
|
92
|
+
const registry = createRegistry();
|
|
93
|
+
const listener = vi.fn();
|
|
94
|
+
|
|
95
|
+
registry.onReload(listener);
|
|
96
|
+
await registry.reload();
|
|
97
|
+
|
|
98
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('lists static path expansions lazily through the page module adapter', async () => {
|
|
102
|
+
const pageModuleAdapter: RouteRegistryPageModuleAdapter = {
|
|
103
|
+
loadPageModule: vi.fn(async () => ({
|
|
104
|
+
staticProps: {},
|
|
105
|
+
staticPaths: async () => ({ paths: [{ params: { slug: 'hello-world' } }] }),
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const registry = createRegistry({ pageModuleAdapter, buildMode: true });
|
|
110
|
+
await registry.init();
|
|
111
|
+
|
|
112
|
+
(registry as unknown as { templateRouteList: TemplateRoute[] }).templateRouteList = [
|
|
113
|
+
{ pathname: '/blog/[slug]', kind: 'dynamic', filePath: '/pages/blog/[slug].ts', paramNames: ['slug'] },
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const expansions = await registry.listStaticPathExpansions({ runtimeOrigin: 'http://localhost:3000' });
|
|
117
|
+
|
|
118
|
+
expect(expansions).toEqual([
|
|
119
|
+
{
|
|
120
|
+
pathname: '/blog/hello-world',
|
|
121
|
+
templateRoute: {
|
|
122
|
+
pathname: '/blog/[slug]',
|
|
123
|
+
kind: 'dynamic',
|
|
124
|
+
filePath: '/pages/blog/[slug].ts',
|
|
125
|
+
paramNames: ['slug'],
|
|
126
|
+
},
|
|
127
|
+
params: { slug: 'hello-world' },
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
expect(pageModuleAdapter.loadPageModule).toHaveBeenCalledWith('/pages/blog/[slug].ts');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('lists static-generation routes through one registry seam', async () => {
|
|
134
|
+
const pageModuleAdapter: RouteRegistryPageModuleAdapter = {
|
|
135
|
+
loadPageModule: vi.fn(async () => ({
|
|
136
|
+
staticProps: {},
|
|
137
|
+
staticPaths: async () => ({ paths: [{ params: { slug: 'hello-world' } }] }),
|
|
138
|
+
})),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const registry = createRegistry({ pageModuleAdapter, buildMode: true });
|
|
142
|
+
await registry.init();
|
|
143
|
+
|
|
144
|
+
(registry as unknown as { templateRouteList: TemplateRoute[] }).templateRouteList = [
|
|
145
|
+
{ pathname: '/', kind: 'exact', filePath: '/pages/index.ts', paramNames: [] },
|
|
146
|
+
{ pathname: '/blog/[slug]', kind: 'dynamic', filePath: '/pages/blog/[slug].ts', paramNames: ['slug'] },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const routes = await registry.listStaticGenerationRoutes({ runtimeOrigin: 'http://localhost:3000' });
|
|
150
|
+
|
|
151
|
+
expect(routes).toEqual([
|
|
152
|
+
{
|
|
153
|
+
requestUrl: 'http://localhost:3000/',
|
|
154
|
+
pathname: '/',
|
|
155
|
+
templateRoute: {
|
|
156
|
+
pathname: '/',
|
|
157
|
+
kind: 'exact',
|
|
158
|
+
filePath: '/pages/index.ts',
|
|
159
|
+
paramNames: [],
|
|
160
|
+
},
|
|
161
|
+
params: {},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
requestUrl: 'http://localhost:3000/blog/hello-world',
|
|
165
|
+
pathname: '/blog/hello-world',
|
|
166
|
+
templateRoute: {
|
|
167
|
+
pathname: '/blog/[slug]',
|
|
168
|
+
kind: 'dynamic',
|
|
169
|
+
filePath: '/pages/blog/[slug].ts',
|
|
170
|
+
paramNames: ['slug'],
|
|
171
|
+
},
|
|
172
|
+
params: { slug: 'hello-world' },
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { fileSystem } from '@ecopages/file-system';
|
|
4
|
+
import { appLogger } from '../../global/app-logger.ts';
|
|
5
|
+
import type { EcoPagesAppConfig, RouteKind } from '../../types/internal-types.ts';
|
|
6
|
+
import { invariant } from '../../utils/invariant.ts';
|
|
7
|
+
|
|
8
|
+
export type RouteParams = Record<string, string | string[]>;
|
|
9
|
+
export type RouteQuery = Record<string, string>;
|
|
10
|
+
|
|
11
|
+
export type TemplateRoute = {
|
|
12
|
+
readonly pathname: string;
|
|
13
|
+
readonly kind: RouteKind;
|
|
14
|
+
readonly filePath: string;
|
|
15
|
+
readonly paramNames: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type RouteMatch = {
|
|
19
|
+
readonly requestedPathname: string;
|
|
20
|
+
readonly templateRoute: TemplateRoute;
|
|
21
|
+
readonly params: RouteParams;
|
|
22
|
+
readonly query: RouteQuery;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type StaticPathExpansion = {
|
|
26
|
+
readonly pathname: string;
|
|
27
|
+
readonly templateRoute: TemplateRoute;
|
|
28
|
+
readonly params: RouteParams;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type StaticGenerationRoute = {
|
|
32
|
+
readonly requestUrl: string;
|
|
33
|
+
readonly pathname: string;
|
|
34
|
+
readonly templateRoute: TemplateRoute;
|
|
35
|
+
readonly params: RouteParams;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type StaticPathsContext = {
|
|
39
|
+
readonly appConfig: EcoPagesAppConfig;
|
|
40
|
+
readonly runtimeOrigin: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type RouteRegistryPageModule = {
|
|
44
|
+
readonly staticPaths?: (context: StaticPathsContext) => Promise<{
|
|
45
|
+
paths: Array<{ params: RouteParams }>;
|
|
46
|
+
}>;
|
|
47
|
+
readonly staticProps?: unknown;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface RouteRegistryPageModuleAdapter {
|
|
51
|
+
loadPageModule(filePath: string): Promise<RouteRegistryPageModule>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type RouteRegistryOptions = {
|
|
55
|
+
pagesDir: string;
|
|
56
|
+
appConfig: EcoPagesAppConfig;
|
|
57
|
+
origin: string;
|
|
58
|
+
templatesExt: readonly string[];
|
|
59
|
+
buildMode: boolean;
|
|
60
|
+
pageModuleAdapter: RouteRegistryPageModuleAdapter;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const ROUTE_PRIORITY: Record<RouteKind, number> = {
|
|
64
|
+
exact: 0,
|
|
65
|
+
dynamic: 1,
|
|
66
|
+
'catch-all': 2,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export class RouteRegistry {
|
|
70
|
+
readonly origin: string;
|
|
71
|
+
readonly appConfig: EcoPagesAppConfig;
|
|
72
|
+
readonly pagesDir: string;
|
|
73
|
+
readonly templatesExt: readonly string[];
|
|
74
|
+
readonly buildMode: boolean;
|
|
75
|
+
private readonly pageModuleAdapter: RouteRegistryPageModuleAdapter;
|
|
76
|
+
private templateRouteList: TemplateRoute[] = [];
|
|
77
|
+
private readonly reloadListeners = new Set<() => void>();
|
|
78
|
+
|
|
79
|
+
constructor(options: RouteRegistryOptions) {
|
|
80
|
+
this.origin = options.origin;
|
|
81
|
+
this.appConfig = options.appConfig;
|
|
82
|
+
this.pagesDir = options.pagesDir;
|
|
83
|
+
this.templatesExt = options.templatesExt;
|
|
84
|
+
this.buildMode = options.buildMode;
|
|
85
|
+
this.pageModuleAdapter = options.pageModuleAdapter;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get templateRoutes(): readonly TemplateRoute[] {
|
|
89
|
+
return this.templateRouteList;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async init(): Promise<void> {
|
|
93
|
+
this.templateRouteList = await this.scanTemplateRoutes();
|
|
94
|
+
appLogger.debug('RouteRegistry initialized', this.templateRouteList);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async reload(): Promise<void> {
|
|
98
|
+
await this.init();
|
|
99
|
+
|
|
100
|
+
for (const listener of this.reloadListeners) {
|
|
101
|
+
listener();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onReload(listener: () => void): () => void {
|
|
106
|
+
this.reloadListeners.add(listener);
|
|
107
|
+
return () => {
|
|
108
|
+
this.reloadListeners.delete(listener);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
matchRequest(requestUrl: string): RouteMatch | null {
|
|
113
|
+
const url = new URL(requestUrl);
|
|
114
|
+
const requestedPathname = normalizePathname(url.pathname);
|
|
115
|
+
const query = this.getSearchParams(url);
|
|
116
|
+
|
|
117
|
+
for (const route of this.templateRouteList) {
|
|
118
|
+
if (route.kind !== 'exact') {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (requestedPathname === route.pathname || requestedPathname === `${route.pathname}/`) {
|
|
123
|
+
return {
|
|
124
|
+
requestedPathname,
|
|
125
|
+
templateRoute: route,
|
|
126
|
+
params: {},
|
|
127
|
+
query,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const route of this.templateRouteList) {
|
|
133
|
+
if (route.kind !== 'dynamic') {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const params = this.tryExtractParams(route, requestedPathname);
|
|
138
|
+
if (!params) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
requestedPathname,
|
|
144
|
+
templateRoute: route,
|
|
145
|
+
params,
|
|
146
|
+
query,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const route of this.templateRouteList) {
|
|
151
|
+
if (route.kind !== 'catch-all') {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const params = this.tryExtractParams(route, requestedPathname);
|
|
156
|
+
if (!params) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
requestedPathname,
|
|
162
|
+
templateRoute: route,
|
|
163
|
+
params,
|
|
164
|
+
query,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async listStaticPathExpansions(input: { runtimeOrigin: string }): Promise<readonly StaticPathExpansion[]> {
|
|
172
|
+
const expansions: StaticPathExpansion[] = [];
|
|
173
|
+
|
|
174
|
+
for (const route of this.templateRouteList) {
|
|
175
|
+
if (route.kind === 'exact') {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const pageModule = await this.pageModuleAdapter.loadPageModule(route.filePath);
|
|
180
|
+
const staticPaths = pageModule.staticPaths;
|
|
181
|
+
|
|
182
|
+
if (this.buildMode) {
|
|
183
|
+
invariant(staticPaths !== undefined, `[ecopages] Missing getStaticPaths in ${route.filePath}`);
|
|
184
|
+
invariant(
|
|
185
|
+
pageModule.staticProps !== undefined,
|
|
186
|
+
`[ecopages] Missing getStaticProps in ${route.filePath}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!staticPaths) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const result = await staticPaths({
|
|
195
|
+
appConfig: this.appConfig,
|
|
196
|
+
runtimeOrigin: input.runtimeOrigin,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
for (const { params } of result.paths) {
|
|
200
|
+
expansions.push({
|
|
201
|
+
pathname: this.resolveTemplatePath(route.pathname, params),
|
|
202
|
+
templateRoute: route,
|
|
203
|
+
params,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return expansions;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async listStaticGenerationRoutes(input: { runtimeOrigin: string }): Promise<readonly StaticGenerationRoute[]> {
|
|
212
|
+
const staticPathExpansions = await this.listStaticPathExpansions(input);
|
|
213
|
+
|
|
214
|
+
return [
|
|
215
|
+
...this.templateRouteList
|
|
216
|
+
.filter((route) => route.kind === 'exact')
|
|
217
|
+
.map((route) => ({
|
|
218
|
+
requestUrl: `${input.runtimeOrigin}${route.pathname}`,
|
|
219
|
+
pathname: route.pathname,
|
|
220
|
+
templateRoute: route,
|
|
221
|
+
params: {},
|
|
222
|
+
})),
|
|
223
|
+
...staticPathExpansions.map((route) => ({
|
|
224
|
+
requestUrl: `${input.runtimeOrigin}${route.pathname}`,
|
|
225
|
+
pathname: route.pathname,
|
|
226
|
+
templateRoute: route.templateRoute,
|
|
227
|
+
params: route.params,
|
|
228
|
+
})),
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async scanTemplateRoutes(): Promise<TemplateRoute[]> {
|
|
233
|
+
if (!existsSync(this.pagesDir)) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const scannedFiles = await fileSystem.glob(
|
|
238
|
+
this.templatesExt.map((ext) => `**/*${ext}`),
|
|
239
|
+
{
|
|
240
|
+
cwd: this.pagesDir,
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const templateRoutes: TemplateRoute[] = [];
|
|
245
|
+
|
|
246
|
+
for await (const file of scannedFiles) {
|
|
247
|
+
if (file.includes('.ecopages-node.')) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const routePathname = this.getRoutePath(file);
|
|
252
|
+
const filePath = path.join(this.pagesDir, file);
|
|
253
|
+
const kind = this.classifyRouteKind(filePath);
|
|
254
|
+
|
|
255
|
+
templateRoutes.push({
|
|
256
|
+
pathname: routePathname,
|
|
257
|
+
kind,
|
|
258
|
+
filePath,
|
|
259
|
+
paramNames: this.getParamNames(routePathname),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return templateRoutes.sort((left, right) => {
|
|
264
|
+
const priorityDifference = ROUTE_PRIORITY[left.kind] - ROUTE_PRIORITY[right.kind];
|
|
265
|
+
if (priorityDifference !== 0) {
|
|
266
|
+
return priorityDifference;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (left.pathname === '/') {
|
|
270
|
+
return -1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (right.pathname === '/') {
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return left.pathname.localeCompare(right.pathname);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private getRoutePath(file: string): string {
|
|
282
|
+
const cleanedRoute = this.templatesExt
|
|
283
|
+
.reduce((route, ext) => route.replace(ext, ''), file)
|
|
284
|
+
.replace(/\/?index$/, '');
|
|
285
|
+
|
|
286
|
+
return normalizePathname(`/${cleanedRoute}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private classifyRouteKind(filePath: string): RouteKind {
|
|
290
|
+
if (filePath.includes('[...')) {
|
|
291
|
+
return 'catch-all';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (filePath.includes('[') && filePath.includes(']')) {
|
|
295
|
+
return 'dynamic';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return 'exact';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private getParamNames(routePathname: string): string[] {
|
|
302
|
+
const matches = routePathname.match(/\[(?:\.\.\.)?([^\]]+)\]/g);
|
|
303
|
+
return matches ? matches.map((match) => match.replace(/^\[(?:\.\.\.)?/, '').replace(/\]$/, '')) : [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private tryExtractParams(route: TemplateRoute, requestedPathname: string): RouteParams | null {
|
|
307
|
+
const routeParts = route.pathname.split('/');
|
|
308
|
+
const pathnameParts = requestedPathname.split('/');
|
|
309
|
+
|
|
310
|
+
if (route.kind === 'dynamic' && routeParts.length !== pathnameParts.length) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const params: RouteParams = {};
|
|
315
|
+
|
|
316
|
+
for (let i = 0; i < routeParts.length; i++) {
|
|
317
|
+
const routePart = routeParts[i];
|
|
318
|
+
const pathnamePart = pathnameParts[i];
|
|
319
|
+
|
|
320
|
+
if (!routePart) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (routePart.startsWith('[...') && routePart.endsWith(']')) {
|
|
325
|
+
const paramName = routePart.slice(4, -1);
|
|
326
|
+
params[paramName] = pathnameParts.slice(i).filter(Boolean);
|
|
327
|
+
return params;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (routePart.startsWith('[') && routePart.endsWith(']')) {
|
|
331
|
+
if (pathnamePart === undefined) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
params[routePart.slice(1, -1)] = pathnamePart;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (routePart !== pathnamePart) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (route.kind === 'catch-all') {
|
|
345
|
+
const cleanPathname = route.pathname.replace(/\[.*?\]/g, '');
|
|
346
|
+
return requestedPathname.includes(cleanPathname) ? params : null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return params;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private getSearchParams(url: URL): RouteQuery {
|
|
353
|
+
const query: RouteQuery = {};
|
|
354
|
+
|
|
355
|
+
for (const [key, value] of url.searchParams) {
|
|
356
|
+
query[key] = value;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return query;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private resolveTemplatePath(pathname: string, params: RouteParams): string {
|
|
363
|
+
let resolvedPath = pathname;
|
|
364
|
+
|
|
365
|
+
for (const [key, value] of Object.entries(params)) {
|
|
366
|
+
const serializedValue = Array.isArray(value) ? value.join('/') : value;
|
|
367
|
+
resolvedPath = resolvedPath.replace(`[...${key}]`, serializedValue);
|
|
368
|
+
resolvedPath = resolvedPath.replace(`[${key}]`, serializedValue);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return normalizePathname(resolvedPath);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function normalizePathname(pathname: string): string {
|
|
376
|
+
if (!pathname || pathname === '/') {
|
|
377
|
+
return '/';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const normalized = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
381
|
+
return normalized.endsWith('/') ? normalized.slice(0, -1) : normalized;
|
|
382
|
+
}
|
package/src/services/README.md
CHANGED
|
@@ -11,7 +11,6 @@ Typical responsibilities include:
|
|
|
11
11
|
- server-module loading and transpilation
|
|
12
12
|
- browser bundle coordination
|
|
13
13
|
- asset processing and runtime asset declaration helpers
|
|
14
|
-
- runtime specifier registry management
|
|
15
14
|
- server invalidation state and entrypoint dependency graphs
|
|
16
15
|
- HTML finalization and dependency injection
|
|
17
16
|
|
|
@@ -20,7 +19,7 @@ Typical responsibilities include:
|
|
|
20
19
|
- `module-loading/`: framework-owned config/app bootstrap loading and server-side source loading
|
|
21
20
|
- `assets/`: shared browser build coordination and processed asset pipelines
|
|
22
21
|
- `invalidation/`: file-change classification and invalidation policy
|
|
23
|
-
- `runtime-state/`: app-owned invalidation state
|
|
22
|
+
- `runtime-state/`: app-owned invalidation state and dependency graphs
|
|
24
23
|
- `runtime-manifest/`: node runtime manifest derivation and persistence
|
|
25
24
|
- `html/`: final HTML dependency injection and rewriter selection
|
|
26
25
|
|
|
@@ -400,10 +400,7 @@ test('AssetProcessingService - grouped content scripts use processGrouped once p
|
|
|
400
400
|
|
|
401
401
|
expect(processGroupedMock).toHaveBeenCalledTimes(1);
|
|
402
402
|
expect(processMock).not.toHaveBeenCalled();
|
|
403
|
-
expect(results.map((result) => result.srcUrl)).toEqual([
|
|
404
|
-
'/assets/page-entry.js',
|
|
405
|
-
'/assets/lazy-entry.js',
|
|
406
|
-
]);
|
|
403
|
+
expect(results.map((result) => result.srcUrl)).toEqual(['/assets/page-entry.js', '/assets/lazy-entry.js']);
|
|
407
404
|
});
|
|
408
405
|
|
|
409
406
|
test('AssetProcessingService - clearCache clears all cached assets', async () => {
|
|
@@ -114,8 +114,7 @@ export class AssetProcessingService {
|
|
|
114
114
|
getCachedAsset: (assetDep, depKey) => this.getCachedAsset(assetDep, depKey),
|
|
115
115
|
getProcessor: (assetDep) => this.registry.getProcessor(assetDep.kind, assetDep.source),
|
|
116
116
|
resolveProcessedAssetSrcUrl: (processed) => this.resolveProcessedAssetSrcUrl(processed),
|
|
117
|
-
setCachedAsset: (assetDep, depKey, processed) =>
|
|
118
|
-
this.setCachedAsset(assetDep, depKey, processed),
|
|
117
|
+
setCachedAsset: (assetDep, depKey, processed) => this.setCachedAsset(assetDep, depKey, processed),
|
|
119
118
|
logMissingProcessor: (assetDep) => {
|
|
120
119
|
appLogger.error(`No processor found for ${assetDep.kind}/${assetDep.source}`);
|
|
121
120
|
},
|
|
@@ -16,6 +16,7 @@ export interface BaseAsset {
|
|
|
16
16
|
attributes?: Record<string, string>;
|
|
17
17
|
position?: AssetPosition;
|
|
18
18
|
packageRole?: AssetPackageRole;
|
|
19
|
+
bundledSourceFilepaths?: string[];
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface ScriptAsset extends BaseAsset {
|
|
@@ -105,6 +106,7 @@ export interface JsonScriptAsset extends ScriptAsset {
|
|
|
105
106
|
|
|
106
107
|
export type ProcessedAsset = {
|
|
107
108
|
filepath?: string;
|
|
109
|
+
sourceFilepath?: string;
|
|
108
110
|
srcUrl?: string;
|
|
109
111
|
content?: string;
|
|
110
112
|
kind: AssetKind;
|
|
@@ -114,6 +116,7 @@ export type ProcessedAsset = {
|
|
|
114
116
|
excludeFromHtml?: boolean;
|
|
115
117
|
packageRole?: AssetPackageRole;
|
|
116
118
|
groupedBundle?: GroupedScriptBundle;
|
|
119
|
+
bundledSourceFilepaths?: string[];
|
|
117
120
|
};
|
|
118
121
|
|
|
119
122
|
export type AssetDefinition =
|