@ecopages/core 0.2.0-beta.38 → 0.2.0-beta.39

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 (108) hide show
  1. package/README.md +1 -0
  2. package/package.json +27 -3
  3. package/src/adapters/bun/server-adapter.js +1 -0
  4. package/src/adapters/node/server-adapter.js +1 -0
  5. package/src/adapters/shared/http/explicit-static-render-preparation.js +1 -1
  6. package/src/adapters/shared/http/explicit-static-route-matcher.js +1 -1
  7. package/src/adapters/shared/http/fs-server-response-matcher.js +15 -7
  8. package/src/adapters/shared/runtime/collect-dev-prewarm-plan.d.ts +11 -0
  9. package/src/adapters/shared/runtime/collect-dev-prewarm-plan.js +29 -0
  10. package/src/adapters/shared/runtime/dev-static-route-prewarm.d.ts +16 -0
  11. package/src/adapters/shared/runtime/dev-static-route-prewarm.js +66 -0
  12. package/src/adapters/shared/runtime/render-context.js +1 -1
  13. package/src/adapters/shared/runtime/server-adapter.d.ts +7 -1
  14. package/src/adapters/shared/runtime/server-adapter.js +64 -5
  15. package/src/build/README.md +1 -1
  16. package/src/build/browser/browser-runtime-plugin.js +2 -5
  17. package/src/build/cache/production-build-cache.d.ts +3 -2
  18. package/src/build/cache/production-build-cache.js +2 -2
  19. package/src/build/contracts/content-virtual-modules.d.ts +12 -0
  20. package/src/build/contracts/content-virtual-modules.js +29 -0
  21. package/src/build/rolldown/rolldown-adapter-helpers.js +1 -1
  22. package/src/build/runtime/build-request-policy.d.ts +3 -4
  23. package/src/build/runtime/build-request-policy.js +4 -4
  24. package/src/cache/index.d.ts +2 -1
  25. package/src/cache/index.js +2 -1
  26. package/src/cache/module-parse-cache.d.ts +5 -0
  27. package/src/cache/module-parse-cache.js +27 -3
  28. package/src/cache/module-transform-profiler.d.ts +8 -0
  29. package/src/cache/module-transform-profiler.js +8 -0
  30. package/src/client/view-transitions.d.ts +17 -1
  31. package/src/client/view-transitions.js +45 -1
  32. package/src/client/view-transitions.test.browser.d.ts +1 -0
  33. package/src/client/view-transitions.test.browser.js +56 -0
  34. package/src/config/config-builder.d.ts +16 -9
  35. package/src/config/config-builder.js +24 -14
  36. package/src/dev/transform-server/dev-transform-vendor-registry.d.ts +1 -0
  37. package/src/dev/transform-server/dev-transform-vendor-registry.js +15 -2
  38. package/src/diagnostics/request-pipeline-metrics.d.ts +38 -0
  39. package/src/diagnostics/request-pipeline-metrics.js +121 -0
  40. package/src/eco/component-identity.d.ts +13 -0
  41. package/src/eco/component-identity.js +18 -0
  42. package/src/eco/eco-declared-component.d.ts +3 -2
  43. package/src/eco/eco-declared-component.js +6 -4
  44. package/src/eco/eco.browser.js +2 -2
  45. package/src/eco/eco.js +7 -6
  46. package/src/eco/eco.types.d.ts +6 -5
  47. package/src/errors/http-error.d.ts +8 -0
  48. package/src/errors/http-error.js +16 -0
  49. package/src/errors/undeclared-component-dependency-error.d.ts +1 -1
  50. package/src/errors/undeclared-component-dependency-error.js +2 -2
  51. package/src/index.browser.d.ts +1 -0
  52. package/src/index.browser.js +1 -0
  53. package/src/index.d.ts +1 -0
  54. package/src/index.js +1 -0
  55. package/src/plugins/README.md +2 -1
  56. package/src/plugins/eco-component-meta-plugin.d.ts +6 -102
  57. package/src/plugins/eco-component-meta-plugin.js +110 -391
  58. package/src/plugins/processor.d.ts +10 -0
  59. package/src/plugins/processor.js +9 -0
  60. package/src/route-renderer/README.md +10 -8
  61. package/src/route-renderer/orchestration/document-shell/document-shell-render.service.js +1 -1
  62. package/src/route-renderer/orchestration/foreign-child/foreign-subtree-execution.service.js +1 -1
  63. package/src/route-renderer/orchestration/integration-renderer.d.ts +0 -8
  64. package/src/route-renderer/orchestration/integration-renderer.js +6 -19
  65. package/src/route-renderer/orchestration/ownership-graph/component-graph-collectors.js +5 -4
  66. package/src/route-renderer/orchestration/ownership-graph/component-graph.js +6 -4
  67. package/src/route-renderer/orchestration/ownership-graph/ownership-validation.service.d.ts +0 -1
  68. package/src/route-renderer/orchestration/ownership-graph/ownership-validation.service.js +10 -19
  69. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.d.ts +1 -0
  70. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.js +12 -0
  71. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.d.ts +9 -0
  72. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.js +7 -0
  73. package/src/route-renderer/orchestration/route-pipeline/route-render-orchestrator.js +65 -33
  74. package/src/route-renderer/page-loading/component-dependency-collection.js +3 -2
  75. package/src/route-renderer/page-loading/ecopages-virtual-imports.d.ts +0 -5
  76. package/src/route-renderer/page-loading/ecopages-virtual-imports.js +4 -10
  77. package/src/route-renderer/page-loading/file-scoped-dependency-components.d.ts +2 -2
  78. package/src/route-renderer/page-loading/file-scoped-dependency-components.js +12 -11
  79. package/src/route-renderer/page-loading/lazy-trigger-planning.js +2 -1
  80. package/src/route-renderer/page-loading/page-module-loader.d.ts +1 -1
  81. package/src/route-renderer/page-loading/page-module-loader.js +8 -3
  82. package/src/router/README.md +8 -0
  83. package/src/services/README.md +5 -0
  84. package/src/services/assets/asset-processing-service/asset-processing.service.d.ts +1 -0
  85. package/src/services/assets/asset-processing-service/asset-processing.service.js +22 -5
  86. package/src/services/cache/cache.types.d.ts +4 -2
  87. package/src/services/cache/html-page-cache-dependency-index.d.ts +34 -0
  88. package/src/services/cache/html-page-cache-dependency-index.js +88 -0
  89. package/src/services/cache/index.d.ts +2 -0
  90. package/src/services/cache/index.js +2 -0
  91. package/src/services/cache/page-cache-service.d.ts +31 -1
  92. package/src/services/cache/page-cache-service.js +79 -3
  93. package/src/services/cache/page-request-cache-coordinator.service.js +4 -0
  94. package/src/services/module-loading/README.md +9 -6
  95. package/src/services/module-loading/app-server-module-transpiler.service.js +0 -3
  96. package/src/services/module-loading/collection-server-module-build.service.d.ts +19 -0
  97. package/src/services/module-loading/collection-server-module-build.service.js +83 -0
  98. package/src/services/module-loading/page-module-import.service.d.ts +5 -11
  99. package/src/services/module-loading/page-module-import.service.js +56 -73
  100. package/src/services/module-loading/route-module-build-cache.store.js +6 -6
  101. package/src/services/module-loading/route-module-build-manifest.d.ts +8 -5
  102. package/src/services/module-loading/route-module-build-manifest.js +20 -32
  103. package/src/services/module-loading/server-module-transpiler.service.d.ts +0 -2
  104. package/src/services/module-loading/server-module-transpiler.service.js +0 -3
  105. package/src/static-site-generator/static-site-generator.js +2 -2
  106. package/src/types/internal-types.d.ts +4 -0
  107. package/src/types/public-types.d.ts +18 -18
  108. package/src/watchers/project-watcher.js +14 -0
