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

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 (110) hide show
  1. package/package.json +27 -3
  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 +27 -2
  31. package/src/build/build-adapter.js +75 -32
  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 +7 -1
  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/rolldown-plugin-bridge.d.ts +10 -1
  47. package/src/build/rolldown-plugin-bridge.js +17 -5
  48. package/src/build/rolldown-source-transform-pass.d.ts +15 -0
  49. package/src/build/rolldown-source-transform-pass.js +58 -0
  50. package/src/build/runtime-build-executor.d.ts +7 -13
  51. package/src/build/runtime-build-executor.js +31 -7
  52. package/src/build/server-entry-build-cache.d.ts +44 -0
  53. package/src/build/server-entry-build-cache.js +169 -0
  54. package/src/config/config-builder.d.ts +11 -1
  55. package/src/config/config-builder.js +11 -1
  56. package/src/hmr/hmr.test.e2e.js +15 -4
  57. package/src/plugins/integration-plugin.d.ts +19 -8
  58. package/src/plugins/integration-plugin.js +7 -2
  59. package/src/plugins/processor.d.ts +5 -0
  60. package/src/plugins/processor.js +7 -0
  61. package/src/plugins/source-transform.d.ts +31 -0
  62. package/src/plugins/source-transform.js +23 -1
  63. package/src/route-renderer/GRAPH.md +4 -4
  64. package/src/route-renderer/README.md +6 -5
  65. package/src/route-renderer/orchestration/foreign-subtree-execution.service.js +1 -3
  66. package/src/route-renderer/orchestration/integration-renderer.d.ts +10 -2
  67. package/src/route-renderer/orchestration/integration-renderer.js +32 -12
  68. package/src/route-renderer/orchestration/render-output.utils.d.ts +2 -0
  69. package/src/route-renderer/orchestration/render-output.utils.js +4 -0
  70. package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +2 -2
  71. package/src/route-renderer/orchestration/route-render-orchestrator.js +17 -16
  72. package/src/router/client/navigation-coordinator.js +10 -0
  73. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.d.ts +1 -0
  74. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.js +1 -0
  75. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.d.ts +4 -2
  76. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.js +27 -7
  77. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +17 -3
  78. package/src/services/assets/browser-bundle.service.d.ts +3 -1
  79. package/src/services/assets/browser-bundle.service.js +14 -5
  80. package/src/services/html/html-transformer.service.d.ts +1 -1
  81. package/src/services/html/html-transformer.service.js +4 -4
  82. package/src/services/invalidation/development-invalidation.service.d.ts +10 -1
  83. package/src/services/invalidation/development-invalidation.service.js +29 -0
  84. package/src/services/module-loading/README.md +49 -0
  85. package/src/services/module-loading/app-server-module-transpiler.service.js +4 -17
  86. package/src/services/module-loading/page-module-import.service.d.ts +4 -1
  87. package/src/services/module-loading/page-module-import.service.js +52 -29
  88. package/src/services/module-loading/route-module-build-cache-registry.d.ts +24 -0
  89. package/src/services/module-loading/route-module-build-cache-registry.js +37 -0
  90. package/src/services/module-loading/route-module-build-cache.d.ts +4 -0
  91. package/src/services/module-loading/route-module-build-cache.js +38 -0
  92. package/src/services/module-loading/route-module-build-cache.store.d.ts +57 -0
  93. package/src/services/module-loading/route-module-build-cache.store.js +203 -0
  94. package/src/services/module-loading/route-module-build-manifest.d.ts +61 -0
  95. package/src/services/module-loading/route-module-build-manifest.js +124 -0
  96. package/src/services/module-loading/route-module-dependency-hasher.d.ts +81 -0
  97. package/src/services/module-loading/route-module-dependency-hasher.js +121 -0
  98. package/src/services/module-loading/server-module-transpiler.service.js +1 -1
  99. package/src/static-site-generator/README.md +33 -0
  100. package/src/static-site-generator/static-build-invalidation.d.ts +10 -0
  101. package/src/static-site-generator/static-build-invalidation.js +78 -0
  102. package/src/static-site-generator/static-export-context.d.ts +22 -0
  103. package/src/static-site-generator/static-export-context.js +0 -0
  104. package/src/static-site-generator/static-site-generator.d.ts +17 -8
  105. package/src/static-site-generator/static-site-generator.js +176 -62
  106. package/src/types/internal-types.d.ts +8 -0
  107. package/src/utils/parse-cli-args.d.ts +1 -0
  108. package/src/utils/parse-cli-args.js +3 -0
  109. package/src/watchers/project-watcher.d.ts +8 -0
  110. package/src/watchers/project-watcher.js +32 -12
