@ecopages/core 0.2.0-alpha.14 → 0.2.0-alpha.16

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 (24) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +2 -2
  3. package/src/adapters/shared/fs-server-response-matcher.d.ts +3 -11
  4. package/src/adapters/shared/fs-server-response-matcher.js +6 -19
  5. package/src/build/build-adapter.d.ts +6 -0
  6. package/src/build/build-adapter.js +109 -36
  7. package/src/plugins/integration-plugin.d.ts +17 -0
  8. package/src/plugins/integration-plugin.js +26 -10
  9. package/src/route-renderer/orchestration/integration-renderer.d.ts +22 -2
  10. package/src/route-renderer/orchestration/integration-renderer.js +26 -7
  11. package/src/route-renderer/orchestration/render-preparation.service.js +6 -0
  12. package/src/route-renderer/page-loading/page-module-loader.d.ts +1 -0
  13. package/src/route-renderer/page-loading/page-module-loader.js +1 -0
  14. package/src/route-renderer/route-renderer.d.ts +6 -2
  15. package/src/route-renderer/route-renderer.js +6 -0
  16. package/src/services/module-loading/app-module-loader.service.d.ts +3 -0
  17. package/src/services/module-loading/app-module-loader.service.js +4 -0
  18. package/src/services/module-loading/app-server-module-transpiler.service.js +2 -2
  19. package/src/services/module-loading/node-bootstrap-plugin.d.ts +23 -0
  20. package/src/services/module-loading/node-bootstrap-plugin.js +23 -6
  21. package/src/services/module-loading/page-module-import.service.d.ts +1 -0
  22. package/src/services/module-loading/page-module-import.service.js +20 -8
  23. package/src/static-site-generator/static-site-generator.d.ts +0 -5
  24. package/src/static-site-generator/static-site-generator.js +6 -21
package/CHANGELOG.md CHANGED
@@ -14,12 +14,17 @@ All notable changes to `@ecopages/core` are documented here.
14
14
 
15
15
  - Consolidated runtime state around shared module-loading services, app-owned build execution, and the universal `createApp()` boundary.
16
16
  - Simplified route-renderer orchestration around renderer-owned boundary runtimes, shared string-boundary queue helpers, and a smaller component render context.
17
+ - Centralized shared integration renderer bootstrapping so package integrations only append renderer-specific config instead of duplicating core lifecycle wiring.
17
18
  - Removed marker-era compatibility capture, the shared route-level fallback resolver, deprecated `@ecopages/core/node*` escape hatches, and other dead route-renderer internals.
18
19
 
19
20
  ### Bug Fixes
20
21
 
21
22
  - Fixed mixed-integration page, layout, document, and component rendering to resolve foreign boundaries inside their owning renderer across the built-in integrations.
22
23
  - Fixed host/runtime module loading, published build-helper exports, asset output normalization, explicit render flows, and static or preview build stability across Bun, Node, Vite, and Nitro.
24
+ - Fixed request-time and static-generation page inspection to preserve integration-specific page loading without reusing the normal render module identity.
25
+ - Fixed Node preview and static-generation React runtime resolution so app-owned page modules and server rendering share one React module identity.
26
+ - Fixed Bun browser output normalization so batched multi-entrypoint HMR rebuilds match emitted files to their expected served paths instead of Bun output order.
27
+ - Fixed render-preparation graph traversal so sparse component dependency arrays do not break custom 404 rendering or file-system response fallback flows.
23
28
 
24
29
  ### Documentation
25
30
 
@@ -28,6 +33,7 @@ All notable changes to `@ecopages/core` are documented here.
28
33
  ### Tests
29
34
 
30
35
  - Added regression coverage for app-owned runtime services, Node fallback paths, and cross-runtime invalidation behavior.
36
+ - Strengthened the core ghtml integration tests so route and explicit render paths await real outcomes and cover `renderToResponse` behavior.
31
37
 
32
38
  ---
33
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-alpha.14",
3
+ "version": "0.2.0-alpha.16",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -17,7 +17,7 @@
17
17
  "directory": "packages/core"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-alpha.14",
20
+ "@ecopages/file-system": "0.2.0-alpha.16",
21
21
  "@ecopages/logger": "^0.2.3",
22
22
  "@ecopages/scripts-injector": "^0.1.3",
23
23
  "@worker-tools/html-rewriter": "0.1.0-pre.19",
@@ -4,10 +4,6 @@ import type { FSRouter } from '../../router/server/fs-router.js';
4
4
  import type { PageCacheService } from '../../services/cache/page-cache-service.js';
5
5
  import type { CacheStrategy } from '../../services/cache/cache.types.js';
6
6
  import type { FileSystemServerResponseFactory } from './fs-server-response-factory.js';