@@ -1,12 +1,10 @@
1
- import { build, type BuildExecutor, type BuildResult } from '../../build/build-adapter.js';
1
+ import { build, type BuildExecutor, type BuildOptions, type BuildResult } from '../../build/build-adapter.js';
2
2
  import type { EcoBuildPlugin } from '../../build/contracts/build-types.js';
3
3
  import type { EcoPagesAppConfig } from '../../types/internal-types.js';
4
4
  import type { SourceModuleLoaderFactory } from './module-loading-types.js';
5
5
  interface PageModuleImportBaseOptions {
6
6
  filePath: string;
7
7
  bypassCache?: boolean;
8
- cacheScope?: string;
9
- invalidationVersion?: number;
10
8
  }
11
9
  /**
12
10
  * Options for imports that must pass through the Ecopages build pipeline.
@@ -21,6 +19,7 @@ export interface PageModuleBuildImportOptions extends PageModuleImportBaseOption
21
19
  buildExecutor?: BuildExecutor;
22
20
  splitting?: boolean;
23
21
  externalPackages?: boolean;
22
+ sourceTransforms?: BuildOptions['sourceTransforms'];
24
23
  jsx?: {
25
24
  development?: boolean;
26
25
  factory?: string;
@@ -64,20 +63,15 @@ export declare class PageModuleImportService {
64
63
  private readonly dependencies;
65
64
  private readonly dependencyHasher;
66
65
  private readonly importCache;
67
- private developmentInvalidationVersion;
66
+ private developmentImportGeneration;
68
67
  constructor(appConfig?: EcoPagesAppConfig, dependencies?: Partial<PageModuleImportDependencies>);
69
68
  /**
70
69
  * Clears the shared import cache used by framework-owned page-module loads.
71
70
  */
72
71
  clearImportCache(): void;
73
72
  /**
74
- * Invalidates all previously imported modules in development by clearing the
75
- * import cache and incrementing the invalidation version included in cache keys.
76
- *
77
- * This forces all modules to be reloaded on the next import, even if their
78
- * source content hasn't changed. This is necessary to ensure that changes to
79
- * non-content aspects of modules (e.g. dependencies, transpilation output)
80
- * are picked up during development.
73
+ * Clears in-memory import promises and dependency-hash memoization so the next
74
+ * import revalidates against current source and transform identity.
81
75
  */
82
76
  invalidateDevelopmentGraph(): void;
