@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.
Files changed (111) hide show
  1. package/README.md +63 -7
  2. package/package.json +4 -47
  3. package/src/adapters/bun/create-app.ts +54 -2
  4. package/src/adapters/bun/hmr-manager.test.ts +0 -2
  5. package/src/adapters/bun/hmr-manager.ts +1 -24
  6. package/src/adapters/bun/server-adapter.ts +30 -4
  7. package/src/adapters/node/node-hmr-manager.test.ts +0 -2
  8. package/src/adapters/node/node-hmr-manager.ts +2 -25
  9. package/src/adapters/shared/explicit-static-render-preparation.ts +58 -0
  10. package/src/adapters/shared/explicit-static-route-matcher.test.ts +6 -6
  11. package/src/adapters/shared/explicit-static-route-matcher.ts +22 -31
  12. package/src/adapters/shared/file-route-middleware-pipeline.test.ts +5 -10
  13. package/src/adapters/shared/file-route-middleware-pipeline.ts +8 -17
  14. package/src/adapters/shared/fs-server-response-factory.test.ts +32 -43
  15. package/src/adapters/shared/fs-server-response-factory.ts +15 -37
  16. package/src/adapters/shared/fs-server-response-matcher.test.ts +65 -39
  17. package/src/adapters/shared/fs-server-response-matcher.ts +94 -43
  18. package/src/adapters/shared/hmr-manager.contract.test.ts +0 -4
  19. package/src/adapters/shared/render-context.ts +3 -3
  20. package/src/adapters/shared/server-adapter.test.ts +53 -0
  21. package/src/adapters/shared/server-adapter.ts +228 -159
  22. package/src/adapters/shared/server-route-handler.test.ts +6 -5
  23. package/src/adapters/shared/server-route-handler.ts +4 -4
  24. package/src/adapters/shared/server-static-builder.test.ts +4 -4
  25. package/src/adapters/shared/server-static-builder.ts +4 -4
  26. package/src/config/README.md +1 -1
  27. package/src/config/config-builder.test.ts +0 -1
  28. package/src/config/config-builder.ts +2 -7
  29. package/src/dev/host-runtime.ts +34 -0
  30. package/src/eco/eco.browser.test.ts +2 -2
  31. package/src/eco/eco.browser.ts +2 -2
  32. package/src/eco/eco.test.ts +6 -6
  33. package/src/eco/eco.ts +12 -12
  34. package/src/eco/eco.types.ts +3 -3
  35. package/src/errors/index.ts +1 -0
  36. package/src/hmr/client/hmr-runtime.ts +4 -2
  37. package/src/hmr/strategies/js-hmr-strategy.test.ts +0 -1
  38. package/src/hmr/strategies/js-hmr-strategy.ts +0 -6
  39. package/src/integrations/ghtml/ghtml-renderer.test.ts +7 -7
  40. package/src/integrations/ghtml/ghtml-renderer.ts +1 -11
  41. package/src/plugins/eco-component-meta-plugin.ts +0 -1
  42. package/src/plugins/integration-plugin.test.ts +9 -14
  43. package/src/plugins/integration-plugin.ts +34 -22
  44. package/src/plugins/processor.ts +17 -0
  45. package/src/route-renderer/GRAPH.md +81 -289
  46. package/src/route-renderer/README.md +67 -105
  47. package/src/route-renderer/orchestration/component-render-context.ts +45 -38
  48. package/src/route-renderer/orchestration/declared-ownership-graph.ts +62 -0
  49. package/src/route-renderer/orchestration/foreign-subtree-execution.service.ts +383 -0
  50. package/src/route-renderer/orchestration/integration-renderer.test.ts +118 -121
  51. package/src/route-renderer/orchestration/integration-renderer.ts +362 -403
  52. package/src/route-renderer/orchestration/ownership-planning.service.ts +97 -0
  53. package/src/route-renderer/orchestration/ownership-validation.service.ts +76 -0
  54. package/src/route-renderer/orchestration/processed-asset-dedupe.ts +1 -1
  55. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.test.ts → queued-foreign-subtree-resolution.service.test.ts} +76 -71
  56. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.ts → queued-foreign-subtree-resolution.service.ts} +68 -63
  57. package/src/route-renderer/orchestration/render-output.utils.ts +21 -13
  58. package/src/route-renderer/orchestration/{render-preparation.service.test.ts → route-render-orchestrator.prepare-render-options.test.ts} +160 -85
  59. package/src/route-renderer/orchestration/route-render-orchestrator.test.ts +265 -0
  60. package/src/route-renderer/orchestration/{render-preparation.service.ts → route-render-orchestrator.ts} +244 -160
  61. package/src/route-renderer/page-loading/component-dependency-collection.ts +9 -3
  62. package/src/route-renderer/page-loading/declared-asset-collection.ts +2 -5
  63. package/src/route-renderer/page-loading/dependency-resolver.test.ts +107 -11
  64. package/src/route-renderer/page-loading/dependency-resolver.ts +6 -12
  65. package/src/route-renderer/page-loading/ecopages-virtual-imports.ts +1 -1
  66. package/src/route-renderer/page-loading/lazy-entry-collection.ts +1 -1
  67. package/src/route-renderer/page-loading/lazy-trigger-planning.ts +1 -1
  68. package/src/route-renderer/page-loading/module-declaration-aggregation.ts +1 -1
  69. package/src/route-renderer/page-loading/module-declaration-scripts.ts +1 -1
  70. package/src/route-renderer/page-loading/page-dependency-bundling.ts +105 -66
  71. package/src/route-renderer/route-renderer.ts +28 -31
  72. package/src/router/README.md +16 -19
  73. package/src/router/server/route-registry.test.ts +176 -0
  74. package/src/router/server/route-registry.ts +382 -0
  75. package/src/services/README.md +1 -2
  76. package/src/services/assets/asset-processing-service/asset-dependency-keys.ts +1 -1
  77. package/src/services/assets/asset-processing-service/asset-processing.service.test.ts +1 -4
  78. package/src/services/assets/asset-processing-service/asset-processing.service.ts +1 -2
  79. package/src/services/assets/asset-processing-service/assets.types.ts +3 -0
  80. package/src/services/assets/asset-processing-service/grouped-content-bundles.ts +1 -1
  81. package/src/services/assets/asset-processing-service/index.ts +1 -0
  82. package/src/{route-renderer/orchestration/page-packaging.service.test.ts → services/assets/asset-processing-service/page-package.test.ts} +38 -14
  83. package/src/services/assets/asset-processing-service/page-package.ts +93 -0
  84. package/src/services/assets/asset-processing-service/processors/base/base-script-processor.ts +4 -5
  85. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.test.ts +13 -10
  86. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.ts +3 -0
  87. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.ts +6 -0
  88. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.ts +2 -0
  89. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.ts +1 -0
  90. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.ts +2 -0
  91. package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.ts +1 -1
  92. package/src/services/html/html-transformer.service.test.ts +1 -4
  93. package/src/services/module-loading/app-server-module-transpiler.service.ts +1 -3
  94. package/src/services/module-loading/node-bootstrap-plugin.ts +17 -3
  95. package/src/services/module-loading/page-module-import.service.ts +0 -1
  96. package/src/services/module-loading/source-module-support.ts +1 -1
  97. package/src/static-site-generator/static-site-generator.test.ts +124 -32
  98. package/src/static-site-generator/static-site-generator.ts +168 -185
  99. package/src/types/internal-types.ts +13 -12
  100. package/src/types/public-types.ts +55 -39
  101. package/src/watchers/project-watcher.test-helpers.ts +4 -3
  102. package/src/route-renderer/orchestration/boundary-planning.service.ts +0 -146
  103. package/src/route-renderer/orchestration/page-packaging.service.ts +0 -85
  104. package/src/route-renderer/orchestration/render-execution.service.test.ts +0 -196
  105. package/src/route-renderer/orchestration/render-execution.service.ts +0 -182
  106. package/src/route-renderer/orchestration/route-shell-composer.service.ts +0 -162
  107. package/src/router/server/fs-router-scanner.test.ts +0 -83
  108. package/src/router/server/fs-router-scanner.ts +0 -224
  109. package/src/router/server/fs-router.test.ts +0 -214
  110. package/src/router/server/fs-router.ts +0 -122
  111. 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
+ }
@@ -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, dependency graphs, and runtime specifier registry
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
 
@@ -63,4 +63,4 @@ export function deduplicateAssetDependencies(deps: AssetDefinition[]): AssetDefi
63
63
  }
64
64
 
65
65
  return Array.from(seen.values());
66
- }
66
+ }
@@ -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 =
@@ -101,4 +101,4 @@ export async function processGroupedDependencyBundles(
101
101
  });
102
102
 
103
103
  return (await Promise.all(groupedPromises)).flat();
104
- }
104
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './asset.factory.ts';
2
+ export * from './page-package.ts';
2
3
  export * from './asset-processing.service.ts';
3
4
  export * from './assets.types.ts';
4
5
  export * from './browser-runtime-asset.factory.ts';