@ecopages/core 0.2.0-beta.7 → 0.2.0-beta.8

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 (102) hide show
  1. package/package.json +26 -2
  2. package/src/adapters/abstract/application-adapter.d.ts +12 -1
  3. package/src/adapters/abstract/application-adapter.js +13 -0
  4. package/src/adapters/abstract/server-adapter.d.ts +3 -0
  5. package/src/adapters/bun/client-bridge.d.ts +1 -0
  6. package/src/adapters/bun/client-bridge.js +3 -0
  7. package/src/adapters/bun/create-app.d.ts +2 -0
  8. package/src/adapters/bun/create-app.js +28 -9
  9. package/src/adapters/bun/server-adapter.d.ts +13 -0
  10. package/src/adapters/bun/server-adapter.js +29 -4
  11. package/src/adapters/create-app.d.ts +13 -0
  12. package/src/adapters/create-app.js +10 -1
  13. package/src/adapters/node/create-app.d.ts +1 -0
  14. package/src/adapters/node/create-app.js +17 -7
  15. package/src/adapters/node/node-client-bridge.d.ts +1 -0
  16. package/src/adapters/node/node-client-bridge.js +3 -0
  17. package/src/adapters/node/server-adapter.d.ts +10 -13
  18. package/src/adapters/node/server-adapter.js +37 -82
  19. package/src/adapters/node/static-content-server.d.ts +1 -0
  20. package/src/adapters/node/static-content-server.js +3 -0
  21. package/src/adapters/shared/copy-runtime-public-dir.d.ts +6 -0
  22. package/src/adapters/shared/copy-runtime-public-dir.js +24 -0
  23. package/src/adapters/shared/runtime-app-bootstrap.d.ts +0 -1
  24. package/src/adapters/shared/runtime-app-bootstrap.js +1 -6
  25. package/src/adapters/shared/server-static-builder.d.ts +4 -6
  26. package/src/adapters/shared/server-static-builder.js +90 -34
  27. package/src/adapters/shared/shared-hmr-manager.d.ts +1 -0
  28. package/src/adapters/shared/shared-hmr-manager.js +15 -2
  29. package/src/build/README.md +44 -4
  30. package/src/build/build-adapter.d.ts +8 -2
  31. package/src/build/build-adapter.js +71 -33
  32. package/src/build/build-input-fingerprint.d.ts +28 -0
  33. package/src/build/build-input-fingerprint.js +31 -0
  34. package/src/build/jsx-ownership-plugins.d.ts +10 -0
  35. package/src/build/jsx-ownership-plugins.js +17 -0
  36. package/src/build/pages-unified-graph-build.d.ts +31 -0
  37. package/src/build/pages-unified-graph-build.js +208 -0
  38. package/src/build/parallel-build-executor.d.ts +21 -0
  39. package/src/build/parallel-build-executor.js +52 -0
  40. package/src/build/rolldown-adapter-helpers.js +5 -0
  41. package/src/build/rolldown-build-adapter.js +2 -0
  42. package/src/build/rolldown-build-invocation-metrics.d.ts +12 -0
  43. package/src/build/rolldown-build-invocation-metrics.js +43 -0
  44. package/src/build/rolldown-dev-build-adapter.d.ts +2 -2
  45. package/src/build/rolldown-dev-build-adapter.js +30 -14
  46. package/src/build/runtime-build-executor.d.ts +7 -13
  47. package/src/build/runtime-build-executor.js +31 -7
  48. package/src/build/server-entry-build-cache.d.ts +44 -0
  49. package/src/build/server-entry-build-cache.js +169 -0
  50. package/src/hmr/hmr.test.e2e.js +15 -4
  51. package/src/plugins/integration-plugin.d.ts +19 -8
  52. package/src/plugins/integration-plugin.js +7 -2
  53. package/src/plugins/processor.d.ts +5 -0
  54. package/src/plugins/processor.js +7 -0
  55. package/src/route-renderer/GRAPH.md +4 -4
  56. package/src/route-renderer/README.md +6 -5
  57. package/src/route-renderer/orchestration/foreign-subtree-execution.service.js +1 -3
  58. package/src/route-renderer/orchestration/integration-renderer.d.ts +10 -2
  59. package/src/route-renderer/orchestration/integration-renderer.js +32 -12
  60. package/src/route-renderer/orchestration/render-output.utils.d.ts +2 -0
  61. package/src/route-renderer/orchestration/render-output.utils.js +4 -0
  62. package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +2 -2
  63. package/src/route-renderer/orchestration/route-render-orchestrator.js +17 -16
  64. package/src/router/client/navigation-coordinator.js +10 -0
  65. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.d.ts +1 -0
  66. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.js +1 -0
  67. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.d.ts +4 -2
  68. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.js +27 -7
  69. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +17 -3
  70. package/src/services/assets/browser-bundle.service.d.ts +1 -0
  71. package/src/services/assets/browser-bundle.service.js +9 -3
  72. package/src/services/html/html-transformer.service.d.ts +1 -1
  73. package/src/services/html/html-transformer.service.js +4 -4
  74. package/src/services/invalidation/development-invalidation.service.d.ts +10 -1
  75. package/src/services/invalidation/development-invalidation.service.js +29 -0
  76. package/src/services/module-loading/README.md +49 -0
  77. package/src/services/module-loading/app-server-module-transpiler.service.js +4 -17
  78. package/src/services/module-loading/page-module-import.service.d.ts +4 -1
  79. package/src/services/module-loading/page-module-import.service.js +52 -29
  80. package/src/services/module-loading/route-module-build-cache-registry.d.ts +24 -0
  81. package/src/services/module-loading/route-module-build-cache-registry.js +37 -0
  82. package/src/services/module-loading/route-module-build-cache.d.ts +4 -0
  83. package/src/services/module-loading/route-module-build-cache.js +38 -0
  84. package/src/services/module-loading/route-module-build-cache.store.d.ts +57 -0
  85. package/src/services/module-loading/route-module-build-cache.store.js +203 -0
  86. package/src/services/module-loading/route-module-build-manifest.d.ts +61 -0
  87. package/src/services/module-loading/route-module-build-manifest.js +124 -0
  88. package/src/services/module-loading/route-module-dependency-hasher.d.ts +81 -0
  89. package/src/services/module-loading/route-module-dependency-hasher.js +121 -0
  90. package/src/services/module-loading/server-module-transpiler.service.js +1 -1
  91. package/src/static-site-generator/README.md +33 -0
  92. package/src/static-site-generator/static-build-invalidation.d.ts +10 -0
  93. package/src/static-site-generator/static-build-invalidation.js +78 -0
  94. package/src/static-site-generator/static-export-context.d.ts +22 -0
  95. package/src/static-site-generator/static-export-context.js +0 -0
  96. package/src/static-site-generator/static-site-generator.d.ts +17 -8
  97. package/src/static-site-generator/static-site-generator.js +176 -62
  98. package/src/types/internal-types.d.ts +8 -0
  99. package/src/utils/parse-cli-args.d.ts +1 -0
  100. package/src/utils/parse-cli-args.js +3 -0
  101. package/src/watchers/project-watcher.d.ts +8 -0
  102. package/src/watchers/project-watcher.js +32 -12