@@ -39,8 +39,8 @@ export declare class RolldownDevBuildAdapter implements BuildAdapter {
39
39
  private readonly appRootRequireCache;
40
40
  /**
41
41
  * Stable key over the inputs that affect the engine configuration.
42
- * Plugin order is intentionally not part of the key: two plugin
43
- * sets with the same names are interchangeable for caching.
42
+ * Output paths and entrypoints must participate HMR registers many
43
+ * distinct script entrypoints with literal naming under the same outdir.
44
44
  */
45
45
  private getCacheKey;
46
46
  /**
@@ -8,7 +8,17 @@ import {
8
8
  toBuildLogs,
9
9
  transpileProfileToOptions
10
10
  } from "./rolldown-adapter-helpers.js";
11
+ import { recordRolldownBuildInvocation } from "./rolldown-build-invocation-metrics.js";
11
12
  const moduleRequire = createRequire(import.meta.url);
13
+ function normalizeEntrypointList(entrypoints) {
14
+ if (Array.isArray(entrypoints)) {
15
+ return entrypoints.map((entrypoint) => path.resolve(entrypoint)).sort();
16
+ }
17
+ if (entrypoints && typeof entrypoints === "object") {
18
+ return Object.values(entrypoints).map((entrypoint) => path.resolve(entrypoint)).sort();
19
+ }
20
+ return [];
21
+ }
12
22
  class RolldownDevBuildAdapter {
13
23
  /**
14
24
  * `'rolldown-dev'` (not `'rolldown'`) so {@link getBuildAdapterOwnership}
@@ -22,12 +32,22 @@ class RolldownDevBuildAdapter {
22
32
  appRootRequireCache = /* @__PURE__ */ new Map();
23
33
  /**
24
34
  * Stable key over the inputs that affect the engine configuration.
25
- * Plugin order is intentionally not part of the key: two plugin
26
- * sets with the same names are interchangeable for caching.
35
+ * Output paths and entrypoints must participate HMR registers many
36
+ * distinct script entrypoints with literal naming under the same outdir.
27
37
  */
28
- getCacheKey(plugins, contextRoot, target, format) {
29
- const pluginNames = plugins.map((p) => p.name).join(":");
30
- return `${contextRoot}::${pluginNames}::${target ?? "default"}::${format ?? "esm"}`;
38
+ getCacheKey(options, contextRoot) {
39
+ const pluginNames = (options.plugins ?? []).map((plugin) => plugin.name).sort().join(":");
40
+ const outdir = path.resolve(options.outdir ?? "dist/assets");
41
+ const entrypoints = normalizeEntrypointList(options.entrypoints).join("|");
42
+ return [
43
+ contextRoot,
44
+ pluginNames,
45
+ options.target ?? "default",
46
+ options.format ?? "esm",
47
+ outdir,
48
+ options.naming ?? "[name]",
49
+ entrypoints
50
+ ].join("::");
31
51
  }
32
52
  /**
33
53
  * Returns the cached engine, creating a new one when the cache
@@ -36,15 +56,9 @@ class RolldownDevBuildAdapter {
36
56
  */
37
57
  async getOrCreateEngine(options, contextRoot) {
38
58
  const bundlePlugins = options.plugins ?? [];
39
- const cacheKey = this.getCacheKey(bundlePlugins, contextRoot, options.target, options.format);
59
+ const cacheKey = this.getCacheKey(options, contextRoot);
40
60
  if (this.cachedEngine) {
41
- const existingKey = this.getCacheKey(
42
- this.cachedEngine.bundlePlugins,
43
- this.cachedEngine.contextRoot,
44
- options.target,
45
- options.format
46
- );
47
- if (existingKey === cacheKey) {
61
+ if (this.cachedEngine.cacheKey === cacheKey) {
48
62
  return this.cachedEngine;
49
63
  }
50
64
  await this.cachedEngine.engine.close();
@@ -73,7 +87,8 @@ class RolldownDevBuildAdapter {
73
87
  engine,
74
88
  engineStarted: false,
75
89
  contextRoot,
76
- bundlePlugins
90
+ bundlePlugins,
91
+ cacheKey
77
92
  };
78
93
  this.engineInstanceCount += 1;
79
94
  return this.cachedEngine;
@@ -108,6 +123,7 @@ class RolldownDevBuildAdapter {
108
123
  }
109
124
  /** Throws on error; see {@link build} for the no-throw variant. */
110
125
  async buildOrThrow(options) {
126
+ recordRolldownBuildInvocation("rolldown-dev");
111
127
  const contextRoot = options.root ? path.resolve(options.root) : process.cwd();
112
128
  const outdir = path.resolve(options.outdir ?? "dist/assets");
113
129
  const plugins = options.plugins ?? [];
@@ -40,6 +40,7 @@
40
40
  * on every source file.
41
41
  */
42
42
  import type { Plugin } from 'rolldown';
43
+ import type { EcoSourceTransform } from '../plugins/source-transform.js';
43
44
  import type { EcoBuildPlugin } from './build-types.js';
44
45
  /**
45
46
  * Creates a Rolldown `Plugin` array that drives the supplied
@@ -55,5 +56,13 @@ import type { EcoBuildPlugin } from './build-types.js';
55
56
  * Plugin ordering is preserved: registrations from earlier eco plugins
56
57
  * are checked before registrations from later ones, matching the
57
58
  * original per-plugin priority semantics.
59
+ *
60
+ * @param plugins - `EcoBuildPlugin` instances registered for this build.
61
+ * @param contextRoot - Project root used to resolve relative load paths.
62
+ * @param sourceTransforms - Optional app-owned transforms applied after a matching
63
+ * `onLoad` handler returns module contents. Browser builds pass
64
+ * {@link getAppSourceTransforms | app source transforms} here so metadata injection
65
+ * still runs on output rewritten by boundary/runtime plugins. Virtual modules,
66
+ * CSS, and asset loads are skipped.
58
67
  */
59
- export declare function createRolldownPluginBridge(plugins: EcoBuildPlugin[], contextRoot: string): Plugin[];
68
+ export declare function createRolldownPluginBridge(plugins: EcoBuildPlugin[], contextRoot: string, sourceTransforms?: readonly EcoSourceTransform[]): Plugin[];
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { finalizeLoadResultWithSourceTransforms } from "./rolldown-source-transform-pass.js";
2
3
  import { escapeRegExp } from "./browser-runtime-plugin-helpers.js";
3
4
  const NAMESPACE_SEPARATOR = ":";
4
5
  function joinNamespace(namespace, value) {
@@ -124,7 +125,7 @@ function convertPluginOnResolveResult(result, importer, contextRoot) {
124
125
  }
125
126
  return partial;
126
127
  }
127
- function createRolldownPluginBridge(plugins, contextRoot) {
128
+ function createRolldownPluginBridge(plugins, contextRoot, sourceTransforms = []) {
128
129
  if (plugins.length === 0) {
129
130
  return [];
130
131
  }
@@ -152,18 +153,29 @@ function createRolldownPluginBridge(plugins, contextRoot) {
152
153
  return void 0;
153
154
  };
154
155
  const loadHandler = async (id) => {
156
+ let loadResult;
155
157
  for (const { filter, callback } of loadRegistrations) {
156
158
  if (!filter.test(id)) {
157
159
  continue;
158
160
  }
159
- const { namespace, path: sourcePath } = splitNamespace(id);
160
- const result = await callback({ path: sourcePath, namespace });
161
+ const { namespace: namespace2, path: sourcePath2 } = splitNamespace(id);
162
+ const result = await callback({ path: sourcePath2, namespace: namespace2 });
161
163
  const converted = convertPluginOnLoadResult({ id }, result);
162
164
  if (converted !== void 0) {
163
- return converted;
165
+ loadResult = converted;
166
+ break;
164
167
  }
165
168
  }
166
- return void 0;
169
+ const { namespace, path: sourcePath } = splitNamespace(id);
170
+ return finalizeLoadResultWithSourceTransforms({
171
+ id,
172
+ namespace,
173
+ sourcePath,
174
+ loadResult,
175
+ sourceTransforms,
176
+ contextRoot,
177
+ inferModuleTypeFromPath: (filePath) => inferRolldownModuleTypeFromPath(filePath)
178
+ });
167
179
  };
168
180
  const plugin = {
169
181
  name: "ecopages-plugin-bridge",
@@ -0,0 +1,15 @@
1
+ import type { LoadResult, SourceDescription } from 'rolldown';
2
+ import { type EcoSourceTransform } from '../plugins/source-transform.js';
3
+ /**
4
+ * Runs app-owned source transforms after first-wins `onLoad` plugins produce
5
+ * module contents for one Rolldown load request.
6
+ */
7
+ export declare function finalizeLoadResultWithSourceTransforms(options: {
8
+ id: string;
9
+ namespace: string | undefined;
10
+ sourcePath: string;
11
+ loadResult: LoadResult | undefined;
12
+ sourceTransforms: readonly EcoSourceTransform[];
13
+ contextRoot: string;
14
+ inferModuleTypeFromPath: (filePath: string) => SourceDescription['moduleType'];
15
+ }): Promise<LoadResult | undefined>;
@@ -0,0 +1,58 @@
1
+ import path from "node:path";
2
+ import { fileSystem } from "@ecopages/file-system";
3
+ import { applySourceTransforms, normalizeTransformId } from "../plugins/source-transform.js";
4
+ const TRANSFORMABLE_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mdx"]);
5
+ function shouldApplySourceTransforms(namespace, sourcePath) {
6
+ if (namespace !== void 0) {
7
+ return false;
8
+ }
9
+ const normalizedPath = normalizeTransformId(sourcePath);
10
+ return TRANSFORMABLE_SOURCE_EXTENSIONS.has(path.extname(normalizedPath).toLowerCase());
11
+ }
12
+ function shouldTransformLoadedModule(moduleType) {
13
+ return moduleType !== "css" && moduleType !== "asset";
14
+ }
15
+ function applyTransformsToLoadedSource(sourceTransforms, id, sourcePath, code) {
16
+ if (sourceTransforms.length === 0) {
17
+ return code;
18
+ }
19
+ return applySourceTransforms(sourceTransforms, code, sourcePath);
20
+ }
21
+ async function finalizeLoadResultWithSourceTransforms(options) {
22
+ const { id, namespace, sourcePath, loadResult, sourceTransforms, contextRoot, inferModuleTypeFromPath } = options;
23
+ if (!shouldApplySourceTransforms(namespace, sourcePath)) {
24
+ return loadResult;
25
+ }
26
+ if (typeof loadResult === "object" && loadResult !== null && "code" in loadResult && typeof loadResult.code === "string") {
27
+ if (!shouldTransformLoadedModule(loadResult.moduleType)) {
28
+ return loadResult;
29
+ }
30
+ const transformedCode2 = applyTransformsToLoadedSource(sourceTransforms, id, sourcePath, loadResult.code);
31
+ if (transformedCode2 === loadResult.code) {
32
+ return loadResult;
33
+ }
34
+ return {
35
+ code: transformedCode2,
36
+ moduleType: loadResult.moduleType
37
+ };
38
+ }
39
+ if (loadResult !== void 0 || sourceTransforms.length === 0) {
40
+ return loadResult;
41
+ }
42
+ const normalizedPath = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(contextRoot, sourcePath);
43
+ if (!fileSystem.exists(normalizedPath)) {
44
+ return loadResult;
45
+ }
46
+ const originalCode = fileSystem.readFileSync(normalizedPath);
47
+ const transformedCode = applyTransformsToLoadedSource(sourceTransforms, id, sourcePath, originalCode);
48
+ if (transformedCode === originalCode) {
49
+ return loadResult;
50
+ }
51
+ return {
52
+ code: transformedCode,
53
+ moduleType: inferModuleTypeFromPath(normalizedPath)
54
+ };
55
+ }
56
+ export {
57
+ finalizeLoadResultWithSourceTransforms
58
+ };
@@ -1,21 +1,15 @@
1
1
  import type { EcoPagesAppConfig } from '../types/internal-types.js';
2
2
  import { type BuildExecutor } from './build-adapter.js';
3
3
  /**
4
- * Installs the app-owned runtime build executor for one app instance.
4
+ * Installs the app-owned runtime build executors for one app instance.
5
5
  *
6
6
  * @remarks
7
- * Wraps the app-owned adapter in a {@link SerializedBuildExecutor} so
8
- * every dev-watch caller issues builds against a single FIFO queue.
9
- * Plugin injection is applied here via {@link withBuildExecutorPlugins}
10
- * so app-owned plugins are merged into every rebuild without callers
11
- * having to know about the manifest.
7
+ * Route-module and HMR browser builds run through a {@link ParallelBuildExecutor}
8
+ * so independent compiles can overlap. Server-entry bundling continues to call
9
+ * the raw adapter directly and stays single-flight by design.
12
10
  *
13
- * Idempotent across calls: re-invoking replaces the existing executor
14
- * on `appConfig.runtime` with a fresh wrapper.
15
- *
16
- * @param appConfig - The app config whose runtime state is updated.
17
- * The function reads the existing executor (falling back to the
18
- * app-owned adapter) and writes the wrapped executor back.
19
- * @returns The installed {@link BuildExecutor}.
11
+ * Idempotent across calls: re-invoking replaces the existing executors on
12
+ * `appConfig.runtime` with fresh wrappers.
20
13
  */
21
14
  export declare function installAppRuntimeBuildExecutor(appConfig: EcoPagesAppConfig): BuildExecutor;
15
+ export declare function getInstalledServerEntryBuildExecutor(appConfig: EcoPagesAppConfig): BuildExecutor;
@@ -1,19 +1,43 @@
1
+ import { availableParallelism } from "node:os";
1
2
  import {
2
3
  getAppBuildAdapter,
3
- getAppBuildExecutor,
4
4
  getAppServerBuildPlugins,
5
5
  setAppBuildExecutor,
6
+ setAppHmrBuildExecutor,
7
+ setAppRouteModuleBuildExecutor,
6
8
  withBuildExecutorPlugins
7
9
  } from "./build-adapter.js";
10
+ import { ParallelBuildExecutor } from "./parallel-build-executor.js";
8
11
  import { SerializedBuildExecutor } from "./serialized-build-executor.js";
12
+ function createPluginWrappedExecutor(appConfig) {
13
+ return withBuildExecutorPlugins(getAppBuildAdapter(appConfig), () => getAppServerBuildPlugins(appConfig));
14
+ }
15
+ function resolveParallelismLimit() {
16
+ return Math.max(1, availableParallelism() - 1);
17
+ }
9
18
  function installAppRuntimeBuildExecutor(appConfig) {
10
- const baseExecutor = getAppBuildExecutor(appConfig) ?? getAppBuildAdapter(appConfig);
11
- const buildExecutor = new SerializedBuildExecutor(
12
- withBuildExecutorPlugins(baseExecutor, () => getAppServerBuildPlugins(appConfig))
13
- );
14
- setAppBuildExecutor(appConfig, buildExecutor);
15
- return buildExecutor;
19
+ const pluginWrappedExecutor = createPluginWrappedExecutor(appConfig);
20
+ const parallelismLimit = resolveParallelismLimit();
21
+ const routeModuleBuildExecutor = new ParallelBuildExecutor(pluginWrappedExecutor, parallelismLimit);
22
+ const hmrBuildExecutor = new ParallelBuildExecutor(pluginWrappedExecutor, Math.min(3, parallelismLimit));
23
+ setAppRouteModuleBuildExecutor(appConfig, routeModuleBuildExecutor);
24
+ setAppHmrBuildExecutor(appConfig, hmrBuildExecutor);
25
+ setAppBuildExecutor(appConfig, routeModuleBuildExecutor);
26
+ return routeModuleBuildExecutor;
27
+ }
28
+ function getInstalledServerEntryBuildExecutor(appConfig) {
29
+ const existing = appConfig.runtime?.serverEntryBuildExecutor;
30
+ if (existing) {
31
+ return existing;
32
+ }
33
+ const serverEntryBuildExecutor = new SerializedBuildExecutor(createPluginWrappedExecutor(appConfig));
34
+ appConfig.runtime = {
35
+ ...appConfig.runtime ?? {},
36
+ serverEntryBuildExecutor
37
+ };
38
+ return serverEntryBuildExecutor;
16
39
  }
17
40
  export {
41
+ getInstalledServerEntryBuildExecutor,
18
42
  installAppRuntimeBuildExecutor
19
43
  };
@@ -0,0 +1,44 @@
1
+ import type { BuildResult } from './build-adapter.js';
2
+ import type { EcoPagesAppConfig } from '../types/internal-types.js';
3
+ /** @deprecated Use {@link ROUTE_MODULE_BUILD_CACHE_FILENAME}; both caches share the same manifest filename. */
4
+ export declare const SERVER_ENTRY_BUILD_CACHE_FILENAME = ".build-cache.json";
5
+ export declare const SERVER_BUNDLE_MANIFEST_FILENAME = "manifest.json";
6
+ export interface ServerBundleDeployManifest {
7
+ serverEntry: string;
8
+ distDir: string;
9
+ builtAt: number;
10
+ }
11
+ export interface ServerEntryBuildCacheManifest {
12
+ invalidationVersion: string;
13
+ entryPath: string;
14
+ entryHash: string;
15
+ buildInputsFingerprint: string;
16
+ buildKey: string;
17
+ outputPaths: string[];
18
+ dependencyHashes: Record<string, string>;
19
+ builtAt: number;
20
+ }
21
+ export interface ServerEntryBuildCacheLookup {
22
+ manifest: ServerEntryBuildCacheManifest;
23
+ outputPaths: string[];
24
+ }
25
+ export declare function getServerBundleOutputPaths(appConfig: EcoPagesAppConfig): {
26
+ distDir: string;
27
+ serverOutdir: string;
28
+ serverEntryPath: string;
29
+ manifestPath: string;
30
+ };
31
+ export declare function readServerEntryBuildCacheManifest(appConfig: EcoPagesAppConfig): ServerEntryBuildCacheManifest | undefined;
32
+ export declare function writeServerBundleDeployManifest(appConfig: EcoPagesAppConfig, serverEntryPath: string): void;
33
+ export declare function lookupServerEntryBuildCache(options: {
34
+ appConfig: EcoPagesAppConfig;
35
+ entryPath: string;
36
+ force?: boolean;
37
+ }): ServerEntryBuildCacheLookup | undefined;
38
+ export declare function recordServerEntryBuildCache(options: {
39
+ appConfig: EcoPagesAppConfig;
40
+ entryPath: string;
41
+ buildResult: BuildResult;
42
+ outputPaths: string[];
43
+ }): void;
44
+ export declare function resolveProductionServerEntry(cwd?: string): string | undefined;
@@ -0,0 +1,169 @@
1
+ import path from "node:path";
2
+ import { fileSystem } from "@ecopages/file-system";
3
+ import { getAppServerBuildPlugins } from "./build-adapter.js";
4
+ import { createBuildInputsFingerprint } from "./build-input-fingerprint.js";
5
+ import { getCorePackageVersion } from "../services/module-loading/route-module-build-manifest.js";
6
+ import {
7
+ RouteModuleDependencyHasher,
8
+ createRouteModuleDependencyHashes
9
+ } from "../services/module-loading/route-module-dependency-hasher.js";
10
+ import { resolveInternalExecutionDir } from "../utils/resolve-work-dir.js";
11
+ import { SERVER_BUNDLE_DIR, SERVER_BUNDLE_FILENAME } from "../utils/resolve-entry-file.js";
12
+ import { ROUTE_MODULE_BUILD_CACHE_FILENAME } from "../services/module-loading/route-module-build-manifest.js";
13
+ const SERVER_ENTRY_BUILD_CACHE_FILENAME = ROUTE_MODULE_BUILD_CACHE_FILENAME;
14
+ const SERVER_BUNDLE_MANIFEST_FILENAME = "manifest.json";
15
+ function getServerEntryCacheDir(appConfig) {
16
+ return path.join(resolveInternalExecutionDir(appConfig), ".server-entry");
17
+ }
18
+ function getServerEntryCacheManifestPath(appConfig) {
19
+ return path.join(getServerEntryCacheDir(appConfig), SERVER_ENTRY_BUILD_CACHE_FILENAME);
20
+ }
21
+ function hashDependencyGraph(entryPath, dependencyGraph, hasher, rootDir) {
22
+ if (!dependencyGraph) {
23
+ return { [path.resolve(entryPath)]: fileSystem.hash(entryPath) };
24
+ }
25
+ return createRouteModuleDependencyHashes(hasher, { dependencyGraph }, entryPath, rootDir);
26
+ }
27
+ function createServerEntryBuildKey(appConfig) {
28
+ try {
29
+ const plugins = getAppServerBuildPlugins(appConfig);
30
+ const pluginNames = plugins.map((plugin) => plugin.name).sort().join("|");
31
+ return ["node", "esm", "external-packages", pluginNames].join("::");
32
+ } catch {
33
+ return "node::esm::external-packages";
34
+ }
35
+ }
36
+ function getServerBundleOutputPaths(appConfig) {
37
+ const distDir = appConfig.absolutePaths?.distDir ?? path.join(appConfig.rootDir, appConfig.distDir);
38
+ const serverOutdir = path.join(distDir, SERVER_BUNDLE_DIR);
39
+ const serverEntryPath = path.join(serverOutdir, SERVER_BUNDLE_FILENAME);
40
+ const manifestPath = path.join(serverOutdir, SERVER_BUNDLE_MANIFEST_FILENAME);
41
+ return { distDir, serverOutdir, serverEntryPath, manifestPath };
42
+ }
43
+ function readServerEntryBuildCacheManifest(appConfig) {
44
+ const manifestPath = getServerEntryCacheManifestPath(appConfig);
45
+ if (!fileSystem.exists(manifestPath)) {
46
+ return void 0;
47
+ }
48
+ try {
49
+ const parsed = JSON.parse(fileSystem.readFileSync(manifestPath));
50
+ if (!parsed || typeof parsed !== "object") {
51
+ return void 0;
52
+ }
53
+ return parsed;
54
+ } catch {
55
+ return void 0;
56
+ }
57
+ }
58
+ function writeServerEntryBuildCacheManifest(appConfig, manifest) {
59
+ const cacheDir = getServerEntryCacheDir(appConfig);
60
+ fileSystem.ensureDir(cacheDir);
61
+ fileSystem.write(
62
+ path.join(cacheDir, SERVER_ENTRY_BUILD_CACHE_FILENAME),
63
+ `${JSON.stringify(manifest, null, " ")}
64
+ `
65
+ );
66
+ }
67
+ function writeServerBundleDeployManifest(appConfig, serverEntryPath) {
68
+ const { distDir, manifestPath } = getServerBundleOutputPaths(appConfig);
69
+ fileSystem.ensureDir(path.dirname(manifestPath));
70
+ const payload = {
71
+ serverEntry: path.relative(path.dirname(manifestPath), serverEntryPath) || SERVER_BUNDLE_FILENAME,
72
+ distDir,
73
+ builtAt: Date.now()
74
+ };
75
+ fileSystem.write(manifestPath, `${JSON.stringify(payload, null, " ")}
76
+ `);
77
+ }
78
+ function lookupServerEntryBuildCache(options) {
79
+ if (options.force) {
80
+ return void 0;
81
+ }
82
+ if (process.env.NODE_ENV !== "production") {
83
+ return void 0;
84
+ }
85
+ const entryPath = path.resolve(options.entryPath);
86
+ const entryHash = fileSystem.hash(entryPath);
87
+ const buildInputsFingerprint = createBuildInputsFingerprint(options.appConfig);
88
+ const buildKey = createServerEntryBuildKey(options.appConfig);
89
+ const manifest = readServerEntryBuildCacheManifest(options.appConfig);
90
+ if (!manifest) {
91
+ return void 0;
92
+ }
93
+ if (manifest.invalidationVersion !== getCorePackageVersion()) {
94
+ return void 0;
95
+ }
96
+ if (manifest.entryPath !== entryPath || manifest.entryHash !== entryHash || manifest.buildInputsFingerprint !== buildInputsFingerprint || manifest.buildKey !== buildKey) {
97
+ return void 0;
98
+ }
99
+ const hasher = new RouteModuleDependencyHasher();
100
+ if (!hasher.matchesStoredHashes(manifest.dependencyHashes, entryPath, entryHash)) {
101
+ return void 0;
102
+ }
103
+ const existingOutputs = manifest.outputPaths.filter((outputPath) => fileSystem.exists(outputPath));
104
+ if (existingOutputs.length === 0) {
105
+ return void 0;
106
+ }
107
+ return {
108
+ manifest,
109
+ outputPaths: existingOutputs
110
+ };
111
+ }
112
+ function recordServerEntryBuildCache(options) {
113
+ if (process.env.NODE_ENV !== "production") {
114
+ return;
115
+ }
116
+ const entryPath = path.resolve(options.entryPath);
117
+ const entryHash = fileSystem.hash(entryPath);
118
+ const hasher = new RouteModuleDependencyHasher();
119
+ const dependencyHashes = hashDependencyGraph(
120
+ entryPath,
121
+ options.buildResult.dependencyGraph,
122
+ hasher,
123
+ options.appConfig.rootDir
124
+ );
125
+ const manifest = {
126
+ invalidationVersion: getCorePackageVersion(),
127
+ entryPath,
128
+ entryHash,
129
+ buildInputsFingerprint: createBuildInputsFingerprint(options.appConfig),
130
+ buildKey: createServerEntryBuildKey(options.appConfig),
131
+ outputPaths: options.outputPaths,
132
+ dependencyHashes,
133
+ builtAt: Date.now()
134
+ };
135
+ writeServerEntryBuildCacheManifest(options.appConfig, manifest);
136
+ }
137
+ function resolveProductionServerEntry(cwd = process.cwd()) {
138
+ const manifestPath = path.join(cwd, "dist", SERVER_BUNDLE_DIR, SERVER_BUNDLE_MANIFEST_FILENAME);
139
+ if (fileSystem.exists(manifestPath)) {
140
+ try {
141
+ const parsed = JSON.parse(fileSystem.readFileSync(manifestPath));
142
+ if (parsed?.serverEntry) {
143
+ const fromManifest = path.isAbsolute(parsed.serverEntry) ? parsed.serverEntry : path.join(path.dirname(manifestPath), parsed.serverEntry);
144
+ if (fileSystem.exists(fromManifest)) {
145
+ return fromManifest;
146
+ }
147
+ }
148
+ if (parsed?.distDir) {
149
+ const fromDistDir = path.join(parsed.distDir, SERVER_BUNDLE_DIR, SERVER_BUNDLE_FILENAME);
150
+ if (fileSystem.exists(fromDistDir)) {
151
+ return fromDistDir;
152
+ }
153
+ }
154
+ } catch {
155
+ }
156
+ }
157
+ const legacyPath = path.join(cwd, "dist", SERVER_BUNDLE_DIR, SERVER_BUNDLE_FILENAME);
158
+ return fileSystem.exists(legacyPath) ? legacyPath : void 0;
159
+ }
160
+ export {
161
+ SERVER_BUNDLE_MANIFEST_FILENAME,
162
+ SERVER_ENTRY_BUILD_CACHE_FILENAME,
163
+ getServerBundleOutputPaths,
164
+ lookupServerEntryBuildCache,
165
+ readServerEntryBuildCacheManifest,
166
+ recordServerEntryBuildCache,
167
+ resolveProductionServerEntry,
168
+ writeServerBundleDeployManifest
169
+ };
@@ -250,7 +250,17 @@ export declare class ConfigBuilder {
250
250
  private compareVersions;
251
251
  /**
252
252
  * Initializes default loaders that are required for EcoPages to function.
253
- * This includes the eco-component-meta-plugin which auto-injects __eco metadata into component configs.
253
+ *
254
+ * @remarks
255
+ * `eco-component-meta` is registered twice on purpose:
256
+ *
257
+ * - `sourceTransforms` is the canonical browser/HMR path. The Rolldown bridge
258
+ * runs these after first-wins `onLoad` plugins rewrite module source.
259
+ * - `loaders` keeps the same transform available to server-oriented builds
260
+ * that still use competing `onLoad` handlers directly.
261
+ *
262
+ * Browser builds exclude the loader copy in {@link getAppBrowserBuildPlugins}
263
+ * when the transform name is already present in `sourceTransforms`.
254
264
  */
255
265
  private initializeDefaultLoaders;
256
266
  private reviewBaseUrl;
@@ -547,7 +547,17 @@ class ConfigBuilder {
547
547
  }
548
548
  /**
549
549
  * Initializes default loaders that are required for EcoPages to function.
550
- * This includes the eco-component-meta-plugin which auto-injects __eco metadata into component configs.
550
+ *
551
+ * @remarks
552
+ * `eco-component-meta` is registered twice on purpose:
553
+ *
554
+ * - `sourceTransforms` is the canonical browser/HMR path. The Rolldown bridge
555
+ * runs these after first-wins `onLoad` plugins rewrite module source.
556
+ * - `loaders` keeps the same transform available to server-oriented builds
557
+ * that still use competing `onLoad` handlers directly.
558
+ *
559
+ * Browser builds exclude the loader copy in {@link getAppBrowserBuildPlugins}
560
+ * when the transform name is already present in `sourceTransforms`.
551
561
  */
552
562
  async initializeDefaultLoaders() {
553
563
  const componentMetaTransform = createEcoComponentMetaTransform({ config: this.config });
@@ -4,6 +4,15 @@ import { writeFile } from "node:fs/promises";
4
4
  import { resolve } from "node:path";
5
5
  const FIXTURE_DIR = resolve(process.cwd(), "packages/core/__fixtures__/app");
6
6
  const TEST_CSS_FILE = resolve(FIXTURE_DIR, "src/pages/index.css");
7
+ async function readMainTitleColor(page) {
8
+ return page.evaluate(() => {
9
+ const element = document.querySelector(".main-title");
10
+ if (!(element instanceof HTMLElement)) {
11
+ return "";
12
+ }
13
+ return getComputedStyle(element).color;
14
+ });
15
+ }
7
16
  test.describe("HMR E2E", () => {
8
17
  test("should load page with .main-title element", async ({ page }) => {
9
18
  await page.goto("/", { waitUntil: "networkidle" });
@@ -23,9 +32,10 @@ test.describe("HMR E2E", () => {
23
32
  const originalCss = readFileSync(TEST_CSS_FILE, "utf-8");
24
33
  const title = page.locator(".main-title").first();
25
34
  try {
26
- await page.goto("/", { waitUntil: "networkidle" });
35
+ await page.goto("/", { waitUntil: "domcontentloaded" });
27
36
  await expect(title).toBeVisible();
28
- const initialColor = await title.evaluate((el) => getComputedStyle(el).color);
37
+ await expect.poll(async () => readMainTitleColor(page), { timeout: 1e4 }).not.toBe("");
38
+ const initialColor = await readMainTitleColor(page);
29
39
  expect(initialColor).toBeTruthy();
30
40
  const modifiedCss = originalCss.replace(".main-title {", ".main-title {\n color: rgb(255, 0, 0);");
31
41
  const reloadPromise = page.waitForEvent("framenavigated", {
@@ -34,8 +44,9 @@ test.describe("HMR E2E", () => {
34
44
  });
35
45
  await writeFile(TEST_CSS_FILE, modifiedCss, { flush: true });
36
46
  await reloadPromise;
37
- await page.waitForLoadState("networkidle");
38
- await expect.poll(async () => title.evaluate((el) => getComputedStyle(el).color), { timeout: 1e4 }).toBe("rgb(255, 0, 0)");
47
+ await page.waitForLoadState("domcontentloaded");
48
+ await expect(title).toBeVisible();
49
+ await expect.poll(async () => readMainTitleColor(page), { timeout: 1e4 }).toBe("rgb(255, 0, 0)");
39
50
  } finally {
40
51
  await writeFile(TEST_CSS_FILE, originalCss, { flush: true });
41
52
  }