@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
@@ -1,182 +0,0 @@
1
- import { inspectBoundaryArtifactHtml } from './render-output.utils.ts';
2
- import type {
3
- IntegrationRendererRenderOptions,
4
- RouteRendererBody,
5
- RouteRendererOptions,
6
- RouteRenderResult,
7
- } from '../../types/public-types.ts';
8
-
9
- export interface CapturedHtmlRenderResult {
10
- body: RouteRendererBody;
11
- html: string;
12
- }
13
-
14
- export interface FinalizeHtmlRenderOptions {
15
- html: string;
16
- componentRootAttributes?: Record<string, string>;
17
- documentAttributes?: Record<string, string>;
18
- }
19
-
20
- export interface RenderExecutionCallbacks<C> {
21
- prepareRenderOptions(options: RouteRendererOptions): Promise<IntegrationRendererRenderOptions<C>>;
22
- render(renderOptions: IntegrationRendererRenderOptions<C>): Promise<RouteRendererBody>;
23
- getDocumentAttributes(renderOptions: IntegrationRendererRenderOptions<C>): Record<string, string> | undefined;
24
- applyAttributesToHtmlElement(html: string, attributes: Record<string, string>): string;
25
- applyAttributesToFirstBodyElement(html: string, attributes: Record<string, string>): string;
26
- transformResponse(response: Response): Promise<RouteRendererBody>;
27
- }
28
-
29
- /**
30
- * Executes the main post-preparation rendering flow for integration renderers.
31
- *
32
- * This service owns the orchestration that happens after normalized render
33
- * options have been prepared: one render pass, unresolved boundary-marker
34
- * enforcement, root-attribute application, and final HTML transformation into
35
- * a response body stream.
36
- */
37
- export class RenderExecutionService {
38
- async captureHtmlRender(render: () => Promise<RouteRendererBody>): Promise<CapturedHtmlRenderResult> {
39
- const renderedBody = await render();
40
- const capturedRender = await this.captureRenderedBody(renderedBody);
41
-
42
- return {
43
- body: capturedRender.body,
44
- html: capturedRender.html,
45
- };
46
- }
47
-
48
- /**
49
- * Executes one integration render pass and returns the final route render
50
- * result.
51
- *
52
- * @typeParam C Integration render output element type.
53
- * @param options Route-level render options.
54
- * @param currentIntegrationName Active integration name for this render pass.
55
- * @param callbacks Renderer-specific hooks required during execution.
56
- * @returns Final route render output with body and cache strategy.
57
- */
58
- async execute<C = unknown>(
59
- options: RouteRendererOptions,
60
- callbacks: RenderExecutionCallbacks<C>,
61
- ): Promise<RouteRenderResult> {
62
- const renderOptions = await callbacks.prepareRenderOptions(options);
63
- const shouldApplyComponentRootAttributes =
64
- renderOptions.componentRender?.canAttachAttributes &&
65
- renderOptions.componentRender.rootAttributes &&
66
- Object.keys(renderOptions.componentRender.rootAttributes).length > 0;
67
-
68
- const renderExecution = await this.captureHtmlRender(async () => callbacks.render(renderOptions));
69
- const boundaryArtifacts = inspectBoundaryArtifactHtml(renderExecution.html);
70
- const documentAttributes = callbacks.getDocumentAttributes(renderOptions);
71
- const hasBoundaryMarkerHtml = boundaryArtifacts.hasUnresolvedBoundaryArtifacts;
72
-
73
- if (hasBoundaryMarkerHtml) {
74
- throw new Error(
75
- '[ecopages] Route render returned unresolved boundary artifact HTML. Full-route unresolved-boundary fallback has been removed; resolve mixed boundaries inside renderComponentBoundary().',
76
- );
77
- }
78
-
79
- const canReuseCapturedBody =
80
- !hasBoundaryMarkerHtml &&
81
- !shouldApplyComponentRootAttributes &&
82
- !(documentAttributes && Object.keys(documentAttributes).length > 0);
83
-
84
- if (canReuseCapturedBody) {
85
- const body = await callbacks.transformResponse(
86
- new Response(renderExecution.body as BodyInit, {
87
- headers: {
88
- 'Content-Type': 'text/html',
89
- },
90
- }),
91
- );
92
-
93
- return {
94
- body,
95
- cacheStrategy: renderOptions.cacheStrategy,
96
- };
97
- }
98
-
99
- const finalization = await this.finalizeHtmlRender(
100
- {
101
- html: boundaryArtifacts.normalizedHtml,
102
- componentRootAttributes: shouldApplyComponentRootAttributes
103
- ? (renderOptions.componentRender?.rootAttributes as Record<string, string>)
104
- : undefined,
105
- documentAttributes,
106
- },
107
- {
108
- applyAttributesToHtmlElement: callbacks.applyAttributesToHtmlElement,
109
- applyAttributesToFirstBodyElement: callbacks.applyAttributesToFirstBodyElement,
110
- },
111
- );
112
-
113
- const body = await callbacks.transformResponse(
114
- new Response(finalization, {
115
- headers: {
116
- 'Content-Type': 'text/html',
117
- },
118
- }),
119
- );
120
-
121
- return {
122
- body,
123
- cacheStrategy: renderOptions.cacheStrategy,
124
- };
125
- }
126
-
127
- private async captureRenderedBody(body: RouteRendererBody): Promise<{ body: RouteRendererBody; html: string }> {
128
- const response = new Response(body as BodyInit);
129
-
130
- if (typeof body === 'string') {
131
- return {
132
- body,
133
- html: await response.text(),
134
- };
135
- }
136
-
137
- if (!response.body) {
138
- return {
139
- body,
140
- html: await response.text(),
141
- };
142
- }
143
-
144
- const [capturedBody, replayBody] = response.body.tee();
145
-
146
- return {
147
- body: replayBody,
148
- html: await new Response(capturedBody).text(),
149
- };
150
- }
151
-
152
- async finalizeHtmlRender(
153
- options: FinalizeHtmlRenderOptions,
154
- callbacks: Pick<
155
- RenderExecutionCallbacks<unknown>,
156
- 'applyAttributesToHtmlElement' | 'applyAttributesToFirstBodyElement'
157
- >,
158
- ): Promise<string> {
159
- return this.applyFinalHtmlAttributes(options.html, options, callbacks);
160
- }
161
-
162
- private applyFinalHtmlAttributes(
163
- html: string,
164
- options: FinalizeHtmlRenderOptions,
165
- callbacks: Pick<
166
- RenderExecutionCallbacks<unknown>,
167
- 'applyAttributesToHtmlElement' | 'applyAttributesToFirstBodyElement'
168
- >,
169
- ): string {
170
- let renderedHtml = html;
171
-
172
- if (options.componentRootAttributes && Object.keys(options.componentRootAttributes).length > 0) {
173
- renderedHtml = callbacks.applyAttributesToFirstBodyElement(renderedHtml, options.componentRootAttributes);
174
- }
175
-
176
- if (options.documentAttributes && Object.keys(options.documentAttributes).length > 0) {
177
- renderedHtml = callbacks.applyAttributesToHtmlElement(renderedHtml, options.documentAttributes);
178
- }
179
-
180
- return renderedHtml;
181
- }
182
- }
@@ -1,162 +0,0 @@
1
- import type {
2
- BaseIntegrationContext,
3
- ComponentRenderInput,
4
- ComponentRenderResult,
5
- EcoComponent,
6
- HtmlTemplateProps,
7
- PageMetadataProps,
8
- } from '../../types/public-types.ts';
9
- import type { ProcessedAsset } from '../../services/assets/asset-processing-service/index.ts';
10
- import type { RenderToResponseContext } from './integration-renderer.ts';
11
-
12
- type ShellRendererCache = BaseIntegrationContext['rendererCache'];
13
-
14
- export interface RouteShellComposerCallbacks {
15
- hasForeignBoundaryDescendants(component: EcoComponent): boolean;
16
- createHtmlResponse(body: BodyInit, ctx: RenderToResponseContext): Response;
17
- renderComponentBoundary(input: ComponentRenderInput): Promise<ComponentRenderResult>;
18
- prepareViewDependencies(view: EcoComponent, layout?: EcoComponent): Promise<ProcessedAsset[]>;
19
- getHtmlTemplate(): Promise<EcoComponent<HtmlTemplateProps>>;
20
- resolveViewMetadata<P>(view: EcoComponent<P>, props: P): Promise<PageMetadataProps>;
21
- appendProcessedDependencies(...assetGroups: Array<readonly ProcessedAsset[] | undefined>): ProcessedAsset[];
22
- finalizeResolvedHtml(options: {
23
- html: string;
24
- partial?: boolean;
25
- componentRootAttributes?: Record<string, string>;
26
- documentAttributes?: Record<string, string>;
27
- transformHtml?: boolean;
28
- }): Promise<string>;
29
- docType: string;
30
- }
31
-
32
- export class RouteShellComposer {
33
- async renderPartialViewResponse<P>(
34
- input: {
35
- view: EcoComponent<P>;
36
- props: P;
37
- ctx: RenderToResponseContext;
38
- renderInline?: () => Promise<BodyInit>;
39
- transformHtml?: (html: string) => string;
40
- },
41
- callbacks: RouteShellComposerCallbacks,
42
- ): Promise<Response> {
43
- if (input.renderInline && !callbacks.hasForeignBoundaryDescendants(input.view as EcoComponent)) {
44
- return callbacks.createHtmlResponse(await input.renderInline(), input.ctx);
45
- }
46
-
47
- const rendererCache = new Map<string, unknown>() as ShellRendererCache;
48
- const viewRender = await callbacks.renderComponentBoundary({
49
- component: input.view as EcoComponent,
50
- props: (input.props ?? {}) as Record<string, unknown>,
51
- integrationContext: { rendererCache },
52
- });
53
- const html = input.transformHtml ? input.transformHtml(viewRender.html) : viewRender.html;
54
-
55
- return callbacks.createHtmlResponse(html, input.ctx);
56
- }
57
-
58
- async renderViewWithDocumentShell<P>(
59
- input: {
60
- view: EcoComponent<P>;
61
- props: P;
62
- ctx: RenderToResponseContext;
63
- layout?: EcoComponent;
64
- },
65
- callbacks: RouteShellComposerCallbacks,
66
- ): Promise<Response> {
67
- const normalizedProps = (input.props ?? {}) as Record<string, unknown>;
68
-
69
- if (input.ctx.partial) {
70
- return this.renderPartialViewResponse(input, callbacks);
71
- }
72
-
73
- await callbacks.prepareViewDependencies(input.view, input.layout);
74
-
75
- const HtmlTemplate = await callbacks.getHtmlTemplate();
76
- const metadata = await callbacks.resolveViewMetadata(input.view, input.props);
77
- const rendererCache = new Map<string, unknown>() as ShellRendererCache;
78
- const viewRender = await callbacks.renderComponentBoundary({
79
- component: input.view as EcoComponent,
80
- props: normalizedProps,
81
- integrationContext: { rendererCache },
82
- });
83
- const layoutRender = input.layout
84
- ? await callbacks.renderComponentBoundary({
85
- component: input.layout,
86
- props: {},
87
- children: viewRender.html,
88
- integrationContext: { rendererCache },
89
- })
90
- : undefined;
91
- const documentRender = await callbacks.renderComponentBoundary({
92
- component: HtmlTemplate as EcoComponent,
93
- props: {
94
- metadata,
95
- pageProps: normalizedProps,
96
- },
97
- children: layoutRender?.html ?? viewRender.html,
98
- integrationContext: { rendererCache },
99
- });
100
-
101
- callbacks.appendProcessedDependencies(viewRender.assets, layoutRender?.assets, documentRender.assets);
102
-
103
- const html = await callbacks.finalizeResolvedHtml({
104
- html: `${callbacks.docType}${documentRender.html}`,
105
- partial: false,
106
- });
107
-
108
- return callbacks.createHtmlResponse(html, input.ctx);
109
- }
110
-
111
- async renderPageWithDocumentShell(
112
- input: {
113
- page: {
114
- component: EcoComponent;
115
- props: Record<string, unknown>;
116
- };
117
- layout?: {
118
- component: EcoComponent;
119
- props?: Record<string, unknown>;
120
- };
121
- htmlTemplate: EcoComponent;
122
- metadata: PageMetadataProps;
123
- pageProps: Record<string, unknown>;
124
- documentProps?: Record<string, unknown>;
125
- transformDocumentHtml?: (html: string) => string;
126
- },
127
- callbacks: RouteShellComposerCallbacks,
128
- ): Promise<string> {
129
- const rendererCache = new Map<string, unknown>() as ShellRendererCache;
130
- const pageRender = await callbacks.renderComponentBoundary({
131
- component: input.page.component,
132
- props: input.page.props,
133
- integrationContext: { rendererCache },
134
- });
135
- const layoutRender = input.layout
136
- ? await callbacks.renderComponentBoundary({
137
- component: input.layout.component,
138
- props: input.layout.props ?? {},
139
- children: pageRender.html,
140
- integrationContext: { rendererCache },
141
- })
142
- : undefined;
143
- const documentRender = await callbacks.renderComponentBoundary({
144
- component: input.htmlTemplate,
145
- props: {
146
- metadata: input.metadata,
147
- pageProps: input.pageProps,
148
- ...(input.documentProps ?? {}),
149
- },
150
- children: layoutRender?.html ?? pageRender.html,
151
- integrationContext: { rendererCache },
152
- });
153
-
154
- callbacks.appendProcessedDependencies(pageRender.assets, layoutRender?.assets, documentRender.assets);
155
-
156
- const documentHtml = input.transformDocumentHtml
157
- ? input.transformDocumentHtml(documentRender.html)
158
- : documentRender.html;
159
-
160
- return `${callbacks.docType}${documentHtml}`;
161
- }
162
- }
@@ -1,83 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { test } from 'vitest';
6
- import type { EcoPagesAppConfig } from '../../types/internal-types.ts';
7
- import { FSRouterScanner } from './fs-router-scanner.ts';
8
-
9
- test('FSRouterScanner scans dynamic routes in Node when module graph needs transpilation', async () => {
10
- if (typeof Bun !== 'undefined') {
11
- // This test expects Node's missing Bun global to trigger Esbuild transpilation. Skip in Bun runner.
12
- return;
13
- }
14
-
15
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecopages-fs-router-scanner-node-'));
16
- const pagesDir = path.join(rootDir, 'src', 'pages');
17
- const dynamicDir = path.join(pagesDir, 'dynamic');
18
- const distDir = path.join(rootDir, 'dist');
19
- const workDir = path.join(rootDir, '.eco');
20
-
21
- try {
22
- fs.mkdirSync(dynamicDir, { recursive: true });
23
- fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true });
24
-
25
- fs.writeFileSync(
26
- path.join(rootDir, 'src', 'decorated.ts'),
27
- [
28
- 'function sealed<T extends new (...args: never[]) => object>(target: T) {',
29
- '\treturn target;',
30
- '}',
31
- '@sealed',
32
- 'export class DecoratedModule {}',
33
- ].join('\n'),
34
- );
35
-
36
- fs.writeFileSync(
37
- path.join(dynamicDir, '[slug].kita.tsx'),
38
- [
39
- "import '../../decorated.ts';",
40
- '',
41
- 'export const getStaticPaths = async () => ({',
42
- "\tpaths: [{ params: { slug: 'hello-world' } }],",
43
- '});',
44
- '',
45
- 'export const getStaticProps = async () => ({ props: {} });',
46
- '',
47
- 'export default async function Page() {',
48
- "\treturn '<div>Hello</div>';",
49
- '}',
50
- ].join('\n'),
51
- );
52
-
53
- const scanner = new FSRouterScanner({
54
- dir: pagesDir,
55
- origin: 'http://localhost:3000',
56
- templatesExt: ['.kita.tsx'],
57
- options: {
58
- buildMode: false,
59
- },
60
- appConfig: {
61
- rootDir,
62
- workDir: '.eco',
63
- absolutePaths: {
64
- distDir,
65
- workDir,
66
- },
67
- } as unknown as EcoPagesAppConfig,
68
- });
69
-
70
- const routes = await scanner.scan();
71
-
72
- assert.equal(routes['http://localhost:3000/dynamic/hello-world']?.kind, 'dynamic');
73
- assert.equal(routes['http://localhost:3000/dynamic/hello-world']?.pathname, '/dynamic/[slug]');
74
-
75
- const transpiledModulesDir = path.join(workDir, '.server-route-modules');
76
- assert.equal(fs.existsSync(transpiledModulesDir), true);
77
-
78
- const transpiledFiles = fs.readdirSync(transpiledModulesDir).filter((file) => file.endsWith('.js'));
79
- assert.ok(transpiledFiles.length > 0);
80
- } finally {
81
- fs.rmSync(rootDir, { recursive: true, force: true });
82
- }
83
- });
@@ -1,224 +0,0 @@
1
- import path from 'node:path';
2
- import { appLogger } from '../../global/app-logger.ts';
3
- import type { EcoPagesAppConfig, RouteKind, Routes } from '../../types/internal-types.ts';
4
- import type { EcoPageFile, GetStaticPaths } from '../../types/public-types.ts';
5
- import { fileSystem } from '@ecopages/file-system';
6
- import { invariant } from '../../utils/invariant.ts';
7
- import { existsSync } from 'node:fs';
8
- import { getAppServerModuleTranspiler } from '../../services/module-loading/app-server-module-transpiler.service.ts';
9
- import type { ServerModuleTranspiler } from '../../services/module-loading/server-module-transpiler.service.ts';
10
- import { resolveInternalExecutionDir } from '../../utils/resolve-work-dir.ts';
11
-
12
- type CreateRouteArgs = {
13
- routePath: string;
14
- filePath: string;
15
- route: string;
16
- };
17
-
18
- type FSRouterScannerOptions = {
19
- buildMode: boolean;
20
- };
21
-
22
- /**
23
- * @class FSRouterScanner
24
- * @description
25
- * This class is responsible for scanning the file system for routes.
26
- * It uses the glob package to scan the file system for files with the specified file extensions.
27
- * It then creates a map of the routes with the pathname as the key.
28
- * The pathname is the route without the file extension.
29
- * For example, if the file is "index.tsx", the pathname will be "/index".
30
- * If the file is "blog/[slug].tsx", the pathname will be "/blog/[slug]".
31
- * If the file is "blog/[...slug].tsx", the pathname will be "/blog/[...slug]".
32
- */
33
- export class FSRouterScanner {
34
- private dir: string;
35
- private origin = '';
36
- private templatesExt: string[];
37
- private options: FSRouterScannerOptions;
38
- readonly appConfig: EcoPagesAppConfig;
39
- routes: Routes = {};
40
- private serverModuleTranspiler: ServerModuleTranspiler;
41
-
42
- constructor({
43
- dir,
44
- origin,
45
- templatesExt,
46
- options,
47
- appConfig,
48
- }: {
49
- dir: string;
50
- origin: string;
51
- templatesExt: string[];
52
- options: FSRouterScannerOptions;
53
- appConfig: EcoPagesAppConfig;
54
- }) {
55
- this.dir = dir;
56
- this.origin = origin;
57
- this.templatesExt = templatesExt;
58
- this.options = options;
59
- this.appConfig = appConfig;
60
- this.serverModuleTranspiler = getAppServerModuleTranspiler(appConfig);
61
- }
62
-
63
- private getRoutePath(path: string): string {
64
- const cleanedRoute = this.templatesExt
65
- .reduce((route, ext) => route.replace(ext, ''), path)
66
- .replace(/\/?index$/, '');
67
- return `/${cleanedRoute}`;
68
- }
69
-
70
- private getDynamicParamsNames(route: string): string[] {
71
- const matches = route.match(/\[.*?\]/g);
72
- return matches ? matches.map((match) => match.slice(1, -1)) : [];
73
- }
74
-
75
- private async getStaticPathsFromDynamicRoute({
76
- route,
77
- filePath,
78
- getStaticPaths,
79
- }: {
80
- route: string;
81
- filePath: string;
82
- getStaticPaths: GetStaticPaths;
83
- }): Promise<string[]> {
84
- const staticPaths = await getStaticPaths({
85
- appConfig: this.appConfig,
86
- runtimeOrigin: this.origin,
87
- });
88
- return staticPaths.paths.map((path) => {
89
- const dynamicParamsNames = this.getDynamicParamsNames(filePath);
90
- let routeWithParams = route;
91
-
92
- for (const param of dynamicParamsNames) {
93
- routeWithParams = routeWithParams.replace(`[${param}]`, (path.params as Record<string, string>)[param]);
94
- }
95
-
96
- return routeWithParams;
97
- });
98
- }
99
-
100
- private async createStaticRoutes({
101
- filePath,
102
- route,
103
- routePath,
104
- getStaticPaths,
105
- }: CreateRouteArgs & { getStaticPaths: GetStaticPaths }): Promise<void> {
106
- try {
107
- const routesWithParams = await this.getStaticPathsFromDynamicRoute({
108
- route,
109
- filePath,
110
- getStaticPaths,
111
- });
112
-
113
- for (const routeWithParams of routesWithParams) {
114
- this.createRoute('dynamic', { filePath, route: routeWithParams, routePath });
115
- }
116
- } catch (error) {
117
- appLogger.error(`[ecopages] Error creating static routes for ${filePath}: ${error}`);
118
- }
119
- }
120
-
121
- private async handleDynamicRouteCreation({ filePath, route, routePath }: CreateRouteArgs): Promise<void> {
122
- const module = (await this.importPageModule(filePath)) as EcoPageFile;
123
- const Page = module.default;
124
-
125
- /**
126
- * Check for attached static functions (consolidated API) or named exports (legacy)
127
- */
128
- const getStaticPaths = Page?.staticPaths ?? module.getStaticPaths;
129
- const getStaticProps = Page?.staticProps ?? module.getStaticProps;
130
-
131
- if (this.options.buildMode) {
132
- invariant(getStaticProps !== undefined, `[ecopages] Missing getStaticProps in ${filePath}`);
133
- invariant(getStaticPaths !== undefined, `[ecopages] Missing getStaticPaths in ${filePath}`);
134
- }
135
-
136
- if (getStaticPaths) {
137
- return this.createStaticRoutes({
138
- filePath,
139
- route,
140
- routePath,
141
- getStaticPaths,
142
- });
143
- }
144
-
145
- return this.createRoute('dynamic', { filePath, route, routePath });
146
- }
147
-
148
- private async importPageModule(filePath: string): Promise<unknown> {
149
- return this.serverModuleTranspiler.importModule({
150
- filePath,
151
- outdir: path.join(resolveInternalExecutionDir(this.appConfig), '.server-route-modules'),
152
- externalPackages: false,
153
- transpileErrorMessage: (details) => `Error transpiling route module: ${details}`,
154
- noOutputMessage: (targetFilePath) => `No transpiled output generated for route module: ${targetFilePath}`,
155
- });
156
- }
157
-
158
- private createRoute(kind: RouteKind, { filePath, route, routePath }: CreateRouteArgs): void {
159
- this.routes[route] = {
160
- kind,
161
- pathname: routePath,
162
- filePath,
163
- };
164
- }
165
-
166
- private getRouteData(file: string): CreateRouteArgs & {
167
- kind: RouteKind;
168
- } {
169
- const routePath = this.getRoutePath(file);
170
- const route = `${this.origin}${routePath}`;
171
- const filePath = path.join(this.dir, file);
172
- const isCatchAll = filePath.includes('[...');
173
- const isDynamic = !isCatchAll && filePath.includes('[') && filePath.includes(']');
174
- let kind: RouteKind = 'exact';
175
- if (isCatchAll) {
176
- kind = 'catch-all';
177
- } else if (isDynamic) {
178
- kind = 'dynamic';
179
- }
180
-
181
- return { route, routePath, filePath, kind };
182
- }
183
-
184
- async scan(): Promise<Routes> {
185
- this.routes = {};
186
-
187
- if (!existsSync(this.dir)) {
188
- return this.routes;
189
- }
190
-
191
- const scannedFiles = await fileSystem.glob(
192
- this.templatesExt.map((ext) => `**/*${ext}`),
193
- { cwd: this.dir },
194
- );
195
-
196
- for await (const file of scannedFiles) {
197
- if (file.includes('.ecopages-node.')) {
198
- continue;
199
- }
200
-
201
- const { kind, ...routeData } = this.getRouteData(file);
202
-
203
- switch (kind) {
204
- case 'dynamic':
205
- await this.handleDynamicRouteCreation(routeData);
206
- break;
207
- case 'catch-all':
208
- if (this.options.buildMode) {
209
- appLogger.warn(
210
- 'Catch-all routes are not supported in static generation, they will not be included in the bundle\n',
211
- `➤ ${routeData.filePath}`,
212
- );
213
- }
214
- this.createRoute(kind, routeData);
215
- break;
216
- default:
217
- this.createRoute(kind, routeData);
218
- break;
219
- }
220
- }
221
-
222
- return this.routes;
223
- }
224
- }