7
- export declare const FILE_SYSTEM_RESPONSE_MATCHER_ERRORS: {
8
- readonly transpilePageModuleFailed: (details: string) => string;
9
- readonly noTranspiledOutputForPageModule: (filePath: string) => string;
10
- };
11
7
  export interface FileSystemResponseMatcherOptions {
12
8
  appConfig: EcoPagesAppConfig;
13
9
  router: FSRouter;
@@ -21,7 +17,6 @@ export interface FileSystemResponseMatcherOptions {
21
17
  /**
22
18
  * Matches file-system routes to rendered HTML responses.
23
19
  *
24
- * This class sits at the request-time boundary between router matches and the
25
20
  * render pipeline. It coordinates page module inspection, request-local policy,
26
21
  * renderer invocation, middleware execution, cache integration, and fallback
27
22
  * error translation.
@@ -31,14 +26,12 @@ export declare class FileSystemResponseMatcher {
31
26
  private router;
32
27
  private routeRendererFactory;
33
28
  private fileSystemResponseFactory;
34
- private serverModuleTranspiler;
35
29
  private pageRequestCacheCoordinator;
36
30
  private fileRouteMiddlewarePipeline;
37
31
  constructor({ appConfig, router, routeRendererFactory, fileSystemResponseFactory, cacheService, defaultCacheStrategy, }: FileSystemResponseMatcherOptions);
38
32
  /**
39
33
  * Resolves unmatched paths either as static asset requests or as the custom
40
34
  * not-found page.
41
- *
42
35
  * @param requestUrl Incoming pathname.
43
36
  * @returns Static file response or rendered 404 response.
44
37
  */
@@ -59,10 +52,9 @@ export declare class FileSystemResponseMatcher {
59
52
  * Loads the matched page module for request-time inspection.
60
53
  *
61
54
  * The matcher needs access to page-level metadata such as `cache` and
62
- * `middleware` before full rendering starts, so it uses the shared module
63
- * import service directly rather than going through route rendering. The
64
- * app config is injected explicitly so build ownership stays at the adapter
65
- * boundary instead of leaking through nested router collaborators.
55
+ * `middleware` before full rendering starts, so it asks the owning route
56
+ * renderer to load the page module. That preserves integration-specific page
57
+ * import setup for request-time inspection as well as for full rendering.
66
58
  *
67
59
  * @param filePath Absolute page module path.
68
60
  * @returns Imported page module.
@@ -1,22 +1,15 @@
1
1
  import path from "node:path";
2
2
  import { appLogger } from "../../global/app-logger.js";
3
3
  import { PageRequestCacheCoordinator } from "../../services/cache/page-request-cache-coordinator.service.js";
4
- import { getAppServerModuleTranspiler } from "../../services/module-loading/app-server-module-transpiler.service.js";
5
- import { resolveInternalExecutionDir } from "../../utils/resolve-work-dir.js";
6
4
  import { ServerUtils } from "../../utils/server-utils.module.js";
7
5
  import { FileRouteMiddlewarePipeline } from "./file-route-middleware-pipeline.js";
8
6
  import { LocalsAccessError } from "../../errors/locals-access-error.js";
9
7
  import { isDevelopmentRuntime } from "../../utils/runtime.js";
10
- const FILE_SYSTEM_RESPONSE_MATCHER_ERRORS = {
11
- transpilePageModuleFailed: (details) => `Error transpiling page module: ${details}`,
12
- noTranspiledOutputForPageModule: (filePath) => `No transpiled output generated for page module: ${filePath}`
13
- };
14
8
  class FileSystemResponseMatcher {
15
9
  appConfig;
16
10
  router;
17
11
  routeRendererFactory;
18
12
  fileSystemResponseFactory;
19
- serverModuleTranspiler;
20
13
  pageRequestCacheCoordinator;
21
14
  fileRouteMiddlewarePipeline;
22
15
  constructor({
@@ -31,14 +24,12 @@ class FileSystemResponseMatcher {
31
24
  this.router = router;
32
25
  this.routeRendererFactory = routeRendererFactory;
33
26
  this.fileSystemResponseFactory = fileSystemResponseFactory;
34
- this.serverModuleTranspiler = getAppServerModuleTranspiler(appConfig);
35
27
  this.pageRequestCacheCoordinator = new PageRequestCacheCoordinator(cacheService, defaultCacheStrategy);
36
28
  this.fileRouteMiddlewarePipeline = new FileRouteMiddlewarePipeline(cacheService);
37
29
  }
38
30
  /**
39
31
  * Resolves unmatched paths either as static asset requests or as the custom
40
32
  * not-found page.
41
- *
42
33
  * @param requestUrl Incoming pathname.
43
34
  * @returns Static file response or rendered 404 response.
44
35
  */
@@ -131,20 +122,17 @@ class FileSystemResponseMatcher {
131
122
  * Loads the matched page module for request-time inspection.
132
123
  *
133
124
  * The matcher needs access to page-level metadata such as `cache` and
134
- * `middleware` before full rendering starts, so it uses the shared module
135
- * import service directly rather than going through route rendering. The
136
- * app config is injected explicitly so build ownership stays at the adapter
137
- * boundary instead of leaking through nested router collaborators.
125
+ * `middleware` before full rendering starts, so it asks the owning route
126
+ * renderer to load the page module. That preserves integration-specific page
127
+ * import setup for request-time inspection as well as for full rendering.
138
128
  *
139
129
  * @param filePath Absolute page module path.
140
130
  * @returns Imported page module.
141
131
  */
142
132
  async importPageModule(filePath) {
143
- return this.serverModuleTranspiler.importModule({
144
- filePath,
145
- outdir: path.join(resolveInternalExecutionDir(this.appConfig), ".server-modules-meta"),
146
- transpileErrorMessage: FILE_SYSTEM_RESPONSE_MATCHER_ERRORS.transpilePageModuleFailed,
147
- noOutputMessage: FILE_SYSTEM_RESPONSE_MATCHER_ERRORS.noTranspiledOutputForPageModule
133
+ const routeRenderer = this.routeRendererFactory.createRenderer(filePath);
134
+ return routeRenderer.loadPageModule(filePath, {
135
+ cacheScope: "request-metadata"
148
136
  });
149
137
  }
150
138
  /**
@@ -155,6 +143,5 @@ class FileSystemResponseMatcher {
155
143
  }
156
144
  }
157
145
  export {
158
- FILE_SYSTEM_RESPONSE_MATCHER_ERRORS,
159
146
  FileSystemResponseMatcher
160
147
  };
@@ -108,8 +108,14 @@ export declare class BunBuildAdapter implements BuildAdapter {
108
108
  private mapBunFormat;
109
109
  private getOutputExtension;
110
110
  private resolveConcreteOutputPath;
111
+ private normalizePathForMatch;
112
+ private normalizeOutputPathForMatch;
113
+ private extractTemplateHashTokens;
114
+ private applyTemplateHashTokens;
111
115
  private resolveTemplatedOutputPath;
112
116
  private relocateOutputFile;
117
+ private hasJavaScriptExtension;
118
+ private findOutputMatchForEntrypoint;
113
119
  private normalizeBunOutputs;
114
120
  private rewriteAliasedRuntimeSpecifiers;
115
121
  build(options: BuildOptions): Promise<BuildResult>;
@@ -234,7 +234,52 @@ class BunBuildAdapter {
234
234
  }
235
235
  return path.join(directory, matches[0]);
236
236
  }
237
- resolveTemplatedOutputPath(options, entrypointPath, concreteOutputPath) {
237
+ normalizePathForMatch(filePath) {
238
+ return path.normalize(filePath).split(path.sep).join("/");
239
+ }
240
+ normalizeOutputPathForMatch(outputPath, templatePath) {
241
+ const normalizedOutputPath = path.normalize(outputPath);
242
+ const templateExtension = path.extname(templatePath);
243
+ if (!templateExtension) {
244
+ return normalizedOutputPath;
245
+ }
246
+ if (templateExtension === ".js") {
247
+ if (this.hasJavaScriptExtension(normalizedOutputPath)) {
248
+ return path.normalize(normalizedOutputPath.replace(/\.(?:[cm]?js)$/u, ".js"));
249
+ }
250
+ return path.normalize(`${normalizedOutputPath}.js`);
251
+ }
252
+ if (normalizedOutputPath.endsWith(templateExtension)) {
253
+ return normalizedOutputPath;
254
+ }
255
+ return path.normalize(`${normalizedOutputPath}${templateExtension}`);
256
+ }
257
+ extractTemplateHashTokens(templatePath, candidatePath) {
258
+ const normalizedTemplatePath = this.normalizePathForMatch(templatePath);
259
+ const normalizedCandidatePath = this.normalizePathForMatch(
260
+ this.normalizeOutputPathForMatch(candidatePath, templatePath)
261
+ );
262
+ const matcher = new RegExp(
263
+ `^${this.escapeRegExp(normalizedTemplatePath).replace(/\\\[hash\\\]/g, "([^/]+)")}$`
264
+ );
265
+ const match = normalizedCandidatePath.match(matcher);
266
+ if (!match) {
267
+ return void 0;
268
+ }
269
+ return match.slice(1);
270
+ }
271
+ applyTemplateHashTokens(templatePath, hashTokens) {
272
+ const hashTokenCount = templatePath.match(/\[hash\]/g)?.length ?? 0;
273
+ if (hashTokenCount !== hashTokens.length) {
274
+ return void 0;
275
+ }
276
+ if (hashTokenCount === 0) {
277
+ return templatePath;
278
+ }
279
+ let hashTokenIndex = 0;
280
+ return templatePath.replace(/\[hash\]/g, () => hashTokens[hashTokenIndex++] ?? "");
281
+ }
282
+ resolveTemplatedOutputPath(options, entrypointPath) {
238
283
  if (!options.outdir) {
239
284
  return void 0;
240
285
  }
@@ -252,18 +297,6 @@ class BunBuildAdapter {
252
297
  resolvedPath += outputExtension;
253
298
  }
254
299
  resolvedPath = resolvedPath.replace(/^\.\//, "");
255
- if (resolvedPath.includes("[hash]")) {
256
- if (!concreteOutputPath) {
257
- return path.join(outdir, resolvedPath);
258
- }
259
- const concreteRelativePath = path.relative(outdir, concreteOutputPath).split(path.sep).join("/");
260
- const matcher = new RegExp(`^${this.escapeRegExp(resolvedPath).replace(/\\\[hash\\\]/g, "(.+)")}$`);
261
- const match = concreteRelativePath.match(matcher);
262
- if (!match?.[1]) {
263
- return concreteOutputPath;
264
- }
265
- resolvedPath = resolvedPath.replaceAll("[hash]", match[1]);
266
- }
267
300
  return path.join(outdir, resolvedPath);
268
301
  }
269
302
  relocateOutputFile(currentPath, targetPath) {
@@ -275,37 +308,77 @@ class BunBuildAdapter {
275
308
  fs.renameSync(currentPath, targetPath);
276
309
  return targetPath;
277
310
  }
311
+ hasJavaScriptExtension(outputPath) {
312
+ return /\.(?:[cm]?js)$/u.test(outputPath);
313
+ }
314
+ findOutputMatchForEntrypoint(options, entrypointPath, outputs, usedOutputIndexes) {
315
+ const expectedOutputPath = this.resolveTemplatedOutputPath(options, entrypointPath);
316
+ if (!expectedOutputPath) {
317
+ return void 0;
318
+ }
319
+ const expectedMatchPaths = [expectedOutputPath];
320
+ if (options.outbase) {
321
+ const bunRootRelativeOutputPath = this.resolveTemplatedOutputPath(
322
+ { ...options, outbase: void 0 },
323
+ entrypointPath
324
+ );
325
+ if (bunRootRelativeOutputPath && bunRootRelativeOutputPath !== expectedOutputPath) {
326
+ expectedMatchPaths.push(bunRootRelativeOutputPath);
327
+ }
328
+ }
329
+ for (const [outputIndex, output] of outputs.entries()) {
330
+ if (usedOutputIndexes.has(outputIndex)) {
331
+ continue;
332
+ }
333
+ for (const matchPath of expectedMatchPaths) {
334
+ const hashTokens = this.extractTemplateHashTokens(matchPath, output.concretePath);
335
+ if (!hashTokens) {
336
+ continue;
337
+ }
338
+ const targetPath = this.applyTemplateHashTokens(expectedOutputPath, hashTokens);
339
+ if (!targetPath) {
340
+ continue;
341
+ }
342
+ usedOutputIndexes.add(outputIndex);
343
+ return { outputIndex, targetPath };
344
+ }
345
+ }
346
+ return void 0;
347
+ }
278
348
  normalizeBunOutputs(result, options) {
279
349
  if (!result.success || result.outputs.length === 0) {
280
350
  return result;
281
351
  }
282
- const normalizedOutputs = [...result.outputs];
283
- const canMapEntrypointsByIndex = options.entrypoints.length === normalizedOutputs.length;
284
- if (canMapEntrypointsByIndex) {
285
- for (const [index, entrypointPath] of options.entrypoints.entries()) {
286
- const concreteOutputPath = this.resolveConcreteOutputPath(normalizedOutputs[index].path);
287
- const expectedOutputPath = this.resolveTemplatedOutputPath(options, entrypointPath, concreteOutputPath);
288
- if (!expectedOutputPath) {
289
- continue;
290
- }
291
- normalizedOutputs[index] = {
292
- path: this.relocateOutputFile(
293
- concreteOutputPath ?? normalizedOutputs[index].path,
294
- expectedOutputPath
295
- )
296
- };
352
+ const normalizedOutputs = result.outputs.map((output) => ({
353
+ concretePath: this.resolveConcreteOutputPath(output.path) ?? output.path
354
+ }));
355
+ const matchedTargetsByIndex = /* @__PURE__ */ new Map();
356
+ const usedOutputIndexes = /* @__PURE__ */ new Set();
357
+ for (const entrypointPath of options.entrypoints) {
358
+ const matchedOutput = this.findOutputMatchForEntrypoint(
359
+ options,
360
+ entrypointPath,
361
+ normalizedOutputs,
362
+ usedOutputIndexes
363
+ );
364
+ if (matchedOutput) {
365
+ matchedTargetsByIndex.set(matchedOutput.outputIndex, matchedOutput.targetPath);
297
366
  }
298
- return {
299
- ...result,
300
- outputs: normalizedOutputs
301
- };
302
367
  }
303
368
  return {
304
369
  ...result,
305
- outputs: normalizedOutputs.map((output) => {
306
- const concreteOutputPath = this.resolveConcreteOutputPath(output.path) ?? output.path;
307
- if (path.extname(concreteOutputPath) !== "") {
308
- return output;
370
+ outputs: normalizedOutputs.map((output, index) => {
371
+ const expectedOutputPath = matchedTargetsByIndex.get(index);
372
+ const concreteOutputPath = output.concretePath;
373
+ if (expectedOutputPath) {
374
+ return {
375
+ path: this.relocateOutputFile(concreteOutputPath, expectedOutputPath)
376
+ };
377
+ }
378
+ if (this.hasJavaScriptExtension(concreteOutputPath)) {
379
+ return {
380
+ path: concreteOutputPath
381
+ };
309
382
  }
310
383
  const normalizedPath = `${concreteOutputPath}.js`;
311
384
  return {
@@ -165,6 +165,22 @@ export declare abstract class IntegrationPlugin<C = EcoPagesElement> {
165
165
  * Returns processed global assets resolved during `setup()`.
166
166
  */
167
167
  getResolvedIntegrationDependencies(): ProcessedAsset[];
168
+ /**
169
+ * Creates the shared renderer options owned by core lifecycle setup.
170
+ */
171
+ protected createRendererOptions(options?: {
172
+ rendererModules?: unknown;
173
+ }): {
174
+ appConfig: EcoPagesAppConfig;
175
+ assetProcessingService: AssetProcessingService;
176
+ resolvedIntegrationDependencies: ProcessedAsset[];
177
+ rendererModules: unknown;
178
+ runtimeOrigin: string;
179
+ };
180
+ /**
181
+ * Attaches runtime-only services after a renderer instance has been created.
182
+ */
183
+ protected attachRendererRuntimeServices<T extends IntegrationRenderer<C>>(renderer: T): T;
168
184
  /**
169
185
  * Instantiates the integration renderer with app-owned services.
170
186
  *
@@ -172,6 +188,7 @@ export declare abstract class IntegrationPlugin<C = EcoPagesElement> {
172
188
  * Renderers are cheap runtime objects. They receive the finalized app config,
173
189
  * a fresh asset-processing service, integration-global processed assets, and
174
190
  * any renderer module context supplied by the active runtime.
191
+ renderer.name ||= this.name;
175
192
  */
176
193
  initializeRenderer(options?: {
177
194
  rendererModules?: unknown;
@@ -112,14 +112,9 @@ class IntegrationPlugin {
112
112
  return this.resolvedIntegrationDependencies;
113
113
  }
114
114
  /**
115
- * Instantiates the integration renderer with app-owned services.
116
- *
117
- * @remarks
118
- * Renderers are cheap runtime objects. They receive the finalized app config,
119
- * a fresh asset-processing service, integration-global processed assets, and
120
- * any renderer module context supplied by the active runtime.
115
+ * Creates the shared renderer options owned by core lifecycle setup.
121
116
  */
122
- initializeRenderer(options) {
117
+ createRendererOptions(options) {
123
118
  if (!this.appConfig) {
124
119
  throw new Error(INTEGRATION_PLUGIN_ERRORS.NOT_INITIALIZED_WITH_APP_CONFIG);
125
120
  }
@@ -127,19 +122,40 @@ class IntegrationPlugin {
127
122
  if (this.hmrManager) {
128
123
  assetProcessingService.setHmrManager(this.hmrManager);
129
124
  }
130
- const renderer = new this.renderer({
125
+ return {
131
126
  appConfig: this.appConfig,
132
127
  assetProcessingService,
133
128
  resolvedIntegrationDependencies: this.resolvedIntegrationDependencies,
134
129
  rendererModules: options?.rendererModules,
135
130
  runtimeOrigin: this.runtimeOrigin
136
- });
137
- renderer.name ||= this.name;
131
+ };
132
+ }
133
+ /**
134
+ * Attaches runtime-only services after a renderer instance has been created.
135
+ */
136
+ attachRendererRuntimeServices(renderer) {
137
+ if (typeof renderer.name !== "string" || renderer.name.length === 0) {
138
+ renderer.name = this.name;
139
+ }
138
140
  if (this.hmrManager) {
139
141
  renderer.setHmrManager(this.hmrManager);
140
142
  }
141
143
  return renderer;
142
144
  }
145
+ /**
146
+ * Instantiates the integration renderer with app-owned services.
147
+ *
148
+ * @remarks
149
+ * Renderers are cheap runtime objects. They receive the finalized app config,
150
+ * a fresh asset-processing service, integration-global processed assets, and
151
+ * any renderer module context supplied by the active runtime.
152
+ renderer.name ||= this.name;
153
+ */
154
+ initializeRenderer(options) {
155
+ const renderer = new this.renderer(this.createRendererOptions(options));
156
+ renderer.name ||= this.name;
157
+ return this.attachRendererRuntimeServices(renderer);
158
+ }
143
159
  /**
144
160
  * Prepares build-facing contributions before the app build manifest is sealed.
145
161
  *
@@ -18,6 +18,17 @@ type BoundaryRenderDecisionInput = {
18
18
  currentIntegration: string;
19
19
  targetIntegration?: string;
20
20
  };
21
+ /**
22
+ * Controls how one route module is loaded outside the normal render path.
23
+ *
24
+ * Request-time metadata inspection and static-generation probes use these
25
+ * options to isolate their module identity from the main render cache while
26
+ * still going through the owning integration's import setup.
27
+ */
28
+ export type RouteModuleLoadOptions = {
29
+ bypassCache?: boolean;
30
+ cacheScope?: string;
31
+ };
21
32
  /**
22
33
  * Context for renderToResponse method.
23
34
  */
@@ -47,6 +58,15 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
47
58
  protected renderExecutionService: RenderExecutionService;
48
59
  protected readonly queuedBoundaryRuntimeService: QueuedBoundaryRuntimeService;
49
60
  protected DOC_TYPE: string;
61
+ /**
62
+ * Loads one route module through the owning renderer's import path.
63
+ *
64
+ * Request-time infrastructure may need page metadata such as cache strategy or
65
+ * middleware before full rendering starts. Exposing this narrow entrypoint lets
66
+ * those callers reuse integration-specific import setup instead of bypassing it
67
+ * with raw transpiler access.
68
+ */
69
+ loadPageModule(file: string, options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
50
70
  /**
51
71
  * Reads the execution-scoped foreign renderer cache from one boundary input.
52
72
  *
@@ -287,7 +307,7 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
287
307
  */
288
308
  protected getMetadataProps(getMetadata: GetMetadata | undefined, { props, params, query }: GetMetadataContext): Promise<PageMetadataProps>;
289
309
  protected usesIntegrationPageImporter(_file: string): boolean;
290
- protected importIntegrationPageFile(_file: string): Promise<EcoPageFile>;
310
+ protected importIntegrationPageFile(_file: string, _options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
291
311
  protected normalizeImportedPageFile<TPageModule extends EcoPageFile>(_file: string, pageModule: TPageModule): TPageModule;
292
312
  /**
293
313
  * Imports the page file from the specified path.
@@ -296,7 +316,7 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
296
316
  * @param file - The file path to import.
297
317
  * @returns The imported module.
298
318
  */
299
- protected importPageFile(file: string): Promise<EcoPageFile>;
319
+ protected importPageFile(file: string, options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
300
320
  /**
301
321
  * Resolves the dependency path based on the component directory.
302
322
  * It combines the component directory with the provided path URL.
@@ -56,6 +56,17 @@ class IntegrationRenderer {
56
56
  renderExecutionService;
57
57
  queuedBoundaryRuntimeService = new QueuedBoundaryRuntimeService();
58
58
  DOC_TYPE = "<!DOCTYPE html>";
59
+ /**
60
+ * Loads one route module through the owning renderer's import path.
61
+ *
62
+ * Request-time infrastructure may need page metadata such as cache strategy or
63
+ * middleware before full rendering starts. Exposing this narrow entrypoint lets
64
+ * those callers reuse integration-specific import setup instead of bypassing it
65
+ * with raw transpiler access.
66
+ */
67
+ async loadPageModule(file, options) {
68
+ return this.importPageFile(file, options);
69
+ }
59
70
  /**
60
71
  * Reads the execution-scoped foreign renderer cache from one boundary input.
61
72
  *
@@ -531,7 +542,7 @@ class IntegrationRenderer {
531
542
  usesIntegrationPageImporter(_file) {
532
543
  return false;
533
544
  }
534
- async importIntegrationPageFile(_file) {
545
+ async importIntegrationPageFile(_file, _options) {
535
546
  invariant(false, "Integration page importer must be implemented when enabled");
536
547
  }
537
548
  normalizeImportedPageFile(_file, pageModule) {
@@ -544,9 +555,15 @@ class IntegrationRenderer {
544
555
  * @param file - The file path to import.
545
556
  * @returns The imported module.
546
557
  */
547
- async importPageFile(file) {
548
- const bypassCache = typeof Bun !== "undefined" && process.env.NODE_ENV === "development";
549
- const pageModule = this.usesIntegrationPageImporter(file) ? await this.importIntegrationPageFile(file) : await this.pageModuleLoaderService.importPageFile(file, { bypassCache });
558
+ async importPageFile(file, options) {
559
+ const bypassCache = options?.bypassCache ?? (typeof Bun !== "undefined" && process.env.NODE_ENV === "development");
560
+ const pageModule = this.usesIntegrationPageImporter(file) ? await this.importIntegrationPageFile(file, {
561
+ bypassCache,
562
+ cacheScope: options?.cacheScope
563
+ }) : await this.pageModuleLoaderService.importPageFile(file, {
564
+ bypassCache,
565
+ cacheScope: options?.cacheScope
566
+ });
550
567
  return this.normalizeImportedPageFile(file, pageModule);
551
568
  }
552
569
  /**
@@ -775,9 +792,11 @@ class IntegrationRenderer {
775
792
  if (!boundaryOwner) {
776
793
  return void 0;
777
794
  }
778
- return await this.getIntegrationRendererForName(boundaryOwner, rendererCache).renderComponentBoundary(
779
- this.withBoundaryRendererCache(input, rendererCache)
780
- );
795
+ const owningRenderer = this.getIntegrationRendererForName(boundaryOwner, rendererCache);
796
+ if (owningRenderer === this || owningRenderer.name === this.name) {
797
+ return void 0;
798
+ }
799
+ return await owningRenderer.renderComponentBoundary(this.withBoundaryRendererCache(input, rendererCache));
781
800
  }
782
801
  /**
783
802
  * Renders one component under this integration's boundary runtime and resolves
@@ -115,6 +115,9 @@ class RenderPreparationService {
115
115
  collectResolvedTriggers(components, seen = /* @__PURE__ */ new Set()) {
116
116
  const triggers = [];
117
117
  for (const comp of components) {
118
+ if (!comp) {
119
+ continue;
120
+ }
118
121
  const ecoComp = comp;
119
122
  if (seen.has(ecoComp)) {
120
123
  continue;
@@ -165,6 +168,9 @@ class RenderPreparationService {
165
168
  collectIntegrationNames(components, seen = /* @__PURE__ */ new Set()) {
166
169
  const integrationNames = /* @__PURE__ */ new Set();
167
170
  for (const comp of components) {
171
+ if (!comp) {
172
+ continue;
173
+ }
168
174
  const ecoComp = comp;
169
175
  if (seen.has(ecoComp)) {
170
176
  continue;
@@ -29,6 +29,7 @@ export declare class PageModuleLoaderService {
29
29
  */
30
30
  importPageFile(file: string, options?: {
31
31
  bypassCache?: boolean;
32
+ cacheScope?: string;
32
33
  }): Promise<EcoPageFile>;
33
34
  /**
34
35
  * Executes the page's static-props hook with Ecopages runtime context.
@@ -30,6 +30,7 @@ class PageModuleLoaderService {
30
30
  rootDir: this.appConfig.rootDir,
31
31
  outdir: `${resolveInternalExecutionDir(this.appConfig)}/.server-modules`,
32
32
  bypassCache: options?.bypassCache,
33
+ cacheScope: options?.cacheScope,
33
34
  transpileErrorMessage: (details) => `Error transpiling page file: ${details}`,
34
35
  noOutputMessage: (targetFilePath) => `No transpiled output generated for page: ${targetFilePath}`
35
36
  });
@@ -1,7 +1,7 @@
1
1
  import type { EcoPagesAppConfig } from '../types/internal-types.js';
2
2
  import type { IntegrationPlugin } from '../plugins/integration-plugin.js';
3
- import type { RouteRenderResult, RouteRendererOptions } from '../types/public-types.js';
4
- import type { IntegrationRenderer } from './orchestration/integration-renderer.js';
3
+ import type { EcoPageFile, RouteRenderResult, RouteRendererOptions } from '../types/public-types.js';
4
+ import type { IntegrationRenderer, RouteModuleLoadOptions } from './orchestration/integration-renderer.js';
5
5
  /**
6
6
  * Thin wrapper around one initialized integration renderer.
7
7
  *
@@ -20,6 +20,10 @@ export declare class RouteRenderer {
20
20
  * Executes the render pipeline for one matched route.
21
21
  */
22
22
  createRoute(options: RouteRendererOptions): Promise<RouteRenderResult>;
23
+ /**
24
+ * Loads the route module through the owning integration renderer.
25
+ */
26
+ loadPageModule(filePath: string, options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
23
27
  }
24
28
  /**
25
29
  * Selects and caches integration renderers for route files and explicit views.
@@ -14,6 +14,12 @@ class RouteRenderer {
14
14
  async createRoute(options) {
15
15
  return this.renderer.execute(options);
16
16
  }
17
+ /**
18
+ * Loads the route module through the owning integration renderer.
19
+ */
20
+ async loadPageModule(filePath, options) {
21
+ return this.renderer.loadPageModule(filePath, options);
22
+ }
17
23
  }
18
24
  class RouteRendererFactory {
19
25
  appConfig;
@@ -1,5 +1,6 @@
1
1
  import { PageModuleImportService, type PageModuleImportDependencies, type PageModuleImportOptions } from './page-module-import.service.js';
2
2
  import type { BuildExecutor } from '../../build/build-adapter.js';
3
+ import type { EcoBuildPlugin } from '../../build/build-types.js';
3
4
  export type AppModuleLoaderOwner = 'bun' | 'host';
4
5
  export interface AppModuleLoader {
5
6
  readonly owner: AppModuleLoaderOwner;
@@ -9,6 +10,7 @@ export interface AppModuleLoader {
9
10
  export type AppModuleLoaderOptions = {
10
11
  dependencies?: Partial<PageModuleImportDependencies>;
11
12
  getBuildExecutor?: () => BuildExecutor | undefined;
13
+ getDefaultPlugins?: () => EcoBuildPlugin[];
12
14
  getOwner?: () => AppModuleLoaderOwner;
13
15
  getInvalidationVersion?: () => number | undefined;
14
16
  pageModuleImportService?: PageModuleImportService;
@@ -16,6 +18,7 @@ export type AppModuleLoaderOptions = {
16
18
  export declare class RuntimeAppModuleLoader implements AppModuleLoader {
17
19
  private readonly pageModuleImportService;
18
20
  private readonly getBuildExecutorValue;
21
+ private readonly getDefaultPluginsValue;
19
22
  private readonly getOwnerValue;
20
23
  private readonly getInvalidationVersionValue;
21
24
  constructor(options?: AppModuleLoaderOptions);
@@ -4,11 +4,13 @@ import {
4
4
  class RuntimeAppModuleLoader {
5
5
  pageModuleImportService;
6
6
  getBuildExecutorValue;
7
+ getDefaultPluginsValue;
7
8
  getOwnerValue;
8
9
  getInvalidationVersionValue;
9
10
  constructor(options = {}) {
10
11
  this.pageModuleImportService = options.pageModuleImportService ?? new PageModuleImportService(options.dependencies);
11
12
  this.getBuildExecutorValue = options.getBuildExecutor ?? (() => void 0);
13
+ this.getDefaultPluginsValue = options.getDefaultPlugins ?? (() => []);
12
14
  this.getOwnerValue = options.getOwner ?? (() => "bun");
13
15
  this.getInvalidationVersionValue = options.getInvalidationVersion ?? (() => void 0);
14
16
  }
@@ -16,8 +18,10 @@ class RuntimeAppModuleLoader {
16
18
  return this.getOwnerValue();
17
19
  }
18
20
  async importModule(options) {
21
+ const mergedPlugins = [...this.getDefaultPluginsValue(), ...options.plugins ?? []];
19
22
  return await this.pageModuleImportService.importModule({
20
23
  ...options,
24
+ ...mergedPlugins.length > 0 ? { plugins: mergedPlugins } : {},
21
25
  buildExecutor: options.buildExecutor ?? this.getBuildExecutorValue(),
22
26
  invalidationVersion: options.invalidationVersion ?? this.getInvalidationVersionValue()
23
27
  });
@@ -49,6 +49,7 @@ function createAppModuleLoader(appConfig) {
49
49
  getHostModuleLoader: () => getAppHostModuleLoader(appConfig)
50
50
  },
51
51
  getBuildExecutor: () => getAppBuildExecutor(appConfig),
52
+ getDefaultPlugins: typeof Bun === "undefined" && appConfig.rootDir ? () => [createAppNodeBootstrapPlugin(appConfig)] : void 0,
52
53
  getOwner: () => getAppModuleLoaderOwner(appConfig),
53
54
  getInvalidationVersion: () => invalidationService.getServerModuleInvalidationVersion()
54
55
  });
@@ -77,8 +78,7 @@ function createAppServerModuleTranspiler(appConfig) {
77
78
  canLoadSourceModuleFromHost: (filePath) => shouldAppUseHostModuleLoader(appConfig, filePath),
78
79
  getInvalidationVersion: () => invalidationService.getServerModuleInvalidationVersion(),
79
80
  invalidateModules: (changedFiles) => invalidationService.invalidateServerModules(changedFiles),
80
- pageModuleImportService: getAppModuleLoader(appConfig),
81
- getDefaultPlugins: typeof Bun === "undefined" && appConfig.rootDir ? () => [createAppNodeBootstrapPlugin(appConfig)] : void 0
81
+ pageModuleImportService: getAppModuleLoader(appConfig)
82
82
  });
83
83
  }
84
84
  function getAppServerModuleTranspiler(appConfig) {
@@ -6,7 +6,15 @@ import type { EcoPagesAppConfig } from '../../types/internal-types.js';
6
6
  */
7
7
  export declare function getAppRuntimeNodeModulesDir(appConfig: Pick<EcoPagesAppConfig, 'rootDir' | 'workDir' | 'absolutePaths'>): string;
8
8
  export interface NodeBootstrapResolutionOptions {
9
+ /**
10
+ * App root used as the fallback package boundary when an importer does not
11
+ * live under a more specific package.json.
12
+ */
9
13
  projectDir: string;
14
+ /**
15
+ * Runtime-local node_modules directory that receives symlinks to resolved
16
+ * package roots so transpiled Node imports share one package graph.
17
+ */
10
18
  runtimeNodeModulesDir: string;
11
19
  preserveImportMetaPaths?: string[];
12
20
  }
@@ -16,7 +24,22 @@ export interface NodeBootstrapResolutionOptions {
16
24
  */
17
25
  export declare function getNodeUnsupportedBuiltinError(specifier: string, importer?: string): string;
18
26
  export declare function resolveNodeBootstrapDependency(args: Pick<EcoBuildOnResolveArgs, 'path' | 'importer'>, options: NodeBootstrapResolutionOptions): EcoBuildOnResolveResult | undefined;
27
+ /**
28
+ * Creates the Node bootstrap plugin used by app-owned server module loads.
29
+ *
30
+ * The resolver anchors third-party imports to the nearest package boundary for
31
+ * the importing file, then mirrors the resolved package root into the runtime
32
+ * node_modules directory. That keeps transpiled Node execution aligned with the
33
+ * package graph each source file was authored against.
34
+ */
19
35
  export declare function createNodeBootstrapPlugin(options: NodeBootstrapResolutionOptions): EcoBuildPlugin;
36
+ /**
37
+ * Creates the default Node bootstrap plugin for one Ecopages app runtime.
38
+ *
39
+ * This binds the shared resolution policy to the app's internal execution
40
+ * directory so transpiled server modules can externalize packages into one
41
+ * stable runtime node_modules graph.
42
+ */
20
43
  export declare function createAppNodeBootstrapPlugin(appConfig: Pick<EcoPagesAppConfig, 'rootDir' | 'workDir' | 'absolutePaths'>, options?: {
21
44
  preserveImportMetaPaths?: string[];
22
45
  }): EcoBuildPlugin;
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { existsSync, mkdirSync, readFileSync, symlinkSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { resolveInternalExecutionDir } from "../../utils/resolve-work-dir.js";
@@ -32,7 +32,10 @@ function ensureRuntimePackageLink(nodeModulesDir, specifier, resolvedPath) {
32
32
  const packageRoot = findPackageRoot(resolvedPath);
33
33
  const linkPath = path.join(nodeModulesDir, packageName);
34
34
  if (existsSync(linkPath)) {
35
- return;
35
+ if (realpathSync(linkPath) === realpathSync(packageRoot)) {
36
+ return;
37
+ }
38
+ rmSync(linkPath, { recursive: true, force: true });
36
39
  }
37
40
  mkdirSync(path.dirname(linkPath), { recursive: true });
38
41
  symlinkSync(packageRoot, linkPath, "dir");
@@ -40,9 +43,6 @@ function ensureRuntimePackageLink(nodeModulesDir, specifier, resolvedPath) {
40
43
  function getNodeUnsupportedBuiltinError(specifier, importer) {
41
44
  return `Node bootstrap transpilation does not support Bun builtin specifier ${JSON.stringify(specifier)}${importer ? ` imported from ${importer}` : ""}.`;
42
45
  }
43
- function shouldResolveFromImporter(importer) {
44
- return Boolean(importer && importer.includes(`${path.sep}node_modules${path.sep}`));
45
- }
46
46
  function resolveSpecifier(specifier, parentPath) {
47
47
  try {
48
48
  return createRequire(parentPath).resolve(specifier);
@@ -53,6 +53,23 @@ function resolveSpecifier(specifier, parentPath) {
53
53
  function resolveFromCore(specifier) {
54
54
  return createRequire(import.meta.url).resolve(specifier);
55
55
  }
56
+ function findResolutionParent(importer, projectDir) {
57
+ if (!importer || !path.isAbsolute(importer)) {
58
+ return path.join(projectDir, "package.json");
59
+ }
60
+ let currentPath = path.dirname(importer);
61
+ while (true) {
62
+ const packageJsonPath = path.join(currentPath, "package.json");
63
+ if (existsSync(packageJsonPath)) {
64
+ return packageJsonPath;
65
+ }
66
+ const parentPath = path.dirname(currentPath);
67
+ if (parentPath === currentPath) {
68
+ return path.join(projectDir, "package.json");
69
+ }
70
+ currentPath = parentPath;
71
+ }
72
+ }
56
73
  function getBootstrapBuildLoaderForPath(filePath) {
57
74
  switch (path.extname(filePath).toLowerCase()) {
58
75
  case ".ts":
@@ -98,7 +115,7 @@ function resolveNodeBootstrapDependency(args, options) {
98
115
  if (args.path.startsWith("./") || args.path.startsWith("../") || args.path.startsWith("@/") || args.path.startsWith("/") || args.path.startsWith("node:")) {
99
116
  return void 0;
100
117
  }
101
- const resolveParent = args.importer && path.isAbsolute(args.importer) && shouldResolveFromImporter(args.importer) ? args.importer : path.join(options.projectDir, "package.json");
118
+ const resolveParent = findResolutionParent(args.importer, options.projectDir);
102
119
  if (args.path.startsWith("@ecopages/")) {
103
120
  let resolvedPath2;
104
121
  try {
@@ -6,6 +6,7 @@ export interface PageModuleImportOptions {
6
6
  rootDir: string;
7
7
  outdir: string;
8
8
  bypassCache?: boolean;
9
+ cacheScope?: string;
9
10
  buildExecutor?: BuildExecutor;
10
11
  invalidationVersion?: number;
11
12
  splitting?: boolean;
@@ -51,7 +51,7 @@ class PageModuleImportService {
51
51
  const fileHash = this.dependencies.hashFile(filePath);
52
52
  const hostModuleLoader = typeof Bun === "undefined" && process.env.NODE_ENV === "development" && this.dependencies.canLoadSourceModuleFromHost(filePath) ? this.dependencies.getHostModuleLoader() : void 0;
53
53
  if (hostModuleLoader) {
54
- const sourceModuleUrl = createRuntimeModuleUrl(filePath, fileHash, invalidationVersion);
54
+ const sourceModuleUrl = createRuntimeModuleUrl(filePath, fileHash, invalidationVersion, options.cacheScope);
55
55
  return await hostModuleLoader(sourceModuleUrl.href);
56
56
  }
57
57
  if (options.bypassCache) {
@@ -69,6 +69,7 @@ class PageModuleImportService {
69
69
  rootDir,
70
70
  splitting ?? "default",
71
71
  externalPackages ?? "default",
72
+ options.cacheScope ?? "default",
72
73
  fileHash,
73
74
  invalidationVersion
74
75
  ].join("::");
@@ -96,11 +97,12 @@ class PageModuleImportService {
96
97
  invalidationVersion = this.developmentInvalidationVersion,
97
98
  splitting,
98
99
  externalPackages,
100
+ cacheScope,
99
101
  transpileErrorMessage = (details) => `Error transpiling page module: ${details}`,
100
102
  noOutputMessage = (targetFilePath) => `No transpiled output generated for page module: ${targetFilePath}`,
101
103
  fileHash
102
104
  } = options;
103
- const sourceModuleUrl = createRuntimeModuleUrl(filePath, fileHash, invalidationVersion);
105
+ const sourceModuleUrl = createRuntimeModuleUrl(filePath, fileHash, invalidationVersion, cacheScope);
104
106
  if (typeof Bun !== "undefined") {
105
107
  return await import(
106
108
  /* @vite-ignore */
@@ -108,7 +110,8 @@ class PageModuleImportService {
108
110
  );
109
111
  }
110
112
  const fileBaseName = path.basename(filePath, path.extname(filePath));
111
- const outputFileName = `${fileBaseName}-${fileHash}.js`;
113
+ const cacheScopeSuffix = cacheScope ? `-${sanitizeCacheScope(cacheScope)}` : "";
114
+ const outputFileName = `${fileBaseName}-${fileHash}${cacheScopeSuffix}.js`;
112
115
  const buildResult = await this.dependencies.buildModule(
113
116
  {
114
117
  entrypoints: [filePath],
@@ -136,8 +139,11 @@ class PageModuleImportService {
136
139
  throw new Error(noOutputMessage(filePath));
137
140
  }
138
141
  const compiledOutputUrl = pathToFileURL(compiledOutput);
139
- if (process.env.NODE_ENV === "development") {
140
- compiledOutputUrl.searchParams.set("update", `${fileHash}-${invalidationVersion}`);
142
+ if (process.env.NODE_ENV === "development" || cacheScope) {
143
+ compiledOutputUrl.searchParams.set(
144
+ "update",
145
+ [fileHash, invalidationVersion, cacheScope ? sanitizeCacheScope(cacheScope) : void 0].filter((value) => value !== void 0).join("-")
146
+ );
141
147
  }
142
148
  return await import(
143
149
  /* @vite-ignore */
@@ -145,13 +151,19 @@ class PageModuleImportService {
145
151
  );
146
152
  }
147
153
  }
148
- function createRuntimeModuleUrl(filePath, fileHash, invalidationVersion) {
154
+ function createRuntimeModuleUrl(filePath, fileHash, invalidationVersion, cacheScope) {
149
155
  const moduleUrl = pathToFileURL(filePath);
150
- if (process.env.NODE_ENV === "development") {
151
- moduleUrl.searchParams.set("update", `${fileHash}-${invalidationVersion}`);
156
+ if (process.env.NODE_ENV === "development" || cacheScope) {
157
+ moduleUrl.searchParams.set(
158
+ "update",
159
+ [fileHash, invalidationVersion, cacheScope ? sanitizeCacheScope(cacheScope) : void 0].filter((value) => value !== void 0).join("-")
160
+ );
152
161
  }
153
162
  return moduleUrl;
154
163
  }
164
+ function sanitizeCacheScope(cacheScope) {
165
+ return cacheScope.replace(/[^a-zA-Z0-9_-]+/g, "-");
166
+ }
155
167
  function supportsHostSourceModuleLoading(filePath) {
156
168
  const extension = path.extname(filePath);
157
169
  return extension === ".js" || extension === ".jsx" || extension === ".ts" || extension === ".tsx" || extension === ".mjs" || extension === ".mts" || extension === ".cjs" || extension === ".cts";
@@ -20,17 +20,12 @@ export declare const STATIC_SITE_GENERATOR_ERRORS: {
20
20
  */
21
21
  export declare class StaticSiteGenerator {
22
22
  appConfig: EcoPagesAppConfig;
23
- private serverModuleTranspiler;
24
23
  /**
25
24
  * Creates the static-site generator for one app config.
26
25
  */
27
26
  constructor({ appConfig }: {
28
27
  appConfig: EcoPagesAppConfig;
29
28
  });
30
- /**
31
- * Returns the transpiler output directory used for static page-module probes.
32
- */
33
- private getStaticPageModuleOutdir;
34
29
  private getExportDir;
35
30
  /**
36
31
  * Logs the standardized warning emitted when a dynamic-cache page is skipped.
@@ -1,9 +1,7 @@
1
1
  import path from "node:path";
2
- import { DEFAULT_ECOPAGES_WORK_DIR } from "../config/constants.js";
3
2
  import { appLogger } from "../global/app-logger.js";
4
3
  import { fileSystem } from "@ecopages/file-system";
5
4
  import { PathUtils } from "../utils/path-utils.module.js";
6
- import { getAppServerModuleTranspiler } from "../services/module-loading/app-server-module-transpiler.service.js";
7
5
  const STATIC_SITE_GENERATOR_ERRORS = {
8
6
  ROUTE_RENDERER_FACTORY_REQUIRED: "RouteRendererFactory is required for render strategy",
9
7
  unsupportedBodyType: (bodyType) => `Unsupported body type for static generation: ${bodyType}`,
@@ -13,20 +11,11 @@ const STATIC_SITE_GENERATOR_ERRORS = {
13
11
  };
14
12
  class StaticSiteGenerator {
15
13
  appConfig;
16
- serverModuleTranspiler;
17
14
  /**
18
15
  * Creates the static-site generator for one app config.
19
16
  */
20
17
  constructor({ appConfig }) {
21
18
  this.appConfig = appConfig;
22
- this.serverModuleTranspiler = getAppServerModuleTranspiler(appConfig);
23
- }
24
- /**
25
- * Returns the transpiler output directory used for static page-module probes.
26
- */
27
- getStaticPageModuleOutdir() {
28
- const workDir = this.appConfig.absolutePaths?.workDir ?? path.join(this.appConfig.rootDir, this.appConfig.workDir ?? DEFAULT_ECOPAGES_WORK_DIR);
29
- return path.join(workDir, ".server-static-page-modules");
30
19
  }
31
20
  getExportDir() {
32
21
  return this.appConfig.absolutePaths?.distDir ?? path.join(this.appConfig.rootDir, this.appConfig.distDir);
@@ -44,13 +33,9 @@ class StaticSiteGenerator {
44
33
  * Determines whether one filesystem-discovered page should be excluded from
45
34
  * static generation.
46
35
  */
47
- async shouldSkipStaticPageFile(filePath) {
48
- const module = await this.serverModuleTranspiler.importModule({
49
- filePath,
50
- outdir: this.getStaticPageModuleOutdir(),
51
- externalPackages: false,
52
- transpileErrorMessage: (details) => `Error transpiling static page module: ${details}`,
53
- noOutputMessage: (targetFilePath) => `No transpiled output generated for static page module: ${targetFilePath}`
36
+ async shouldSkipStaticPageFile(filePath, routeRendererFactory) {
37
+ const module = await routeRendererFactory.createRenderer(filePath).loadPageModule(filePath, {
38
+ cacheScope: "static-page-probe"
54
39
  });
55
40
  if (module.default?.cache !== "dynamic") {
56
41
  return false;
@@ -146,9 +131,6 @@ class StaticSiteGenerator {
146
131
  for (const route of routes) {
147
132
  try {
148
133
  const { filePath, pathname: routePathname } = router.routes[route];
149
- if (await this.shouldSkipStaticPageFile(filePath)) {
150
- continue;
151
- }
152
134
  const ext = PathUtils.getEcoTemplateExtension(filePath);
153
135
  const integration = this.appConfig.integrations.find((plugin) => plugin.extensions.includes(ext));
154
136
  const strategy = integration?.staticBuildStep || "render";
@@ -165,6 +147,9 @@ class StaticSiteGenerator {
165
147
  if (!routeRendererFactory) {
166
148
  throw new Error(STATIC_SITE_GENERATOR_ERRORS.ROUTE_RENDERER_FACTORY_REQUIRED);
167
149
  }
150
+ if (await this.shouldSkipStaticPageFile(filePath, routeRendererFactory)) {
151
+ continue;
152
+ }
168
153
  let pathname2 = routePathname;
169
154
  const pathnameSegments2 = pathname2.split("/").filter(Boolean);
170
155
  if (pathname2 === "/") {