83
77
  /**
@@ -6,8 +6,8 @@ import { createServerBuildRequest } from '../../build/runtime/build-request-poli
6
6
  import { resolveBuildProfileOptions } from '../../build/runtime/build-profile-options.js';
7
7
  import { importPagesUnifiedGraphModule, isPagesUnifiedGraphPage, shouldBuildPagesUnifiedGraph, } from '../../build/cache/pages-unified-graph-build.js';
8
8
  import { recordPageModuleBuildInvocation } from '../../build/rolldown/rolldown-build-invocation-metrics.js';
9
- import { resolvePageModuleOutputFileName } from './route-module-build-cache.js';
10
- import { createPluginCacheKey, createJsxCacheKey } from '../../build/cache/cache-keys.js';
9
+ import { recordPageModuleLoad } from '../../diagnostics/request-pipeline-metrics.js';
10
+ import { createRouteModuleReuseIdentity, resolvePageModuleOutputFileName } from './route-module-build-manifest.js';
11
11
  import { getSharedRouteModuleBuildCache } from './route-module-build-cache-registry.js';
12
12
  import { RouteModuleDependencyHasher, resolveRouteModuleDependencyPaths, } from './route-module-dependency-hasher.js';
13
13
  import { supportsSourceModuleLoading } from './source-module-support.js';
@@ -28,7 +28,7 @@ export class PageModuleImportService {
28
28
  dependencies;
29
29
  dependencyHasher;
30
30
  importCache = new Map();
31
- developmentInvalidationVersion = 0;
31
+ developmentImportGeneration = 0;
32
32
  constructor(appConfig, dependencies) {
33
33
  this.appConfig = appConfig;
34
34
  this.dependencies = {
@@ -49,18 +49,13 @@ export class PageModuleImportService {
49
49
  this.importCache.clear();
50
50
  }
51
51
  /**
52
- * Invalidates all previously imported modules in development by clearing the
53
- * import cache and incrementing the invalidation version included in cache keys.
54
- *
55
- * This forces all modules to be reloaded on the next import, even if their
56
- * source content hasn't changed. This is necessary to ensure that changes to
57
- * non-content aspects of modules (e.g. dependencies, transpilation output)
58
- * are picked up during development.
52
+ * Clears in-memory import promises and dependency-hash memoization so the next
53
+ * import revalidates against current source and transform identity.
59
54
  */
60
55
  invalidateDevelopmentGraph() {
56
+ this.developmentImportGeneration += 1;
61
57
  this.clearImportCache();
62
58
  this.dependencyHasher.clearMemo();
63
- this.developmentInvalidationVersion += 1;
64
59
  }
65
60
  /**
66
61
  * Imports a page-like module from source.
@@ -76,8 +71,6 @@ export class PageModuleImportService {
76
71
  */
