@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
|
@@ -203,28 +203,38 @@ describe('DependencyResolverService', () => {
|
|
|
203
203
|
file: componentFile,
|
|
204
204
|
},
|
|
205
205
|
dependencies: {
|
|
206
|
-
scripts: [
|
|
206
|
+
scripts: [
|
|
207
|
+
'./widget.script.ts',
|
|
208
|
+
'./other.ts',
|
|
209
|
+
{ src: './lazy.ts', lazy: { 'on:interaction': 'click' } },
|
|
210
|
+
],
|
|
207
211
|
},
|
|
208
212
|
};
|
|
209
213
|
|
|
210
214
|
try {
|
|
211
215
|
await service.processComponentDependencies([component], 'ecopages-jsx');
|
|
212
216
|
|
|
217
|
+
const fileScripts = capturedDeps.filter(
|
|
218
|
+
(dep): dep is Extract<AssetDefinition, { kind: 'script'; source: 'file' }> =>
|
|
219
|
+
dep.kind === 'script' && dep.source === 'file',
|
|
220
|
+
);
|
|
213
221
|
const contentScripts = capturedDeps.filter(
|
|
214
222
|
(dep): dep is Extract<AssetDefinition, { kind: 'script'; source: 'content' }> =>
|
|
215
223
|
dep.kind === 'script' && dep.source === 'content',
|
|
216
224
|
);
|
|
217
225
|
|
|
218
|
-
expect(
|
|
219
|
-
|
|
220
|
-
const pageScript = contentScripts.find((dep) => dep.packageRole === 'page-script');
|
|
221
|
-
const lazyEntry = contentScripts.find((dep) => dep.excludeFromHtml === true);
|
|
222
|
-
|
|
223
|
-
expect(pageScript).toEqual(
|
|
226
|
+
expect(fileScripts).toEqual([
|
|
224
227
|
expect.objectContaining({
|
|
225
|
-
|
|
228
|
+
filepath: widgetScript,
|
|
226
229
|
}),
|
|
227
|
-
|
|
230
|
+
expect.objectContaining({
|
|
231
|
+
filepath: siblingScript,
|
|
232
|
+
}),
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
expect(contentScripts).toHaveLength(1);
|
|
236
|
+
|
|
237
|
+
const lazyEntry = contentScripts.find((dep) => dep.excludeFromHtml === true);
|
|
228
238
|
|
|
229
239
|
expect(lazyEntry).toEqual(
|
|
230
240
|
expect.objectContaining({
|
|
@@ -236,7 +246,6 @@ describe('DependencyResolverService', () => {
|
|
|
236
246
|
}),
|
|
237
247
|
);
|
|
238
248
|
|
|
239
|
-
expect(pageScript?.groupedBundle).toBeUndefined();
|
|
240
249
|
expect(lazyEntry?.groupedBundle).toBeUndefined();
|
|
241
250
|
} finally {
|
|
242
251
|
rmSync(tempDir, { recursive: true, force: true });
|
|
@@ -248,7 +257,11 @@ describe('DependencyResolverService', () => {
|
|
|
248
257
|
const componentFile = join(tempDir, 'component.tsx');
|
|
249
258
|
const lazyScript = join(tempDir, 'theme-toggle.script.ts');
|
|
250
259
|
|
|
251
|
-
writeFileSync(
|
|
260
|
+
writeFileSync(
|
|
261
|
+
componentFile,
|
|
262
|
+
"import './theme-toggle.script';\nexport const Component = () => null;\n",
|
|
263
|
+
'utf-8',
|
|
264
|
+
);
|
|
252
265
|
writeFileSync(lazyScript, 'export const themeToggle = true;\n', 'utf-8');
|
|
253
266
|
|
|
254
267
|
let capturedDeps: AssetDefinition[] = [];
|
|
@@ -420,6 +433,8 @@ describe('DependencyResolverService', () => {
|
|
|
420
433
|
});
|
|
421
434
|
|
|
422
435
|
it('should collapse bundleable page stylesheets and scripts into page-owned assets', async () => {
|
|
436
|
+
const previousNodeEnv = process.env.NODE_ENV;
|
|
437
|
+
process.env.NODE_ENV = 'production';
|
|
423
438
|
const tempDir = mkdtempSync(join(tmpdir(), 'ecopages-page-bundle-'));
|
|
424
439
|
const componentFile = join(tempDir, 'component.tsx');
|
|
425
440
|
const stylesheetA = join(tempDir, 'first.css');
|
|
@@ -473,15 +488,96 @@ describe('DependencyResolverService', () => {
|
|
|
473
488
|
source: 'content',
|
|
474
489
|
packageRole: 'page-style',
|
|
475
490
|
content: '.first { color: red; }\n.second { color: blue; }',
|
|
491
|
+
bundledSourceFilepaths: [stylesheetA, stylesheetB],
|
|
476
492
|
}),
|
|
477
493
|
expect.objectContaining({
|
|
478
494
|
kind: 'script',
|
|
479
495
|
source: 'content',
|
|
480
496
|
packageRole: 'page-script',
|
|
481
497
|
content: `import ${JSON.stringify(scriptA)};\nimport ${JSON.stringify(scriptB)};`,
|
|
498
|
+
bundledSourceFilepaths: [scriptA, scriptB],
|
|
499
|
+
}),
|
|
500
|
+
]);
|
|
501
|
+
} finally {
|
|
502
|
+
process.env.NODE_ENV = previousNodeEnv;
|
|
503
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should keep bundleable page stylesheets and scripts as file assets during development', async () => {
|
|
508
|
+
const previousNodeEnv = process.env.NODE_ENV;
|
|
509
|
+
process.env.NODE_ENV = 'development';
|
|
510
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'ecopages-page-style-dev-'));
|
|
511
|
+
const componentFile = join(tempDir, 'component.tsx');
|
|
512
|
+
const stylesheetA = join(tempDir, 'first.css');
|
|
513
|
+
const stylesheetB = join(tempDir, 'second.css');
|
|
514
|
+
const scriptA = join(tempDir, 'first.ts');
|
|
515
|
+
const scriptB = join(tempDir, 'second.ts');
|
|
516
|
+
|
|
517
|
+
writeFileSync(componentFile, 'export const Component = () => null;', 'utf-8');
|
|
518
|
+
writeFileSync(stylesheetA, '.first { color: red; }', 'utf-8');
|
|
519
|
+
writeFileSync(stylesheetB, '.second { color: blue; }', 'utf-8');
|
|
520
|
+
writeFileSync(scriptA, 'export const first = true;', 'utf-8');
|
|
521
|
+
writeFileSync(scriptB, 'export const second = true;', 'utf-8');
|
|
522
|
+
|
|
523
|
+
let capturedDeps: AssetDefinition[] = [];
|
|
524
|
+
const assetProcessingService = {
|
|
525
|
+
processDependencies: vi.fn(async (deps: AssetDefinition[]) => {
|
|
526
|
+
capturedDeps = deps;
|
|
527
|
+
return [] as ProcessedAsset[];
|
|
528
|
+
}),
|
|
529
|
+
} as unknown as AssetProcessingService;
|
|
530
|
+
|
|
531
|
+
const bundleAppConfig = {
|
|
532
|
+
...appConfig,
|
|
533
|
+
rootDir: tempDir,
|
|
534
|
+
absolutePaths: {
|
|
535
|
+
srcDir: tempDir,
|
|
536
|
+
distDir: join(tempDir, '.eco/public'),
|
|
537
|
+
},
|
|
538
|
+
} as EcoPagesAppConfig;
|
|
539
|
+
|
|
540
|
+
const service = new DependencyResolverService(bundleAppConfig, assetProcessingService);
|
|
541
|
+
const component = ((_) => '<div></div>') as EcoComponent<Record<string, unknown>>;
|
|
542
|
+
component.config = {
|
|
543
|
+
__eco: {
|
|
544
|
+
id: 'page-style-dev',
|
|
545
|
+
integration: 'react',
|
|
546
|
+
file: componentFile,
|
|
547
|
+
},
|
|
548
|
+
dependencies: {
|
|
549
|
+
stylesheets: ['./first.css', './second.css'],
|
|
550
|
+
scripts: ['./first.ts', './second.ts'],
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
await service.processComponentDependencies([component], 'react');
|
|
556
|
+
|
|
557
|
+
expect(capturedDeps).toEqual([
|
|
558
|
+
expect.objectContaining({
|
|
559
|
+
kind: 'stylesheet',
|
|
560
|
+
source: 'file',
|
|
561
|
+
filepath: stylesheetA,
|
|
562
|
+
}),
|
|
563
|
+
expect.objectContaining({
|
|
564
|
+
kind: 'stylesheet',
|
|
565
|
+
source: 'file',
|
|
566
|
+
filepath: stylesheetB,
|
|
567
|
+
}),
|
|
568
|
+
expect.objectContaining({
|
|
569
|
+
kind: 'script',
|
|
570
|
+
source: 'file',
|
|
571
|
+
filepath: scriptA,
|
|
572
|
+
}),
|
|
573
|
+
expect.objectContaining({
|
|
574
|
+
kind: 'script',
|
|
575
|
+
source: 'file',
|
|
576
|
+
filepath: scriptB,
|
|
482
577
|
}),
|
|
483
578
|
]);
|
|
484
579
|
} finally {
|
|
580
|
+
process.env.NODE_ENV = previousNodeEnv;
|
|
485
581
|
rmSync(tempDir, { recursive: true, force: true });
|
|
486
582
|
}
|
|
487
583
|
});
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import type { EcoComponent } from '../../types/public-types.ts';
|
|
3
3
|
import type { EcoPagesAppConfig } from '../../types/internal-types.ts';
|
|
4
|
-
import type {
|
|
5
|
-
AssetProcessingService,
|
|
6
|
-
ProcessedAsset,
|
|
7
|
-
} from '../../services/assets/asset-processing-service/index.ts';
|
|
4
|
+
import type { AssetProcessingService, ProcessedAsset } from '../../services/assets/asset-processing-service/index.ts';
|
|
8
5
|
import { rapidhash } from '../../utils/hash.ts';
|
|
9
6
|
import { AssetFactory } from '../../services/assets/asset-processing-service/index.ts';
|
|
10
|
-
import {
|
|
11
|
-
buildResolvedLazyTriggers,
|
|
12
|
-
type ResolvedLazyGroup,
|
|
13
|
-
} from './lazy-trigger-planning.ts';
|
|
7
|
+
import { buildResolvedLazyTriggers, type ResolvedLazyGroup } from './lazy-trigger-planning.ts';
|
|
14
8
|
import { collectComponentDependencies } from './component-dependency-collection.ts';
|
|
15
|
-
import {
|
|
9
|
+
import { packagePageDependencies } from './page-dependency-bundling.ts';
|
|
16
10
|
|
|
17
11
|
export const DEPENDENCY_ERRORS = {
|
|
18
12
|
INVALID_STYLESHEET_ENTRY: 'Invalid stylesheet dependency entry: expected src or content',
|
|
@@ -104,13 +98,13 @@ export class DependencyResolverService {
|
|
|
104
98
|
},
|
|
105
99
|
});
|
|
106
100
|
|
|
107
|
-
const
|
|
108
|
-
const hasLazyDependencies =
|
|
101
|
+
const packagedDependencies = packagePageDependencies(dependencies, integrationName);
|
|
102
|
+
const hasLazyDependencies = packagedDependencies.some(
|
|
109
103
|
(dep) => dep.kind === 'script' && dep.excludeFromHtml === true,
|
|
110
104
|
);
|
|
111
105
|
|
|
112
106
|
const processedDependencies = await this.assetProcessingService.processDependencies(
|
|
113
|
-
|
|
107
|
+
packagedDependencies,
|
|
114
108
|
integrationName,
|
|
115
109
|
);
|
|
116
110
|
const lazyKeyToOutputUrl = new Map<string, string>();
|
|
@@ -47,7 +47,7 @@ function normalizeCssReferenceToken(token: string): string {
|
|
|
47
47
|
* working once multiple files are collapsed into one page-owned bundle.
|
|
48
48
|
*/
|
|
49
49
|
function isSafeBundledStylesheetContent(content: string): boolean {
|
|
50
|
-
for (const match of content.matchAll(/@import\s+(?:url\()?['"]?([^'"
|
|
50
|
+
for (const match of content.matchAll(/@import\s+(?:url\()?['"]?([^'")\s]+)['"]?\)?/g)) {
|
|
51
51
|
const reference = normalizeCssReferenceToken(match[1] ?? '');
|
|
52
52
|
if (reference && !isSafeBundledStylesheetReference(reference)) {
|
|
53
53
|
return false;
|
|
@@ -72,6 +72,13 @@ function isBundleableFileScriptAsset(dependency: AssetDefinition): dependency is
|
|
|
72
72
|
return dependency.kind === 'script' && dependency.source === 'file';
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
type PageDependencyPackagingPlan = {
|
|
76
|
+
bundledStylesheet?: AssetDefinition;
|
|
77
|
+
bundledScript?: AssetDefinition;
|
|
78
|
+
bundleableStyleFilepaths: Set<string>;
|
|
79
|
+
bundleableScriptFilepaths: Set<string>;
|
|
80
|
+
};
|
|
81
|
+
|
|
75
82
|
/**
|
|
76
83
|
* Returns whether the current integration should collapse eligible page assets into
|
|
77
84
|
* page-owned bundle entries.
|
|
@@ -80,53 +87,57 @@ export function shouldBundlePageDependencies(integrationName: string): boolean {
|
|
|
80
87
|
return integrationName === 'react' || integrationName === 'ecopages-jsx';
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
* Rewrites eligible flat dependency declarations into page-owned stylesheet and script bundles.
|
|
85
|
-
*
|
|
86
|
-
* Only assets with the default attribute shape and without explicit packaging roles are
|
|
87
|
-
* collapsed so integration-specific or lazy behavior keeps its existing ownership model.
|
|
88
|
-
*/
|
|
89
|
-
export function createUnifiedPageDependencies(
|
|
90
|
+
function createPageDependencyPackagingPlan(
|
|
90
91
|
dependencies: AssetDefinition[],
|
|
91
92
|
integrationName: string,
|
|
92
|
-
):
|
|
93
|
-
|
|
94
|
-
return dependencies;
|
|
95
|
-
}
|
|
93
|
+
): PageDependencyPackagingPlan | undefined {
|
|
94
|
+
const shouldBundleDependencies = process.env.NODE_ENV === 'production';
|
|
96
95
|
|
|
97
|
-
const bundleableStyles = dependencies
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
const bundleableStyles = dependencies
|
|
97
|
+
.filter((dependency) => {
|
|
98
|
+
if (!shouldBundleDependencies) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
if (dependency.kind !== 'stylesheet' || dependency.inline || dependency.position === 'body') {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
if (dependency.packageRole || !hasOnlyExpectedAttributes(dependency.attributes, { rel: 'stylesheet' })) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
if (dependency.source === 'content') {
|
|
111
|
+
return isSafeBundledStylesheetContent(dependency.content);
|
|
112
|
+
}
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
if (!existsSync(dependency.filepath)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
118
|
+
return isSafeBundledStylesheetContent(readFileSync(dependency.filepath, 'utf8'));
|
|
119
|
+
})
|
|
120
|
+
.filter(isBundleableFileStylesheetAsset);
|
|
121
|
+
|
|
122
|
+
const bundleableScripts = dependencies
|
|
123
|
+
.filter((dependency) => {
|
|
124
|
+
if (!shouldBundleDependencies) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
dependency.kind === 'script' &&
|
|
130
|
+
dependency.source === 'file' &&
|
|
131
|
+
!dependency.inline &&
|
|
132
|
+
!dependency.excludeFromHtml &&
|
|
133
|
+
dependency.position !== 'body' &&
|
|
134
|
+
!dependency.packageRole &&
|
|
135
|
+
dependency.bundle !== false &&
|
|
136
|
+
hasOnlyExpectedAttributes(dependency.attributes, { type: 'module', defer: '' }) &&
|
|
137
|
+
existsSync(dependency.filepath)
|
|
138
|
+
);
|
|
139
|
+
})
|
|
140
|
+
.filter(isBundleableFileScriptAsset);
|
|
130
141
|
|
|
131
142
|
const bundledStylesheet =
|
|
132
143
|
bundleableStyles.length > 1
|
|
@@ -135,56 +146,65 @@ export function createUnifiedPageDependencies(
|
|
|
135
146
|
position: 'head',
|
|
136
147
|
attributes: { rel: 'stylesheet' },
|
|
137
148
|
packageRole: 'page-style',
|
|
149
|
+
bundledSourceFilepaths: bundleableStyles.map((dependency) => dependency.filepath),
|
|
138
150
|
})
|
|
139
151
|
: undefined;
|
|
140
152
|
|
|
141
|
-
const bundleableStyleFilepaths = new Set(bundleableStyles.map((dependency) => dependency.filepath));
|
|
142
153
|
const pageScriptImports = [...new Set(bundleableScripts.map((dependency) => dependency.filepath))];
|
|
143
|
-
const bundleableScriptFilepaths = new Set(pageScriptImports);
|
|
144
|
-
|
|
145
154
|
const shouldBundlePageScript = pageScriptImports.length > 0 && bundleableScripts.length > 1;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
: undefined;
|
|
155
|
+
const bundledScript = shouldBundlePageScript
|
|
156
|
+
? AssetFactory.createContentScript({
|
|
157
|
+
name: `${integrationName}-page-${rapidhash(pageScriptImports.join('|')).toString(16)}`,
|
|
158
|
+
content: pageScriptImports.map((filepath) => `import ${JSON.stringify(filepath)};`).join('\n'),
|
|
159
|
+
position: 'head',
|
|
160
|
+
attributes: { type: 'module', defer: '' },
|
|
161
|
+
packageRole: 'page-script',
|
|
162
|
+
bundledSourceFilepaths: pageScriptImports,
|
|
163
|
+
})
|
|
164
|
+
: undefined;
|
|
157
165
|
|
|
158
166
|
if (!bundledStylesheet && !bundledScript) {
|
|
159
|
-
return
|
|
167
|
+
return undefined;
|
|
160
168
|
}
|
|
161
169
|
|
|
170
|
+
return {
|
|
171
|
+
bundledStylesheet,
|
|
172
|
+
bundledScript,
|
|
173
|
+
bundleableStyleFilepaths: new Set(bundleableStyles.map((dependency) => dependency.filepath)),
|
|
174
|
+
bundleableScriptFilepaths: new Set(pageScriptImports),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function applyPageDependencyPackagingPlan(
|
|
179
|
+
dependencies: AssetDefinition[],
|
|
180
|
+
plan: PageDependencyPackagingPlan,
|
|
181
|
+
): AssetDefinition[] {
|
|
162
182
|
const unifiedDependencies: AssetDefinition[] = [];
|
|
163
183
|
let insertedStylesheet = false;
|
|
164
184
|
let insertedScript = false;
|
|
165
185
|
|
|
166
186
|
for (const dependency of dependencies) {
|
|
167
187
|
if (
|
|
168
|
-
bundledStylesheet &&
|
|
188
|
+
plan.bundledStylesheet &&
|
|
169
189
|
dependency.kind === 'stylesheet' &&
|
|
170
190
|
dependency.source === 'file' &&
|
|
171
|
-
bundleableStyleFilepaths.has(dependency.filepath)
|
|
191
|
+
plan.bundleableStyleFilepaths.has(dependency.filepath)
|
|
172
192
|
) {
|
|
173
193
|
if (!insertedStylesheet) {
|
|
174
|
-
unifiedDependencies.push(bundledStylesheet);
|
|
194
|
+
unifiedDependencies.push(plan.bundledStylesheet);
|
|
175
195
|
insertedStylesheet = true;
|
|
176
196
|
}
|
|
177
197
|
continue;
|
|
178
198
|
}
|
|
179
199
|
|
|
180
200
|
if (
|
|
181
|
-
bundledScript &&
|
|
201
|
+
plan.bundledScript &&
|
|
182
202
|
dependency.kind === 'script' &&
|
|
183
203
|
dependency.source === 'file' &&
|
|
184
|
-
bundleableScriptFilepaths.has(dependency.filepath)
|
|
204
|
+
plan.bundleableScriptFilepaths.has(dependency.filepath)
|
|
185
205
|
) {
|
|
186
206
|
if (!insertedScript) {
|
|
187
|
-
unifiedDependencies.push(bundledScript);
|
|
207
|
+
unifiedDependencies.push(plan.bundledScript);
|
|
188
208
|
insertedScript = true;
|
|
189
209
|
}
|
|
190
210
|
continue;
|
|
@@ -193,13 +213,32 @@ export function createUnifiedPageDependencies(
|
|
|
193
213
|
unifiedDependencies.push(dependency);
|
|
194
214
|
}
|
|
195
215
|
|
|
196
|
-
if (bundledScript && !insertedScript) {
|
|
197
|
-
unifiedDependencies.push(bundledScript);
|
|
216
|
+
if (plan.bundledScript && !insertedScript) {
|
|
217
|
+
unifiedDependencies.push(plan.bundledScript);
|
|
198
218
|
}
|
|
199
219
|
|
|
200
|
-
if (bundledStylesheet && !insertedStylesheet) {
|
|
201
|
-
unifiedDependencies.push(bundledStylesheet);
|
|
220
|
+
if (plan.bundledStylesheet && !insertedStylesheet) {
|
|
221
|
+
unifiedDependencies.push(plan.bundledStylesheet);
|
|
202
222
|
}
|
|
203
223
|
|
|
204
224
|
return unifiedDependencies;
|
|
205
|
-
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Rewrites eligible flat dependency declarations into page-owned stylesheet and script bundles.
|
|
229
|
+
*
|
|
230
|
+
* Only assets with the default attribute shape and without explicit packaging roles are
|
|
231
|
+
* collapsed so integration-specific or lazy behavior keeps its existing ownership model.
|
|
232
|
+
*/
|
|
233
|
+
export function packagePageDependencies(dependencies: AssetDefinition[], integrationName: string): AssetDefinition[] {
|
|
234
|
+
if (!shouldBundlePageDependencies(integrationName)) {
|
|
235
|
+
return dependencies;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const plan = createPageDependencyPackagingPlan(dependencies, integrationName);
|
|
239
|
+
if (!plan) {
|
|
240
|
+
return dependencies;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return applyPageDependencyPackagingPlan(dependencies, plan);
|
|
244
|
+
}
|
|
@@ -1,43 +1,41 @@
|
|
|
1
1
|
import type { EcoPagesAppConfig } from '../types/internal-types.ts';
|
|
2
2
|
import type { IntegrationPlugin } from '../plugins/integration-plugin.ts';
|
|
3
|
-
import type { EcoPageFile, RouteRenderResult, RouteRendererOptions } from '../types/public-types.ts';
|
|
4
3
|
import { invariant } from '../utils/invariant.ts';
|
|
5
4
|
import { PathUtils } from '../utils/path-utils.module.ts';
|
|
6
|
-
import type { IntegrationRenderer
|
|
5
|
+
import type { IntegrationRenderer } from './orchestration/integration-renderer.ts';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
8
|
+
* Narrow route-render contract exposed to higher-level routing code.
|
|
10
9
|
*
|
|
11
10
|
* @remarks
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Higher-level routing code only needs request execution and page-module
|
|
12
|
+
* loading. Returning this narrowed shape avoids a dedicated wrapper class while
|
|
13
|
+
* still keeping callers off the full integration renderer surface.
|
|
15
14
|
*/
|
|
16
|
-
export
|
|
17
|
-
private renderer: IntegrationRenderer;
|
|
15
|
+
export type PageRouteRenderer = Pick<IntegrationRenderer, 'execute' | 'loadPageModule'>;
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Narrow explicit-view render contract exposed to static route handling.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Explicit static routes only need `renderToResponse()`, so the factory can
|
|
22
|
+
* hide the broader integration renderer surface there as well.
|
|
23
|
+
*/
|
|
24
|
+
export type ExplicitViewRenderer = Pick<IntegrationRenderer, 'renderToResponse'>;
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
async createRoute(options: RouteRendererOptions): Promise<RouteRenderResult> {
|
|
30
|
-
return this.renderer.execute(options);
|
|
31
|
-
}
|
|
26
|
+
export interface PageRendererResolver {
|
|
27
|
+
getPageRenderer(filePath: string): PageRouteRenderer;
|
|
28
|
+
}
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*/
|
|
36
|
-
async loadPageModule(filePath: string, options?: RouteModuleLoadOptions): Promise<EcoPageFile> {
|
|
37
|
-
return this.renderer.loadPageModule(filePath, options);
|
|
38
|
-
}
|
|
30
|
+
export interface ExplicitViewRendererResolver {
|
|
31
|
+
getExplicitViewRenderer(integrationName: string): ExplicitViewRenderer | null;
|
|
39
32
|
}
|
|
40
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Combined renderer-factory contract used by static generation.
|
|
36
|
+
*/
|
|
37
|
+
export type StaticGenerationRendererResolver = PageRendererResolver & ExplicitViewRendererResolver;
|
|
38
|
+
|
|
41
39
|
/**
|
|
42
40
|
* Selects and caches integration renderers for route files and explicit views.
|
|
43
41
|
*
|
|
@@ -72,17 +70,16 @@ export class RouteRendererFactory {
|
|
|
72
70
|
/**
|
|
73
71
|
* Returns a route renderer for the supplied route file.
|
|
74
72
|
*/
|
|
75
|
-
|
|
73
|
+
getPageRenderer(filePath: string): PageRouteRenderer {
|
|
76
74
|
const integrationRenderer = this.getRouteRendererEngine(filePath);
|
|
77
75
|
invariant(!!integrationRenderer, `No integration renderer found for file: ${filePath}`);
|
|
78
|
-
return
|
|
76
|
+
return integrationRenderer;
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
/**
|
|
82
|
-
*
|
|
83
|
-
* Used for explicit routing where views specify their integration via __eco.integration.
|
|
80
|
+
* Returns a renderer for an explicit view integration.
|
|
84
81
|
*/
|
|
85
|
-
|
|
82
|
+
getExplicitViewRenderer(integrationName: string): ExplicitViewRenderer | null {
|
|
86
83
|
const integrationPlugin = this.appConfig.integrations.find((plugin) => plugin.name === integrationName);
|
|
87
84
|
if (!integrationPlugin) {
|
|
88
85
|
return null;
|
package/src/router/README.md
CHANGED
|
@@ -8,9 +8,9 @@ The router layer determines what route is being handled and how the client runti
|
|
|
8
8
|
|
|
9
9
|
It is responsible for:
|
|
10
10
|
|
|
11
|
-
- filesystem route
|
|
12
|
-
- matching incoming request URLs to
|
|
13
|
-
- server-side static-path expansion for dynamic routes
|
|
11
|
+
- filesystem route discovery and classification (`exact`, `dynamic`, `catch-all`)
|
|
12
|
+
- matching incoming request URLs to canonical template routes
|
|
13
|
+
- server-side static-path expansion for dynamic template routes
|
|
14
14
|
- client-side navigation ownership and cross-runtime handoff
|
|
15
15
|
- keeping route discovery separate from rendering execution
|
|
16
16
|
|
|
@@ -18,9 +18,8 @@ It is responsible for:
|
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
router/
|
|
21
|
-
├── server/ # Server-side route
|
|
22
|
-
│
|
|
23
|
-
│ └── fs-router.ts # Matches request URLs to discovered routes
|
|
21
|
+
├── server/ # Server-side route discovery and matching
|
|
22
|
+
│ └── route-registry.ts # Owns template routes, request matching, static expansion, and reload
|
|
24
23
|
└── client/ # Browser-side navigation coordination
|
|
25
24
|
├── navigation-coordinator.ts # Singleton runtime coordinator
|
|
26
25
|
└── link-intent.ts # Shared anchor detection and intent recovery helpers
|
|
@@ -28,9 +27,9 @@ router/
|
|
|
28
27
|
|
|
29
28
|
## `server/`
|
|
30
29
|
|
|
31
|
-
### `
|
|
30
|
+
### `RouteRegistry`
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
Owns the canonical set of filesystem-discovered template routes for one application.
|
|
34
33
|
|
|
35
34
|
File patterns determine route kind:
|
|
36
35
|
|
|
@@ -40,13 +39,17 @@ File patterns determine route kind:
|
|
|
40
39
|
| `[slug].tsx` | `dynamic` | `/blog/[slug]` |
|
|
41
40
|
| `[...slug].tsx` | `catch-all` | `/docs/[...slug]` |
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
The registry stores canonical template routes only. It compiles request-time matching metadata during `init()` and `reload()`, but it does not execute `staticPaths()` during discovery.
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
Build-time static expansion is a separate operation. The registry invokes `staticPaths()` lazily through an injected page-module adapter and returns concrete static path expansions for the static generator.
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
The public interface is intentionally small:
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
- `templateRoutes` — ordered readonly template routes
|
|
49
|
+
- `init()` / `reload()` — rebuild discovery state and match metadata
|
|
50
|
+
- `matchRequest(requestUrl)` — request-time matching result with requested pathname, matched template route, params, and query
|
|
51
|
+
- `listStaticPathExpansions()` — build-time expansion for dynamic routes
|
|
52
|
+
- `listStaticGenerationRoutes()` — build-time route planning for static generation across exact and expanded routes
|
|
50
53
|
|
|
51
54
|
Match priority:
|
|
52
55
|
|
|
@@ -54,12 +57,6 @@ Match priority:
|
|
|
54
57
|
2. `dynamic` — the clean (bracket-stripped) prefix must appear in the pathname, and the segment counts must match.
|
|
55
58
|
3. `catch-all` — the clean prefix must appear in the pathname.
|
|
56
59
|
|
|
57
|
-
Additional helpers:
|
|
58
|
-
|
|
59
|
-
- `getDynamicParams(route, pathname)` — extracts named and spread parameters from a matched dynamic or catch-all route.
|
|
60
|
-
- `getSearchParams(url)` — converts `URLSearchParams` to a plain object.
|
|
61
|
-
- `setOnReload(cb)` / `reload()` — re-scans routes and fires an optional callback, used during development HMR.
|
|
62
|
-
|
|
63
60
|
## `client/`
|
|
64
61
|
|
|
65
62
|
### Navigation Coordinator (`navigation-coordinator.ts`)
|
|
@@ -69,7 +66,7 @@ A singleton browser-side runtime stored on `window.__ECO_PAGES__.navigation`.
|
|
|
69
66
|
Access it with:
|
|
70
67
|
|
|
71
68
|
```ts
|
|
72
|
-
import { getEcoNavigationRuntime } from '@ecopages/core/router/
|
|
69
|
+
import { getEcoNavigationRuntime } from '@ecopages/core/router/navigation-coordinator';
|
|
73
70
|
const runtime = getEcoNavigationRuntime();
|
|
74
71
|
```
|
|
75
72
|
|