@@ -0,0 +1,124 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileSystem } from "@ecopages/file-system";
4
+ const ROUTE_MODULE_BUILD_CACHE_FILENAME = ".build-cache.json";
5
+ let cachedCorePackageVersion;
6
+ function getCorePackageVersion() {
7
+ if (cachedCorePackageVersion) {
8
+ return cachedCorePackageVersion;
9
+ }
10
+ const packageJsonPath = new URL("../../../package.json", import.meta.url);
11
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
12
+ cachedCorePackageVersion = packageJson.version ?? "0.0.0";
13
+ return cachedCorePackageVersion;
14
+ }
15
+ function normalizeRouteModuleCachePath(filePath) {
16
+ return path.resolve(filePath);
17
+ }
18
+ function shouldPersistRouteModuleBuildCache(options) {
19
+ if (process.env.NODE_ENV !== "production") {
20
+ return false;
21
+ }
22
+ if (options.bypassCache) {
23
+ return false;
24
+ }
25
+ if ((options.invalidationVersion ?? 0) > 0) {
26
+ return false;
27
+ }
28
+ if (options.cacheScope) {
29
+ return false;
30
+ }
31
+ return true;
32
+ }
33
+ function resolvePageModuleOutputFileName(options) {
34
+ const fileBaseName = path.basename(options.filePath, path.extname(options.filePath));
35
+ const cacheScopeSuffix = options.cacheScope ? `-${sanitizeCacheScope(options.cacheScope)}` : "";
36
+ const invalidationSuffix = shouldVersionBuildOutputPath(options.invalidationVersion ?? 0) ? `-v${options.invalidationVersion}` : "";
37
+ return `${fileBaseName}-${options.fileHash}${cacheScopeSuffix}${invalidationSuffix}.mjs`;
38
+ }
39
+ function createPersistedRouteModuleBuildKey(options) {
40
+ return [
41
+ path.resolve(options.rootDir),
42
+ path.resolve(options.outdir),
43
+ options.splitting ?? "default",
44
+ options.externalPackages ?? "default",
45
+ createJsxCacheKey(options.jsx),
46
+ createPluginCacheKey(options.plugins)
47
+ ].join("::");
48
+ }
49
+ function createEmptyRouteModuleBuildCacheManifest() {
50
+ return {
51
+ invalidationVersion: getCorePackageVersion(),
52
+ entries: {}
53
+ };
54
+ }
55
+ function readRouteModuleBuildCacheManifest(manifestPath) {
56
+ if (!fileSystem.exists(manifestPath)) {
57
+ return void 0;
58
+ }
59
+ try {
60
+ const parsed = JSON.parse(fileSystem.readFileSync(manifestPath));
61
+ if (!parsed || typeof parsed !== "object" || typeof parsed.entries !== "object") {
62
+ return void 0;
63
+ }
64
+ return {
65
+ invalidationVersion: parsed.invalidationVersion ?? "",
66
+ configHash: parsed.configHash,
67
+ buildInputsFingerprint: parsed.buildInputsFingerprint,
68
+ entries: parsed.entries
69
+ };
70
+ } catch {
71
+ return void 0;
72
+ }
73
+ }
74
+ function writeRouteModuleBuildCacheManifest(manifestPath, manifest) {
75
+ fileSystem.write(manifestPath, `${JSON.stringify(manifest, null, " ")}
76
+ `);
77
+ }
78
+ function shouldVersionBuildOutputPath(invalidationVersion) {
79
+ return typeof Bun !== "undefined" && invalidationVersion > 0;
80
+ }
81
+ function sanitizeCacheScope(cacheScope) {
82
+ return cacheScope.replace(/[^a-zA-Z0-9_-]+/g, "-");
83
+ }
84
+ function createJsxCacheKey(jsx) {
85
+ if (!jsx) {
86
+ return "jsx:default";
87
+ }
88
+ return JSON.stringify({
89
+ development: jsx.development ?? false,
90
+ factory: jsx.factory ?? null,
91
+ fragment: jsx.fragment ?? null,
92
+ importSource: jsx.importSource ?? null,
93
+ runtime: jsx.runtime ?? null,
94
+ sideEffects: jsx.sideEffects ?? null
95
+ });
96
+ }
97
+ function createPluginCacheKey(plugins) {
98
+ if (!plugins || plugins.length === 0) {
99
+ return "plugins:default";
100
+ }
101
+ return `plugins:${plugins.map((plugin) => `${plugin.name}:${hashPluginSetup(plugin.setup)}`).join(",")}`;
102
+ }
103
+ function hashPluginSetup(setup) {
104
+ let hash = 0;
105
+ const source = setup.toString();
106
+ for (let index = 0; index < source.length; index += 1) {
107
+ hash = hash * 31 + source.charCodeAt(index) | 0;
108
+ }
109
+ return (hash >>> 0).toString(36);
110
+ }
111
+ export {
112
+ ROUTE_MODULE_BUILD_CACHE_FILENAME,
113
+ createEmptyRouteModuleBuildCacheManifest,
114
+ createJsxCacheKey,
115
+ createPersistedRouteModuleBuildKey,
116
+ createPluginCacheKey,
117
+ getCorePackageVersion,
118
+ hashPluginSetup,
119
+ normalizeRouteModuleCachePath,
120
+ readRouteModuleBuildCacheManifest,
121
+ resolvePageModuleOutputFileName,
122
+ shouldPersistRouteModuleBuildCache,
123
+ writeRouteModuleBuildCacheManifest
124
+ };
@@ -0,0 +1,81 @@
1
+ import type { BuildResult } from '../../build/build-adapter.js';
2
+ /**
3
+ * Content hashes for every trackable source file in one route module's import graph.
4
+ *
5
+ * @remarks
6
+ * Keys are normalized absolute filesystem paths. Values are opaque content hashes
7
+ * produced by the active {@link FileSystem} implementation. Only app-owned source
8
+ * files are tracked; `node:` builtins and `node_modules` paths are excluded
9
+ * because they are covered by the core package {@link getCorePackageVersion | invalidation version}.
10
+ */
11
+ export type RouteModuleDependencyHashes = Record<string, string>;
12
+ type HashFileFn = (filePath: string) => string;
13
+ type ExistsFn = (filePath: string) => boolean;
14
+ /**
15
+ * Lazily hashes route-module dependency files while memoizing results for one build run.
16
+ *
17
+ * @remarks
18
+ * Multiple route modules often share layouts or utilities. Memoizing per path avoids
19
+ * re-reading the same dependency file while validating or recording many cache entries
20
+ * during one production build.
21
+ */
22
+ export declare class RouteModuleDependencyHasher {
23
+ private readonly memo;
24
+ private readonly hashFile;
25
+ private readonly exists;
26
+ constructor(dependencies?: {
27
+ hashFile?: HashFileFn;
28
+ exists?: ExistsFn;
29
+ });
30
+ /**
31
+ * Returns a memoized content hash for one trackable dependency path.
32
+ *
33
+ * @param filePath - Absolute or project-relative source path.
34
+ * @returns The content hash, or `undefined` when the path is not trackable or missing.
35
+ */
36
+ hashDependency(filePath: string): string | undefined;
37
+ /**
38
+ * Builds the dependency-hash map for one route entrypoint graph.
39
+ *
40
+ * @param modulePaths - Raw module ids from the bundler dependency graph.
41
+ * @returns Hashes for every trackable, existing source file in the graph.
42
+ */
43
+ createDependencyHashes(modulePaths: readonly string[]): RouteModuleDependencyHashes;
44
+ /**
45
+ * Returns whether every stored dependency hash still matches the current file contents.
46
+ *
47
+ * @param storedHashes - Dependency hashes previously persisted for one route module.
48
+ * @param entrypointPath - Route entry file whose hash is validated separately.
49
+ * @param entrypointSourceHash - Already-validated content hash for {@link entrypointPath}.
50
+ * @returns `true` when all stored hashes are present and still valid.
51
+ */
52
+ matchesStoredHashes(storedHashes: RouteModuleDependencyHashes | undefined, entrypointPath?: string, entrypointSourceHash?: string): boolean;
53
+ /** Clears the per-build memo. Intended for unit tests only. */
54
+ clearMemoForTests(): void;
55
+ }
56
+ /**
57
+ * Returns whether a bundler module id should participate in route-module cache invalidation.
58
+ *
59
+ * @remarks
60
+ * Builtins and installed packages are excluded. Their changes are expected to be
61
+ * reflected through framework-level invalidation instead of per-route file hashing.
62
+ */
63
+ export declare function isTrackableRouteModuleDependency(modulePath: string): boolean;
64
+ /**
65
+ * Filters a bundler module list down to trackable app-owned source files.
66
+ */
67
+ export declare function filterTrackableRouteModuleDependencies(modulePaths: readonly string[]): string[];
68
+ /**
69
+ * Resolves the dependency module list for one route entrypoint from a build result.
70
+ *
71
+ * @param buildResult - Completed route-module build output.
72
+ * @param entrypointPath - Absolute or root-relative route source path used as the build entry.
73
+ * @param rootDir - Project root used to normalize relative module ids.
74
+ * @returns Normalized module paths, falling back to the entrypoint when no graph is available.
75
+ */
76
+ export declare function resolveRouteModuleDependencyPaths(buildResult: Pick<BuildResult, 'dependencyGraph'>, entrypointPath: string, rootDir: string): string[];
77
+ /**
78
+ * Convenience helper that combines graph extraction and dependency hashing.
79
+ */
80
+ export declare function createRouteModuleDependencyHashes(hasher: RouteModuleDependencyHasher, buildResult: Pick<BuildResult, 'dependencyGraph'>, entrypointPath: string, rootDir: string): RouteModuleDependencyHashes;
81
+ export {};
@@ -0,0 +1,121 @@
1
+ import path from "node:path";
2
+ import { fileSystem } from "@ecopages/file-system";
3
+ class RouteModuleDependencyHasher {
4
+ memo = /* @__PURE__ */ new Map();
5
+ hashFile;
6
+ exists;
7
+ constructor(dependencies) {
8
+ this.hashFile = dependencies?.hashFile ?? ((filePath) => fileSystem.hash(filePath));
9
+ this.exists = dependencies?.exists ?? ((filePath) => fileSystem.exists(filePath));
10
+ }
11
+ /**
12
+ * Returns a memoized content hash for one trackable dependency path.
13
+ *
14
+ * @param filePath - Absolute or project-relative source path.
15
+ * @returns The content hash, or `undefined` when the path is not trackable or missing.
16
+ */
17
+ hashDependency(filePath) {
18
+ const normalizedPath = path.normalize(filePath);
19
+ if (!isTrackableRouteModuleDependency(normalizedPath)) {
20
+ return void 0;
21
+ }
22
+ if (!this.exists(normalizedPath)) {
23
+ return void 0;
24
+ }
25
+ const cachedHash = this.memo.get(normalizedPath);
26
+ if (cachedHash) {
27
+ return cachedHash;
28
+ }
29
+ const nextHash = this.hashFile(normalizedPath);
30
+ this.memo.set(normalizedPath, nextHash);
31
+ return nextHash;
32
+ }
33
+ /**
34
+ * Builds the dependency-hash map for one route entrypoint graph.
35
+ *
36
+ * @param modulePaths - Raw module ids from the bundler dependency graph.
37
+ * @returns Hashes for every trackable, existing source file in the graph.
38
+ */
39
+ createDependencyHashes(modulePaths) {
40
+ const dependencyHashes = {};
41
+ for (const modulePath of filterTrackableRouteModuleDependencies(modulePaths)) {
42
+ const dependencyHash = this.hashDependency(modulePath);
43
+ if (dependencyHash) {
44
+ dependencyHashes[modulePath] = dependencyHash;
45
+ }
46
+ }
47
+ return dependencyHashes;
48
+ }
49
+ /**
50
+ * Returns whether every stored dependency hash still matches the current file contents.
51
+ *
52
+ * @param storedHashes - Dependency hashes previously persisted for one route module.
53
+ * @param entrypointPath - Route entry file whose hash is validated separately.
54
+ * @param entrypointSourceHash - Already-validated content hash for {@link entrypointPath}.
55
+ * @returns `true` when all stored hashes are present and still valid.
56
+ */
57
+ matchesStoredHashes(storedHashes, entrypointPath, entrypointSourceHash) {
58
+ if (!storedHashes || Object.keys(storedHashes).length === 0) {
59
+ return false;
60
+ }
61
+ const normalizedEntrypoint = entrypointPath ? path.normalize(entrypointPath) : void 0;
62
+ for (const [dependencyPath, storedHash] of Object.entries(storedHashes)) {
63
+ if (normalizedEntrypoint && path.normalize(dependencyPath) === normalizedEntrypoint) {
64
+ if (entrypointSourceHash && storedHash !== entrypointSourceHash) {
65
+ return false;
66
+ }
67
+ continue;
68
+ }
69
+ const currentHash = this.hashDependency(dependencyPath);
70
+ if (!currentHash || currentHash !== storedHash) {
71
+ return false;
72
+ }
73
+ }
74
+ return true;
75
+ }
76
+ /** Clears the per-build memo. Intended for unit tests only. */
77
+ clearMemoForTests() {
78
+ this.memo.clear();
79
+ }
80
+ }
81
+ function isTrackableRouteModuleDependency(modulePath) {
82
+ const normalizedPath = path.normalize(modulePath);
83
+ if (normalizedPath.startsWith("node:")) {
84
+ return false;
85
+ }
86
+ if (normalizedPath.includes(`${path.sep}node_modules${path.sep}`)) {
87
+ return false;
88
+ }
89
+ return /\.(?:[cm]?[jt]sx?|mdx)$/iu.test(normalizedPath) || normalizedPath.endsWith(".json");
90
+ }
91
+ function filterTrackableRouteModuleDependencies(modulePaths) {
92
+ const uniquePaths = /* @__PURE__ */ new Set();
93
+ for (const modulePath of modulePaths) {
94
+ const normalizedPath = path.normalize(modulePath);
95
+ if (isTrackableRouteModuleDependency(normalizedPath)) {
96
+ uniquePaths.add(normalizedPath);
97
+ }
98
+ }
99
+ return Array.from(uniquePaths).sort();
100
+ }
101
+ function resolveRouteModuleDependencyPaths(buildResult, entrypointPath, rootDir) {
102
+ const normalizedEntrypoint = path.isAbsolute(entrypointPath) ? path.normalize(entrypointPath) : path.normalize(path.resolve(rootDir, entrypointPath));
103
+ const graphEntrypoints = buildResult.dependencyGraph?.entrypoints ?? {};
104
+ const dependencyPaths = graphEntrypoints[normalizedEntrypoint] ?? graphEntrypoints[entrypointPath] ?? Object.entries(graphEntrypoints).find(([candidate]) => path.normalize(candidate) === normalizedEntrypoint)?.[1];
105
+ if (!dependencyPaths || dependencyPaths.length === 0) {
106
+ return [normalizedEntrypoint];
107
+ }
108
+ return dependencyPaths.map(
109
+ (dependencyPath) => path.isAbsolute(dependencyPath) ? path.normalize(dependencyPath) : path.normalize(path.resolve(rootDir, dependencyPath))
110
+ );
111
+ }
112
+ function createRouteModuleDependencyHashes(hasher, buildResult, entrypointPath, rootDir) {
113
+ return hasher.createDependencyHashes(resolveRouteModuleDependencyPaths(buildResult, entrypointPath, rootDir));
114
+ }
115
+ export {
116
+ RouteModuleDependencyHasher,
117
+ createRouteModuleDependencyHashes,
118
+ filterTrackableRouteModuleDependencies,
119
+ isTrackableRouteModuleDependency,
120
+ resolveRouteModuleDependencyPaths
121
+ };
@@ -11,7 +11,7 @@ class ServerModuleTranspiler {
11
11
  * context.
12
12
  */
13
13
  constructor(args) {
14
- this.pageModuleImportService = args.pageModuleImportService ?? new PageModuleImportService({
14
+ this.pageModuleImportService = args.pageModuleImportService ?? new PageModuleImportService(void 0, {
15
15
  canLoadSourceModuleFromHost: args.canLoadSourceModuleFromHost,
16
16
  getHostModuleLoader: args.getHostModuleLoader
17
17
  });
@@ -24,3 +24,36 @@ That means it should reuse:
24
24
  - asset processing
25
25
 
26
26
  It should not invent a parallel rendering stack just for build mode.
27
+
28
+ ## Files
29
+
30
+ | File | Role |
31
+ | ------------------------------ | ---------------------------------------------------------------------------- |
32
+ | `static-site-generator.ts` | Route enumeration, HTML artifact writes, integration export hooks |
33
+ | `static-export-context.ts` | Hook context type for `beforeStaticExport` / `afterStaticExport` |
34
+ | `static-build-invalidation.ts` | `dist/` reset policy, production cache clearing, static-render cache context |
35
+
36
+ Build-input fingerprinting (`hashAppConfigFile`, `createBuildInputsFingerprint`) lives in `packages/core/src/build/build-input-fingerprint.ts` and is shared with server-entry and unified-graph caches.
37
+
38
+ ## Incremental static export
39
+
40
+ `ServerStaticBuilder.prepareExportDirectory()` decides whether to wipe `dist/`:
41
+
42
+ 1. `force: true` → always reset
43
+ 2. Any processor/integration reports `didChange()` → reset
44
+ 3. Route-module manifest lacks matching `configHash` + `buildInputsFingerprint` → reset
45
+
46
+ When `dist/` is preserved (`preserveExportDirectory: true`), `StaticSiteGenerator.run()` prunes HTML files no longer in the active route set.
47
+
48
+ Rendered HTML reuse is tracked in `.eco/.server-modules/.build-cache.json` under each route module's `renderedOutputs`. `canReuseStaticRender()` skips re-rendering when the source file, dependency hashes, and output path are unchanged.
49
+
50
+ `clearProductionBuildCaches()` runs on `force` builds and removes all persisted `.eco` build manifests (route modules, server entry, unified pages graph).
51
+
52
+ ## Integration hooks
53
+
54
+ Integrations may implement:
55
+
56
+ - `beforeStaticExport(context)` — runs after unified-graph prebuild, before page rendering
57
+ - `afterStaticExport(context)` — runs in a `finally` block after generation completes
58
+
59
+ See `StaticExportContext` in `static-export-context.ts`.
@@ -0,0 +1,10 @@
1
+ import type { EcoPagesAppConfig } from '../types/internal-types.js';
2
+ import { type RouteModuleStaticRenderCacheContext } from '../services/module-loading/route-module-build-manifest.js';
3
+ export type { BuildInputChangeContributor, IntegrationPlugin, Processor } from '../build/build-input-fingerprint.js';
4
+ export { collectBuildInputContributors, createBuildInputsFingerprint, didBuildInputContributorChange, hashAppConfigFile, haveBuildInputsChanged, } from '../build/build-input-fingerprint.js';
5
+ /** Builds the static-render invalidation context for one app config. */
6
+ export declare function createRouteModuleStaticRenderCacheContext(appConfig: EcoPagesAppConfig): RouteModuleStaticRenderCacheContext;
7
+ /** Returns whether `dist/` should be wiped before the next static export. */
8
+ export declare function shouldResetStaticExportDirectory(appConfig: EcoPagesAppConfig, force?: boolean): boolean;
9
+ /** Removes persisted production build caches so the next build recomputes everything. */
10
+ export declare function clearProductionBuildCaches(appConfig: EcoPagesAppConfig): void;
@@ -0,0 +1,78 @@
1
+ import path from "node:path";
2
+ import { fileSystem } from "@ecopages/file-system";
3
+ import {
4
+ createBuildInputsFingerprint,
5
+ hashAppConfigFile,
6
+ haveBuildInputsChanged
7
+ } from "../build/build-input-fingerprint.js";
8
+ import {
9
+ ROUTE_MODULE_BUILD_CACHE_FILENAME
10
+ } from "../services/module-loading/route-module-build-manifest.js";
11
+ import {
12
+ getSharedRouteModuleBuildCache,
13
+ getServerModuleBuildCacheOutdir
14
+ } from "../services/module-loading/route-module-build-cache-registry.js";
15
+ import { resolveInternalExecutionDir } from "../utils/resolve-work-dir.js";
16
+ import {
17
+ collectBuildInputContributors,
18
+ createBuildInputsFingerprint as createBuildInputsFingerprint2,
19
+ didBuildInputContributorChange,
20
+ hashAppConfigFile as hashAppConfigFile2,
21
+ haveBuildInputsChanged as haveBuildInputsChanged2
22
+ } from "../build/build-input-fingerprint.js";
23
+ function createRouteModuleStaticRenderCacheContext(appConfig) {
24
+ return {
25
+ configHash: hashAppConfigFile(appConfig),
26
+ buildInputsFingerprint: createBuildInputsFingerprint(appConfig)
27
+ };
28
+ }
29
+ function shouldResetStaticExportDirectory(appConfig, force = false) {
30
+ if (force) {
31
+ return true;
32
+ }
33
+ if (haveBuildInputsChanged(appConfig)) {
34
+ return true;
35
+ }
36
+ const routeModuleCache = getSharedRouteModuleBuildCache(getServerModuleBuildCacheOutdir(appConfig), appConfig);
37
+ return !routeModuleCache.isIncrementalStaticGenerationAvailable(
38
+ createRouteModuleStaticRenderCacheContext(appConfig)
39
+ );
40
+ }
41
+ const PRODUCTION_BUILD_CACHE_OUTDIRS = [".server-modules", ".server-route-modules"];
42
+ function clearProductionBuildCaches(appConfig) {
43
+ if (process.env.NODE_ENV !== "production") {
44
+ return;
45
+ }
46
+ const executionDir = resolveInternalExecutionDir(appConfig);
47
+ for (const subdir of PRODUCTION_BUILD_CACHE_OUTDIRS) {
48
+ const manifestPath = path.join(executionDir, subdir, ROUTE_MODULE_BUILD_CACHE_FILENAME);
49
+ if (fileSystem.exists(manifestPath)) {
50
+ fileSystem.remove(manifestPath);
51
+ }
52
+ }
53
+ const serverEntryCachePath = path.join(executionDir, ".server-entry", ROUTE_MODULE_BUILD_CACHE_FILENAME);
54
+ if (fileSystem.exists(serverEntryCachePath)) {
55
+ fileSystem.remove(serverEntryCachePath);
56
+ }
57
+ const pagesGraphCachePath = path.join(executionDir, ".server-pages-graph", ROUTE_MODULE_BUILD_CACHE_FILENAME);
58
+ if (fileSystem.exists(pagesGraphCachePath)) {
59
+ fileSystem.remove(pagesGraphCachePath);
60
+ }
61
+ for (const cache of appConfig.runtime?.routeModuleBuildCaches?.values() ?? []) {
62
+ cache.resetMemory();
63
+ }
64
+ appConfig.runtime?.routeModuleBuildCaches?.clear();
65
+ if (appConfig.runtime) {
66
+ appConfig.runtime.serverEntryBuildExecutor = void 0;
67
+ }
68
+ }
69
+ export {
70
+ clearProductionBuildCaches,
71
+ collectBuildInputContributors,
72
+ createBuildInputsFingerprint2 as createBuildInputsFingerprint,
73
+ createRouteModuleStaticRenderCacheContext,
74
+ didBuildInputContributorChange,
75
+ hashAppConfigFile2 as hashAppConfigFile,
76
+ haveBuildInputsChanged2 as haveBuildInputsChanged,
77
+ shouldResetStaticExportDirectory
78
+ };
@@ -0,0 +1,22 @@
1
+ import type { EcoPagesAppConfig } from '../types/internal-types.js';
2
+ import type { StaticRoute } from '../types/public-types.js';
3
+ import type { StaticGenerationRendererResolver } from '../route-renderer/route-renderer.js';
4
+ import type { StaticGenerationRoute } from '../router/server/route-registry.js';
5
+ type StaticGenerationRouteSource = {
6
+ listStaticGenerationRoutes(input: {
7
+ runtimeOrigin: string;
8
+ }): Promise<readonly StaticGenerationRoute[]>;
9
+ };
10
+ /**
11
+ * Context passed to integration plugins during static export lifecycle hooks.
12
+ */
13
+ export interface StaticExportContext {
14
+ appConfig: EcoPagesAppConfig;
15
+ router: StaticGenerationRouteSource;
16
+ baseUrl: string;
17
+ routeRendererFactory?: StaticGenerationRendererResolver;
18
+ staticRoutes?: StaticRoute[];
19
+ force: boolean;
20
+ preserveExportDirectory: boolean;
21
+ }
22
+ export {};
@@ -2,6 +2,7 @@ import type { EcoPagesAppConfig } from '../types/internal-types.js';
2
2
  import type { StaticRoute } from '../types/public-types.js';
3
3
  import type { PageRendererResolver, StaticGenerationRendererResolver } from '../route-renderer/route-renderer.js';
4
4
  import type { StaticGenerationRoute } from '../router/server/route-registry.js';
5
+ import type { RouteModuleBuildCache } from '../services/module-loading/route-module-build-cache.store.js';
5
6
  type StaticGenerationRouteSource = {
6
7
  listStaticGenerationRoutes(input: {
7
8
  runtimeOrigin: string;
@@ -27,12 +28,17 @@ export declare const STATIC_SITE_GENERATOR_ERRORS: {
27
28
  */
28
29
  export declare class StaticSiteGenerator {
29
30
  appConfig: EcoPagesAppConfig;
31
+ private readonly routeModuleBuildCacheOverride?;
32
+ private staticRenderCacheContext;
33
+ private forceFullStaticGeneration;
30
34
  /**
31
35
  * Creates the static-site generator for one app config.
32
36
  */
33
- constructor({ appConfig }: {
37
+ constructor({ appConfig, routeModuleBuildCache, }: {
34
38
  appConfig: EcoPagesAppConfig;
39
+ routeModuleBuildCache?: RouteModuleBuildCache;
35
40
  });
41
+ private getRouteModuleBuildCache;
36
42
  private getExportDir;
37
43
  private shouldSkipStaticPageFile;
38
44
  private shouldSkipStaticView;
@@ -49,32 +55,35 @@ export declare class StaticSiteGenerator {
49
55
  */
50
56
  getDirectories(routes: string[]): string[];
51
57
  private writeStaticOutput;
52
- private getStaticBuildStrategy;
58
+ private writeStaticPageArtifact;
59
+ private pruneStaleStaticOutputs;
53
60
  private createFilesystemStaticContents;
54
61
  /**
55
62
  * Generates static output for all filesystem-discovered routes.
56
63
  *
57
64
  * @remarks
58
- * Routes whose integrations opt into fetch-based static builds are rendered by
59
- * issuing a request against the running server origin. Render-strategy routes
60
- * go through the normal route renderer directly.
65
+ * Routes are rendered through the normal route renderer directly.
61
66
  */
62
- generateStaticPages(router: StaticGenerationRouteSource, baseUrl: string, routeRendererFactory?: StaticPageRouteRendererFactory, skipped?: string[]): Promise<void>;
63
- private resolveStaticFetchUrl;
67
+ generateStaticPages(router: StaticGenerationRouteSource, baseUrl: string, routeRendererFactory?: StaticPageRouteRendererFactory, skipped?: string[], activeStaticPathnames?: Set<string>, preloadedRoutes?: readonly StaticGenerationRoute[]): Promise<void>;
68
+ private createStaticExportContext;
69
+ private invokeStaticExportHook;
64
70
  /**
65
71
  * Executes the full static-generation workflow for one app run.
66
72
  */
67
- run({ router, baseUrl, routeRendererFactory, staticRoutes, }: {
73
+ run({ router, baseUrl, routeRendererFactory, staticRoutes, force, preserveExportDirectory, }: {
68
74
  router: StaticGenerationRouteSource;
69
75
  baseUrl: string;
70
76
  routeRendererFactory?: StaticGenerationRendererFactory;
71
77
  staticRoutes?: StaticRoute[];
78
+ force?: boolean;
79
+ preserveExportDirectory?: boolean;
72
80
  }): Promise<void>;
73
81
  /**
74
82
  * Generates static pages from explicit static routes registered via app.static().
75
83
  * These routes use eco.page views via loader functions for HMR support.
76
84
  */
77
85
  private generateExplicitStaticPages;
86
+ private resolveExplicitViewSourceFile;
78
87
  private generateExplicitStaticRoute;
79
88
  private planExplicitStaticRoute;
80
89
  private createExplicitStaticContents;