77
72
  async importModule(options) {
78
73
  const { filePath } = options;
79
- const invalidationVersion = options.invalidationVersion ?? this.developmentInvalidationVersion;
80
- const { externalPackages, splitting } = options;
81
74
  const fileHash = this.dependencies.hashFile(filePath);
82
75
  const hostModuleLoader = typeof Bun === 'undefined' &&
83
76
  process.env.NODE_ENV === 'development' &&
@@ -85,63 +78,58 @@ export class PageModuleImportService {
85
78
  ? this.dependencies.getHostModuleLoader()
86
79
  : undefined;
87
80
  if (hostModuleLoader) {
88
- const sourceModuleUrl = createRuntimeModuleUrl(filePath, fileHash, invalidationVersion, options.cacheScope);
89
- return (await hostModuleLoader(sourceModuleUrl.href));
81
+ const sourceModuleUrl = createRuntimeModuleUrl(filePath, fileHash, this.developmentImportGeneration);
82
+ const loadedModule = (await hostModuleLoader(sourceModuleUrl.href));
83
+ recordPageModuleLoad('host-loader');
84
+ return loadedModule;
90
85
  }
91
- if (options.bypassCache) {
92
- this.developmentInvalidationVersion += 1;
93
- return await this.loadModule({
86
+ if (!options.bypassCache) {
87
+ const runtime = typeof Bun !== 'undefined' ? 'bun' : 'node-build';
88
+ const cacheKey = [
89
+ runtime,
90
+ filePath,
91
+ createRouteModuleReuseIdentity(options, options.sourceTransforms),
92
+ fileHash,
93
+ ].join('::');
94
+ const cachedModule = this.importCache.get(cacheKey);
95
+ if (cachedModule) {
96
+ if (!cachedModule.dependencyHashes) {
97
+ const loadedModule = (await cachedModule.promise);
98
+ recordPageModuleLoad('import-cache-hit');
99
+ return loadedModule;
100
+ }
101
+ if (this.dependencyHasher.matchesStoredHashes(cachedModule.dependencyHashes, filePath, fileHash)) {
102
+ const loadedModule = (await cachedModule.promise);
103
+ recordPageModuleLoad('import-cache-hit');
104
+ return loadedModule;
105
+ }
106
+ this.importCache.delete(cacheKey);
107
+ }
108
+ const importPromise = this.loadModule({
94
109
  ...options,
95
- invalidationVersion: this.developmentInvalidationVersion,
96
110
  fileHash,
111
+ importCacheKey: cacheKey,
97
112
  });
98
- }
99
- const runtime = typeof Bun !== 'undefined' ? 'bun' : 'node-build';
100
- const cacheKey = [
101
- runtime,
102
- filePath,
103
- options.rootDir,
104
- path.resolve(options.outdir),
105
- splitting ?? 'default',
106
- externalPackages ?? 'default',
107
- options.cacheScope ?? 'default',
108
- createJsxCacheKey(options.jsx),
109
- createPluginCacheKey(options.plugins),
110
- fileHash,
111
- invalidationVersion,
112
- ].join('::');
113
- const cachedModule = this.importCache.get(cacheKey);
114
- if (cachedModule) {
115
- if (!cachedModule.dependencyHashes) {
116
- return (await cachedModule.promise);
113
+ this.importCache.set(cacheKey, { promise: importPromise });
114
+ try {
115
+ return await importPromise;
117
116
  }
118
- if (this.dependencyHasher.matchesStoredHashes(cachedModule.dependencyHashes, filePath, fileHash)) {
119
- return (await cachedModule.promise);
117
+ catch (error) {
118
+ this.importCache.delete(cacheKey);
119
+ throw error;
120
120
  }
121
- this.importCache.delete(cacheKey);
122
121
  }
123
- const importPromise = this.loadModule({
122
+ return await this.loadModule({
124
123
  ...options,
125
124
  fileHash,
126
- importCacheKey: cacheKey,
127
125
  });
128
- this.importCache.set(cacheKey, { promise: importPromise });
129
- try {
130
- return await importPromise;
131
- }
132
- catch (error) {
133
- this.importCache.delete(cacheKey);
134
- throw error;
135
- }
136
126
  }
137
127
  async loadModule(options) {
138
- const { filePath, invalidationVersion = this.developmentInvalidationVersion, cacheScope, fileHash, importCacheKey, } = options;
128
+ const { filePath, fileHash, importCacheKey } = options;
139
129
  const { rootDir, outdir, splitting, externalPackages, transpileErrorMessage = (details) => `Error transpiling page module: ${details}`, noOutputMessage = (targetFilePath) => `No transpiled output generated for page module: ${targetFilePath}`, } = options;
140
130
  const outputFileName = resolvePageModuleOutputFileName({
141
131
  filePath,
142
132
  fileHash,
143
- cacheScope,
144
- invalidationVersion,
145
133
  });
146
134
  const outputNamingTemplate = outputFileName.replace(/\.mjs$/u, '.[ext]');
147
135
  const preferredOutputPath = path.join(outdir, outputFileName);
@@ -178,24 +166,26 @@ export class PageModuleImportService {
178
166
  externalPackages: buildOptions.externalPackages,
179
167
  jsx: buildOptions.jsx,
180
168
  plugins: buildOptions.plugins,
169
+ sourceTransforms: buildOptions.sourceTransforms,
181
170
  fileHash,
182
171
  };
183
172
  const routeModuleBuildCache = this.getRouteModuleBuildCache(outdir);
184
173
  const cachedBuild = routeModuleBuildCache.lookup(cacheBuildOptions);
185
174
  if (cachedBuild) {
186
- return (await import(/* @vite-ignore */ pathToFileURL(cachedBuild.outputPath).href));
175
+ const loadedModule = (await import(/* @vite-ignore */ pathToFileURL(cachedBuild.outputPath).href));
176
+ recordPageModuleLoad('disk-cache-hit');
177
+ return loadedModule;
187
178
  }
188
- if (!cacheScope &&
189
- shouldBuildPagesUnifiedGraph() &&
190
- this.appConfig &&
191
- isPagesUnifiedGraphPage(filePath, this.appConfig)) {
179
+ if (shouldBuildPagesUnifiedGraph() && this.appConfig && isPagesUnifiedGraphPage(filePath, this.appConfig)) {
192
180
  const graphModule = await importPagesUnifiedGraphModule(this.appConfig, filePath);
193
181
  if (graphModule !== undefined) {
182
+ recordPageModuleLoad('unified-graph');
194
183
  return graphModule;
195
184
  }
196
185
  }
197
186
  recordPageModuleBuildInvocation();
198
187
  const buildResult = await this.dependencies.buildModule(buildOptions, options.buildExecutor);
188
+ recordPageModuleLoad('cold-build', buildResult.outputs.length);
199
189
  if (!buildResult.success) {
200
190
  const details = buildResult.logs.map((log) => log.message).join(' | ');
201
191
  throw new Error(transpileErrorMessage(details));
@@ -220,10 +210,8 @@ export class PageModuleImportService {
220
210
  dependencyModulePaths,
221
211
  });
222
212
  const compiledOutputUrl = pathToFileURL(compiledOutput);
223
- if (shouldAddRuntimeUpdateQuery(invalidationVersion, cacheScope)) {
224
- compiledOutputUrl.searchParams.set('update', [fileHash, invalidationVersion, cacheScope ? sanitizeCacheScope(cacheScope) : undefined]
225
- .filter((value) => value !== undefined)
226
- .join('-'));
213
+ if (shouldAddRuntimeUpdateQuery()) {
214
+ compiledOutputUrl.searchParams.set('update', `${fileHash}-${this.developmentImportGeneration}`);
227
215
  }
228
216
  return (await import(/* @vite-ignore */ compiledOutputUrl.href));
229
217
  }
@@ -231,18 +219,13 @@ export class PageModuleImportService {
231
219
  return getSharedRouteModuleBuildCache(outdir, this.appConfig);
232
220
  }
233
221
  }
234
- function createRuntimeModuleUrl(filePath, fileHash, invalidationVersion, cacheScope) {
222
+ function createRuntimeModuleUrl(filePath, fileHash, developmentImportGeneration) {
235
223
  const moduleUrl = pathToFileURL(filePath);
236
- if (shouldAddRuntimeUpdateQuery(invalidationVersion, cacheScope)) {
237
- moduleUrl.searchParams.set('update', [fileHash, invalidationVersion, cacheScope ? sanitizeCacheScope(cacheScope) : undefined]
238
- .filter((value) => value !== undefined)
239
- .join('-'));
224
+ if (shouldAddRuntimeUpdateQuery()) {
225
+ moduleUrl.searchParams.set('update', `${fileHash}-${developmentImportGeneration}`);
240
226
  }
241
227
  return moduleUrl;
242
228
  }
243
- function shouldAddRuntimeUpdateQuery(invalidationVersion, cacheScope) {
244
- return process.env.NODE_ENV === 'development' || invalidationVersion > 0 || !!cacheScope;
245
- }
246
- function sanitizeCacheScope(cacheScope) {
247
- return cacheScope.replace(/[^a-zA-Z0-9_-]+/g, '-');
229
+ function shouldAddRuntimeUpdateQuery() {
230
+ return process.env.NODE_ENV === 'development';
248
231
  }
@@ -28,10 +28,10 @@ export class RouteModuleBuildCache {
28
28
  return undefined;
29
29
  }
30
30
  const manifest = this.loadManifest();
31
- if (manifest.invalidationVersion !== this.dependencies.getCorePackageVersion()) {
31
+ if (manifest.corePackageVersion !== this.dependencies.getCorePackageVersion()) {
32
32
  return undefined;
33
33
  }
34
- const buildKey = createPersistedRouteModuleBuildKey(options);
34
+ const buildKey = createPersistedRouteModuleBuildKey(options, options.sourceTransforms);
35
35
  const cacheFilePath = normalizeRouteModuleCachePath(options.filePath);
36
36
  const entry = manifest.entries[cacheFilePath];
37
37
  if (!entry || entry.sourceHash !== options.fileHash || entry.buildKey !== buildKey) {
@@ -60,12 +60,12 @@ export class RouteModuleBuildCache {
60
60
  dependencyHashes[cacheFilePath] = options.fileHash;
61
61
  const manifest = this.loadManifest();
62
62
  const existingEntry = manifest.entries[cacheFilePath];
63
- manifest.invalidationVersion = this.dependencies.getCorePackageVersion();
63
+ manifest.corePackageVersion = this.dependencies.getCorePackageVersion();
64
64
  manifest.entries[cacheFilePath] = {
65
65
  sourceHash: options.fileHash,
66
66
  outputPath: options.outputPath,
67
67
  builtAt: Date.now(),
68
- buildKey: createPersistedRouteModuleBuildKey(options),
68
+ buildKey: createPersistedRouteModuleBuildKey(options, options.sourceTransforms),
69
69
  dependencyHashes,
70
70
  renderedOutputs: existingEntry?.renderedOutputs,
71
71
  };
@@ -87,7 +87,7 @@ export class RouteModuleBuildCache {
87
87
  return;
88
88
  }
89
89
  const manifest = this.loadManifest();
90
- manifest.invalidationVersion = this.dependencies.getCorePackageVersion();
90
+ manifest.corePackageVersion = this.dependencies.getCorePackageVersion();
91
91
  manifest.configHash = context.configHash;
92
92
  manifest.buildInputsFingerprint = context.buildInputsFingerprint;
93
93
  this.persistManifest(manifest);
@@ -140,7 +140,7 @@ export class RouteModuleBuildCache {
140
140
  const cacheFilePath = normalizeRouteModuleCachePath(options.filePath);
141
141
  const manifest = this.loadManifest();
142
142
  const existingEntry = manifest.entries[cacheFilePath];
143
- manifest.invalidationVersion = this.dependencies.getCorePackageVersion();
143
+ manifest.corePackageVersion = this.dependencies.getCorePackageVersion();
144
144
  manifest.configHash = options.context.configHash;
145
145
  manifest.buildInputsFingerprint = options.context.buildInputsFingerprint;
146
146
  manifest.entries[cacheFilePath] = {
@@ -1,3 +1,4 @@
1
+ import type { BuildOptions } from '../../build/contracts/build-contracts.js';
1
2
  import type { PageModuleBuildImportOptions } from './page-module-import.service.js';
2
3
  import type { RouteModuleDependencyHashes } from './route-module-dependency-hasher.js';
3
4
  export { createJsxCacheKey, createPluginCacheKey, getCorePackageVersion, hashPluginSetup, } from '../../build/cache/cache-keys.js';
@@ -19,7 +20,7 @@ export interface RouteModuleStaticRenderCacheEntry {
19
20
  }
20
21
  /** On-disk manifest describing all cached route-module builds for one server outdir. */
21
22
  export interface RouteModuleBuildCacheManifest {
22
- invalidationVersion: string;
23
+ corePackageVersion: string;
23
24
  configHash?: string;
24
25
  buildInputsFingerprint?: string;
25
26
  entries: Record<string, RouteModuleBuildCacheEntry>;
@@ -38,17 +39,19 @@ export type RouteModuleBuildCacheManifestReader = (manifestPath: string) => Rout
38
39
  export type RouteModuleBuildCacheManifestWriter = (manifestPath: string, manifest: RouteModuleBuildCacheManifest) => void;
39
40
  /** Normalizes filesystem paths used as manifest keys and build-key inputs. */
40
41
  export declare function normalizeRouteModuleCachePath(filePath: string): string;
41
- /** Returns whether route-module disk caching should run for the current import. */
42
+ /**
43
+ * Returns whether route-module disk caching should run for the current import.
44
+ */
42
45
  export declare function shouldPersistRouteModuleBuildCache(options: PageModuleBuildImportOptions): boolean;
46
+ /** Canonical reuse identity for route-module memory and disk caches. */
47
+ export declare function createRouteModuleReuseIdentity(options: PageModuleBuildImportOptions, sourceTransforms?: BuildOptions['sourceTransforms']): string;
43
48
  /** Derives the deterministic on-disk filename for one transpiled route module. */
44
49
  export declare function resolvePageModuleOutputFileName(options: {
45
50
  filePath: string;
46
51
  fileHash: string;
47
- cacheScope?: string;
48
- invalidationVersion?: number;
49
52
  }): string;
50
53
  /** Builds the cache key for persisted production route-module builds. */
51
- export declare function createPersistedRouteModuleBuildKey(options: PageModuleBuildImportOptions): string;
54
+ export declare function createPersistedRouteModuleBuildKey(options: PageModuleBuildImportOptions, sourceTransforms?: BuildOptions['sourceTransforms']): string;
52
55
  export declare function createEmptyRouteModuleBuildCacheManifest(): RouteModuleBuildCacheManifest;
53
56
  export declare function readRouteModuleBuildCacheManifest(manifestPath: string): RouteModuleBuildCacheManifest | undefined;
54
57
  export declare function writeRouteModuleBuildCacheManifest(manifestPath: string, manifest: RouteModuleBuildCacheManifest): void;
@@ -1,39 +1,23 @@
1
1
  import path from 'node:path';
2
2
  import { readProductionCacheManifest, writeProductionCacheManifest } from '../../build/cache/production-build-cache.js';
3
- import { createJsxCacheKey, createPluginCacheKey, getCorePackageVersion } from '../../build/cache/cache-keys.js';
3
+ import { createJsxCacheKey, createPluginCacheKey, createSourceTransformCacheKey, getCorePackageVersion, } from '../../build/cache/cache-keys.js';
4
4
  export { createJsxCacheKey, createPluginCacheKey, getCorePackageVersion, hashPluginSetup, } from '../../build/cache/cache-keys.js';
5
5
  export { ROUTE_MODULE_BUILD_CACHE_FILENAME } from '../../build/cache/cache-constants.js';
6
6
  /** Normalizes filesystem paths used as manifest keys and build-key inputs. */
7
7
  export function normalizeRouteModuleCachePath(filePath) {
8
8
  return path.resolve(filePath);
9
9
  }
10
- /** Returns whether route-module disk caching should run for the current import. */
10
+ /**
11
+ * Returns whether route-module disk caching should run for the current import.
12
+ */
11
13
  export function shouldPersistRouteModuleBuildCache(options) {
12
- if (process.env.NODE_ENV !== 'production') {
14
+ if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'development') {
13
15
  return false;
14
16
  }
15
- if (options.bypassCache) {
16
- return false;
17
- }
18
- if ((options.invalidationVersion ?? 0) > 0) {
19
- return false;
20
- }
21
- if (options.cacheScope) {
22
- return false;
23
- }
24
- return true;
25
- }
26
- /** Derives the deterministic on-disk filename for one transpiled route module. */
27
- export function resolvePageModuleOutputFileName(options) {
28
- const fileBaseName = path.basename(options.filePath, path.extname(options.filePath));
29
- const cacheScopeSuffix = options.cacheScope ? `-${sanitizeCacheScope(options.cacheScope)}` : '';
30
- const invalidationSuffix = shouldVersionBuildOutputPath(options.invalidationVersion ?? 0)
31
- ? `-v${options.invalidationVersion}`
32
- : '';
33
- return `${fileBaseName}-${options.fileHash}${cacheScopeSuffix}${invalidationSuffix}.mjs`;
17
+ return !options.bypassCache;
34
18
  }
35
- /** Builds the cache key for persisted production route-module builds. */
36
- export function createPersistedRouteModuleBuildKey(options) {
19
+ /** Canonical reuse identity for route-module memory and disk caches. */
20
+ export function createRouteModuleReuseIdentity(options, sourceTransforms) {
37
21
  return [
38
22
  path.resolve(options.rootDir),
39
23
  path.resolve(options.outdir),
@@ -41,11 +25,21 @@ export function createPersistedRouteModuleBuildKey(options) {
41
25
  options.externalPackages ?? 'default',
42
26
  createJsxCacheKey(options.jsx),
43
27
  createPluginCacheKey(options.plugins),
28
+ createSourceTransformCacheKey(sourceTransforms),
44
29
  ].join('::');
45
30
  }
31
+ /** Derives the deterministic on-disk filename for one transpiled route module. */
32
+ export function resolvePageModuleOutputFileName(options) {
33
+ const fileBaseName = path.basename(options.filePath, path.extname(options.filePath));
34
+ return `${fileBaseName}-${options.fileHash}.mjs`;
35
+ }
36
+ /** Builds the cache key for persisted production route-module builds. */
37
+ export function createPersistedRouteModuleBuildKey(options, sourceTransforms) {
38
+ return createRouteModuleReuseIdentity(options, sourceTransforms);
39
+ }
46
40
  export function createEmptyRouteModuleBuildCacheManifest() {
47
41
  return {
48
- invalidationVersion: getCorePackageVersion(),
42
+ corePackageVersion: getCorePackageVersion(),
49
43
  entries: {},
50
44
  };
51
45
  }
@@ -55,7 +49,7 @@ export function readRouteModuleBuildCacheManifest(manifestPath) {
55
49
  return undefined;
56
50
  }
57
51
  return {
58
- invalidationVersion: parsed.invalidationVersion ?? '',
52
+ corePackageVersion: parsed.corePackageVersion ?? parsed.invalidationVersion ?? '',
59
53
  configHash: parsed.configHash,
60
54
  buildInputsFingerprint: parsed.buildInputsFingerprint,
61
55
  entries: parsed.entries,
@@ -64,9 +58,3 @@ export function readRouteModuleBuildCacheManifest(manifestPath) {
64
58
  export function writeRouteModuleBuildCacheManifest(manifestPath, manifest) {
65
59
  writeProductionCacheManifest(manifestPath, manifest);
66
60
  }
67
- function shouldVersionBuildOutputPath(invalidationVersion) {
68
- return typeof Bun !== 'undefined' && invalidationVersion > 0;
69
- }
70
- function sanitizeCacheScope(cacheScope) {
71
- return cacheScope.replace(/[^a-zA-Z0-9_-]+/g, '-');
72
- }
@@ -18,7 +18,6 @@ export type ServerModuleTranspilerBootstrapArgs = {
18
18
  getBuildExecutor?: () => BuildExecutor | undefined;
19
19
  canLoadSourceModuleFromHost?: (filePath: string) => boolean;
20
20
  getHostModuleLoader?: SourceModuleLoaderFactory;
21
- getInvalidationVersion?: () => number;
22
21
  invalidateModules?: (changedFiles?: string[]) => void;
23
22
  pageModuleImportService?: ServerModuleImportDependency;
24
23
  /** Factory evaluated on each importModule call to produce plugins applied to every load. */
@@ -35,7 +34,6 @@ export declare class ServerModuleTranspiler {
35
34
  private readonly pageModuleImportService;
36
35
  private readonly getRootDir;
37
36
  private readonly getBuildExecutor;
38
- private readonly getInvalidationVersion;
39
37
  private readonly invalidateModules;
40
38
  private readonly getDefaultPlugins;
41
39
  /**
@@ -10,7 +10,6 @@ export class ServerModuleTranspiler {
10
10
  pageModuleImportService;
11
11
  getRootDir;
12
12
  getBuildExecutor;
13
- getInvalidationVersion;
14
13
  invalidateModules;
15
14
  getDefaultPlugins;
16
15
  /**
@@ -26,7 +25,6 @@ export class ServerModuleTranspiler {
26
25
  });
27
26
  this.getRootDir = () => args.rootDir;
28
27
  this.getBuildExecutor = args.getBuildExecutor ?? (() => undefined);
29
- this.getInvalidationVersion = () => args.getInvalidationVersion?.();
30
28
  this.getDefaultPlugins = args.getDefaultPlugins ?? (() => []);
31
29
  this.invalidateModules = (changedFiles) => {
32
30
  if (args.invalidateModules) {
@@ -48,7 +46,6 @@ export class ServerModuleTranspiler {
48
46
  ...(mergedPlugins.length > 0 ? { plugins: mergedPlugins } : {}),
49
47
  rootDir: this.getRootDir(),
50
48
  buildExecutor: this.getBuildExecutor(),
51
- invalidationVersion: this.getInvalidationVersion(),
52
49
  });
53
50
  }
54
51
  /**
@@ -15,7 +15,7 @@ import { normalizePathname } from '../utils/path-pattern.js';
15
15
  export const STATIC_SITE_GENERATOR_ERRORS = {
16
16
  ROUTE_RENDERER_FACTORY_REQUIRED: 'RouteRendererFactory is required for render strategy',
17
17
  unsupportedBodyType: (bodyType) => `Unsupported body type for static generation: ${bodyType}`,
18
- missingIntegration: (routePath) => `View at ${routePath} is missing __eco.integration. Ensure it's defined with eco.page().`,
18
+ missingIntegration: (routePath) => `View at ${routePath} is missing component identity integration. Ensure it's defined with eco.page().`,
19
19
  noRendererForIntegration: (integrationName) => `No renderer found for integration: ${integrationName}`,
20
20
  dynamicRouteRequiresStaticPaths: (routePath) => `Dynamic route ${routePath} requires staticPaths to be defined on the view.`,
21
21
  };
@@ -430,7 +430,7 @@ export class StaticSiteGenerator {
430
430
  }
431
431
  }
432
432
  resolveExplicitViewSourceFile(view) {
433
- const sourceFile = view.config?.__eco?.file;
433
+ const sourceFile = view.config?.identity?.file;
434
434
  if (!sourceFile) {
435
435
  return undefined;
436
436
  }
@@ -107,6 +107,10 @@ export type EcoPagesAppConfig = {
107
107
  sitemap: SitemapConfig;
108
108
  /** Additional paths to watch. Use this to monitor extra files. It is relative to the rootDir */
109
109
  additionalWatchPaths: string[];
110
+ /** Static paths to SSR-prewarm in development before user navigation. */
111
+ devPrewarmPaths?: string[];
112
+ /** Static paths to SSR-prewarm before the development server reports ready. */
113
+ devPrewarmBeforeReadyPaths?: string[];
110
114
  /**
111
115
  * @default { title: 'Ecopages', description: 'Ecopages' }
112
116
  */
@@ -1,4 +1,5 @@
1
1
  import type { Readable } from 'node:stream';
2
+ import type { ComponentIdentity } from '../eco/component-identity.js';
2
3
  import type { ApiResponseBuilder } from '../adapters/shared/http/api-response.js';
3
4
  import type { ForeignChildRuntime } from '../route-renderer/orchestration/foreign-child/component-render-context.js';
4
5
  import type { EcoPageComponent } from '../eco/eco.types.js';
@@ -339,7 +340,7 @@ export type EcoComponentDependencies = {
339
340
  /**
340
341
  * Child components whose assets and foreign-child graph are collected transitively.
341
342
  * Each entry must be an `eco.component()`, `eco.layout()`, or `eco.html()` result
342
- * with plugin-injected `config.__eco` metadata.
343
+ * with bound component identity.
343
344
  */
344
345
  components?: EcoDeclaredComponent[];
345
346
  };
@@ -347,8 +348,8 @@ export type EcoComponentDependencies = {
347
348
  * Component returned from `eco.component()`, `eco.layout()`, or `eco.html()`.
348
349
  *
349
350
  * @remarks
350
- * Used for `dependencies.components` and eco factory return types. Plugin-injected
351
- * `config.__eco` is enforced at runtime via `isEcoDeclaredComponent()`, not by this alias.
351
+ * Used for `dependencies.components` and eco factory return types. Bound
352
+ * component identity is enforced at runtime via `isEcoDeclaredComponent()`, not by this alias.
352
353
  */
353
354
  export type EcoDeclaredComponent<P = any, R = any> = EcoComponent<P, R>;
354
355
  export type EcoPagesElement = string | Promise<string>;
@@ -370,22 +371,9 @@ export type EcoChildren = string | Promise<string> | number | bigint | boolean |
370
371
  * Represents the input configuration for EcoPages.
371
372
  */
372
373
  export type EcoPagesConfig = Omit<Partial<EcoPagesAppConfig>, 'baseUrl' | 'derivedPaths' | 'templatesExt' | 'integrationsDependencies'> & Pick<EcoPagesAppConfig, 'baseUrl' | 'rootDir'>;
373
- /**
374
- * Internal metadata injected by eco-component-meta-plugin.
375
- * Contains component file path and integration info for dependency resolution.
376
- * @internal
377
- */
378
- export interface EcoInjectedMeta {
379
- /** Hashed identifier for client-side use (doesn't expose file paths) */
380
- id: string;
381
- /** Full file path of the component (use path.dirname() to get directory) */
382
- file: string;
383
- /** The integration identifier (e.g., 'react', 'kitajs', 'lit', 'ghtml', 'mdx') */
384
- integration: string;
385
- }
386
374
  export type EcoComponentConfig = {
387
- /** @internal Injected by eco-component-meta-plugin */
388
- __eco?: EcoInjectedMeta;
375
+ /** Canonical integration, module, and stable component attribution. */
376
+ identity?: ComponentIdentity;
389
377
  /**
390
378
  * Explicit integration override for this component.
391
379
  * When provided, this takes precedence over auto-detected integration metadata.
@@ -799,6 +787,14 @@ export type RouteRendererOptions = {
799
787
  params?: PageParams;
800
788
  query?: PageQuery;
801
789
  locals?: RequestLocals;
790
+ /**
791
+ * Preloaded page module from request-time inspection.
792
+ *
793
+ * @remarks
794
+ * When present, render preparation reuses this module instead of importing the
795
+ * page file again for the same request.
796
+ */
797
+ pageModule?: EcoPageFile;
802
798
  /**
803
799
  * Extra props merged into the page props for this render.
804
800
  * @remarks Used by semantic error pages (for example development `message` /
@@ -817,6 +813,8 @@ export type RouteRenderResult = {
817
813
  body: RouteRendererBody;
818
814
  /** Cache strategy from page component's eco.page({ cache }) option */
819
815
  cacheStrategy?: CacheStrategy;
816
+ /** Source paths observed during render preparation for HTML cache invalidation. */
817
+ sourceDependencyPaths?: readonly string[];
820
818
  };
821
819
  /**
822
820
  * Represents the dependencies required for an integration plugin.
@@ -888,6 +886,8 @@ export type IntegrationRendererRenderOptions<C = EcoPagesElement> = RouteRendere
888
886
  pageProps?: Record<string, unknown>;
889
887
  cacheStrategy?: CacheStrategy;
890
888
  pageLocals?: RequestLocals;
889
+ /** @internal Source paths from render preparation for HTML cache dependency registration. */
890
+ sourceDependencyPaths?: readonly string[];
891
891
  };
892
892
  /**
893
893
  * Structured page asset package produced after dependency resolution.
@@ -4,6 +4,7 @@ import { fileSystem } from '@ecopages/file-system';
4
4
  import { appLogger } from '../global/app-logger.js';
5
5
  import { DevelopmentInvalidationService, } from '../services/invalidation/development-invalidation.service.js';
6
6
  import { prepareHmrFileChange } from '../hmr/hmr-file-change-prep.js';
7
+ import { invalidateAppPageCacheBySourcePaths, clearAppPageCache } from '../services/cache/page-cache-service.js';
7
8
  import { getAppPageBrowserGraphSession } from '../route-renderer/orchestration/page-browser-graph/page-browser-graph-session.js';
8
9
  import { isRegisteredDevTransformEntrypoint } from '../hmr/hmr-entrypoint-output.js';
9
10
  import { resolveInternalExecutionDir } from '../utils/resolve-work-dir.js';
@@ -163,6 +164,12 @@ export class ProjectWatcher {
163
164
  }
164
165
  this.uncacheModules();
165
166
  const resolvedFilePath = path.resolve(filePath);
167
+ if (shouldClearAllPageHtmlCache(plan.category)) {
168
+ await clearAppPageCache(this.appConfig);
169
+ }
170
+ else {
171
+ await invalidateAppPageCacheBySourcePaths(this.appConfig, [resolvedFilePath]);
172
+ }
166
173
  const graphPreparation = this.hmrManager.isEnabled()
167
174
  ? prepareHmrFileChange(this.appConfig, resolvedFilePath)
168
175
  : undefined;
@@ -404,3 +411,10 @@ export class ProjectWatcher {
404
411
  await this.changeQueue.catch(() => undefined);
405
412
  }
406
413
  }
414
+ function shouldClearAllPageHtmlCache(category) {
415
+ return (category === 'other' ||
416
+ category === 'server-source' ||
417
+ category === 'additional-watch' ||
418
+ category === 'include-source' ||
419
+ category === 'explicit-server-view');
420
+ }