@ecopages/core 0.2.0-beta.17 → 0.2.0-beta.18

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-beta.17",
3
+ "version": "0.2.0-beta.18",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -17,7 +17,7 @@
17
17
  "directory": "packages/core"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-beta.17",
20
+ "@ecopages/file-system": "0.2.0-beta.18",
21
21
  "@ecopages/logger": "^0.2.3",
22
22
  "@ecopages/scripts-injector": "^0.1.5",
23
23
  "@oxc-project/runtime": "0.134.0",
@@ -133,6 +133,10 @@
133
133
  "types": "./src/dev/client-bridge-registry.d.ts",
134
134
  "default": "./src/dev/client-bridge-registry.js"
135
135
  },
136
+ "./dev/hmr-manager-registry": {
137
+ "types": "./src/dev/hmr-manager-registry.d.ts",
138
+ "default": "./src/dev/hmr-manager-registry.js"
139
+ },
136
140
  "./dev/dev-client-ownership": {
137
141
  "types": "./src/dev/dev-client-ownership.d.ts",
138
142
  "default": "./src/dev/dev-client-ownership.js"
@@ -338,6 +342,10 @@
338
342
  "types": "./src/dev/client-bridge-registry.d.ts",
339
343
  "default": "./src/dev/client-bridge-registry.js"
340
344
  },
345
+ "./dev/hmr-manager-registry.ts": {
346
+ "types": "./src/dev/hmr-manager-registry.d.ts",
347
+ "default": "./src/dev/hmr-manager-registry.js"
348
+ },
341
349
  "./dev/dev-client-ownership.ts": {
342
350
  "types": "./src/dev/dev-client-ownership.d.ts",
343
351
  "default": "./src/dev/dev-client-ownership.js"
@@ -24,6 +24,7 @@ import {
24
24
  } from "../shared/runtime-server-lifecycle.js";
25
25
  import { ClientBridge } from "./client-bridge.js";
26
26
  import { setAppDevClientBridge } from "../../dev/client-bridge-registry.js";
27
+ import { setAppHmrManager } from "../../dev/hmr-manager-registry.js";
27
28
  import { HmrManager } from "./hmr-manager.js";
28
29
  import { BunStaticPreviewHost } from "./static-preview-host.js";
29
30
  class BunServerAdapter extends SharedServerAdapter {
@@ -600,6 +601,7 @@ async function createBunServerAdapter(params) {
600
601
  const bridge = params.bridge ?? new ClientBridge();
601
602
  const hmrManager = params.hmrManager ?? new HmrManager({ appConfig: params.appConfig, bridge });
602
603
  setAppDevClientBridge(params.appConfig, bridge);
604
+ setAppHmrManager(params.appConfig, hmrManager);
603
605
  const previewHost = params.previewHost ?? new BunStaticPreviewHost();
604
606
  const adapter = new BunServerAdapter({
605
607
  ...params,
@@ -1,5 +1,6 @@
1
1
  import { WebSocketServer } from "ws";
2
2
  import { setAppDevClientBridge } from "../../dev/client-bridge-registry.js";
3
+ import { setAppHmrManager } from "../../dev/hmr-manager-registry.js";
3
4
  import { NodeClientBridge } from "./node-client-bridge.js";
4
5
  import { NodeHmrManager } from "./node-hmr-manager.js";
5
6
  class DefaultNodeServerDevRuntimeFactory {
@@ -8,6 +9,7 @@ class DefaultNodeServerDevRuntimeFactory {
8
9
  const bridge = new NodeClientBridge();
9
10
  const hmrManager = new NodeHmrManager({ appConfig: options.appConfig, bridge });
10
11
  setAppDevClientBridge(options.appConfig, bridge);
12
+ setAppHmrManager(options.appConfig, hmrManager);
11
13
  return {
12
14
  websocketServer,
13
15
  bridge,
@@ -50,6 +50,4 @@ export declare class HmrEntrypointRegistrar {
50
50
  private registerEntrypointInternal;
51
51
  private awaitEntrypointRegistration;
52
52
  private getEntrypointOutput;
53
- private removeStaleEntrypointOutput;
54
- private encodeDynamicSegments;
55
53
  }
@@ -1,7 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileSystem } from "@ecopages/file-system";
3
- import { appLogger } from "../../global/app-logger.js";
4
- import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
3
+ import { removeStaleHmrEntrypointOutput, resolveHmrEntrypointOutputPaths } from "../../hmr/hmr-entrypoint-output.js";
5
4
  class HmrEntrypointRegistrar {
6
5
  options;
7
6
  constructor(options) {
@@ -36,7 +35,7 @@ class HmrEntrypointRegistrar {
36
35
  }
37
36
  const { outputPath, outputUrl } = this.getEntrypointOutput(entrypointPath);
38
37
  this.options.watchedFiles.set(entrypointPath, outputUrl);
39
- this.removeStaleEntrypointOutput(outputPath);
38
+ removeStaleHmrEntrypointOutput(outputPath, "HMR");
40
39
  await registrationOptions.emit(entrypointPath, outputPath);
41
40
  if (!fileSystem.exists(outputPath)) {
42
41
  throw registrationOptions.getMissingOutputError(entrypointPath, outputPath);
@@ -57,29 +56,7 @@ class HmrEntrypointRegistrar {
57
56
  ]);
58
57
  }
59
58
  getEntrypointOutput(entrypointPath) {
60
- const relativePath = path.relative(this.options.srcDir, entrypointPath);
61
- const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, ".js");
62
- const encodedPathJs = this.encodeDynamicSegments(relativePathJs);
63
- const urlPath = encodedPathJs.split(path.sep).join("/");
64
- return {
65
- outputUrl: `/${path.join(RESOLVED_ASSETS_DIR, "_hmr", urlPath)}`,
66
- outputPath: path.join(this.options.distDir, urlPath)
67
- };
68
- }
69
- removeStaleEntrypointOutput(outputPath) {
70
- if (!fileSystem.exists(outputPath)) {
71
- return;
72
- }
73
- try {
74
- fileSystem.remove(outputPath);
75
- } catch (error) {
76
- appLogger.warn(
77
- `[HMR] Failed to remove stale entrypoint output ${outputPath}: ${error instanceof Error ? error.message : String(error)}`
78
- );
79
- }
80
- }
81
- encodeDynamicSegments(filepath) {
82
- return filepath.replace(/\[([^\]]+)\]/g, "_$1_");
59
+ return resolveHmrEntrypointOutputPaths(this.options.srcDir, this.options.distDir, entrypointPath);
83
60
  }
84
61
  }
85
62
  export {
@@ -4,6 +4,8 @@ import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
4
4
  import { disposeAppBuildRuntime } from "../../build/build-runtime.js";
5
5
  import { getAppBrowserBuildPlugins } from "../../build/build-adapter.js";
6
6
  import { copyRuntimePublicDirIfChanged } from "./copy-runtime-public-dir.js";
7
+ import { clearAppDevClientBridge } from "../../dev/client-bridge-registry.js";
8
+ import { clearAppHmrManager } from "../../dev/hmr-manager-registry.js";
7
9
  import { injectHmrRuntimeIntoHtmlResponse, isHtmlResponse, shouldInjectHmrHtmlResponse } from "./hmr-html-response.js";
8
10
  function prepareRuntimePublicDir(appConfig) {
9
11
  const srcPublicDir = path.join(appConfig.rootDir, appConfig.srcDir, appConfig.publicDir);
@@ -29,6 +31,8 @@ async function disposeDevResources(options) {
29
31
  await disposeAppBuildRuntime(options.appConfig);
30
32
  options.hmrManager?.stop();
31
33
  options.bridge?.destroy();
34
+ clearAppDevClientBridge(options.appConfig);
35
+ clearAppHmrManager(options.appConfig);
32
36
  await options.previewHost.stop();
33
37
  }
34
38
  export {
@@ -47,10 +47,12 @@ export declare abstract class SharedHmrManager implements IHmrManager {
47
47
  handleFileChange(filePath: string, options?: HandleFileChangeOptions): Promise<void>;
48
48
  getOutputUrl(entrypointPath: string): string | undefined;
49
49
  /**
50
- * Returns the emitted HMR script output when the artifact already exists on disk.
50
+ * Returns the emitted HMR script output when the entrypoint is already registered
51
+ * and its browser bundle exists on disk.
51
52
  *
52
- * SSR must not block on entrypoint registration when a previous build already
53
- * produced the browser bundle.
53
+ * @remarks
54
+ * Disk artifacts alone are not enough: a fresh dev session must still register
55
+ * the entrypoint so file watchers can rebuild it on change.
54
56
  */
55
57
  getResolvedScriptOutput(entrypointPath: string): {
56
58
  outputUrl: string;
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
5
+ import { resolveHmrEntrypointOutputPaths } from "../../hmr/hmr-entrypoint-output.js";
5
6
  import { requireBuildRuntime } from "../../build/build-runtime.js";
6
7
  import { fileSystem } from "@ecopages/file-system";
7
8
  import { HmrStrategyType } from "../../hmr/hmr-strategy.js";
@@ -184,21 +185,27 @@ class SharedHmrManager {
184
185
  return this.watchedFiles.get(entrypointPath);
185
186
  }
186
187
  /**
187
- * Returns the emitted HMR script output when the artifact already exists on disk.
188
+ * Returns the emitted HMR script output when the entrypoint is already registered
189
+ * and its browser bundle exists on disk.
188
190
  *
189
- * SSR must not block on entrypoint registration when a previous build already
190
- * produced the browser bundle.
191
+ * @remarks
192
+ * Disk artifacts alone are not enough: a fresh dev session must still register
193
+ * the entrypoint so file watchers can rebuild it on change.
191
194
  */
192
195
  getResolvedScriptOutput(entrypointPath) {
193
196
  const normalizedEntrypoint = path.resolve(entrypointPath);
194
- const relativePath = path.relative(this.appConfig.absolutePaths.srcDir, normalizedEntrypoint);
195
- const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, ".js").replace(/\[([^\]]+)\]/g, "_$1_");
196
- const urlPath = relativePathJs.split(path.sep).join("/");
197
- const outputPath = path.join(this.distDir, urlPath);
197
+ if (!this.watchedFiles.has(normalizedEntrypoint)) {
198
+ return void 0;
199
+ }
200
+ const { outputPath, outputUrl: derivedOutputUrl } = resolveHmrEntrypointOutputPaths(
201
+ this.appConfig.absolutePaths.srcDir,
202
+ this.distDir,
203
+ normalizedEntrypoint
204
+ );
198
205
  if (!fileSystem.exists(outputPath)) {
199
206
  return void 0;
200
207
  }
201
- const outputUrl = this.watchedFiles.get(normalizedEntrypoint) ?? `/${path.join(RESOLVED_ASSETS_DIR, "_hmr", urlPath).split(path.sep).join("/")}`;
208
+ const outputUrl = this.watchedFiles.get(normalizedEntrypoint) ?? derivedOutputUrl;
202
209
  return { outputUrl, outputPath };
203
210
  }
204
211
  getWatchedFiles() {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Per-app registry for the shared HMR manager used by host-owned dev clients.
3
+ */
4
+ import type { EcoPagesAppConfig } from '../types/public-types.js';
5
+ import type { IHmrManager } from '../types/public-types.js';
6
+ export declare function setAppHmrManager(appConfig: EcoPagesAppConfig, hmrManager: IHmrManager): void;
7
+ export declare function getAppHmrManager(appConfig: EcoPagesAppConfig): IHmrManager | undefined;
8
+ export declare function clearAppHmrManager(appConfig: EcoPagesAppConfig): void;
@@ -0,0 +1,15 @@
1
+ const appHmrManagers = /* @__PURE__ */ new WeakMap();
2
+ function setAppHmrManager(appConfig, hmrManager) {
3
+ appHmrManagers.set(appConfig, hmrManager);
4
+ }
5
+ function getAppHmrManager(appConfig) {
6
+ return appHmrManagers.get(appConfig);
7
+ }
8
+ function clearAppHmrManager(appConfig) {
9
+ appHmrManagers.delete(appConfig);
10
+ }
11
+ export {
12
+ clearAppHmrManager,
13
+ getAppHmrManager,
14
+ setAppHmrManager
15
+ };
@@ -0,0 +1,14 @@
1
+ export declare function encodeHmrDynamicSegments(filepath: string): string;
2
+ /**
3
+ * Resolves the on-disk and browser URL targets for one HMR script entrypoint.
4
+ */
5
+ export declare function resolveHmrEntrypointOutputPaths(srcDir: string, distDir: string, entrypointPath: string): {
6
+ outputPath: string;
7
+ outputUrl: string;
8
+ };
9
+ /**
10
+ * @remarks
11
+ * Rebuilds must delete the previous artifact so browser-hmr does not reuse a
12
+ * stale bundle when the source file changed.
13
+ */
14
+ export declare function removeStaleHmrEntrypointOutput(outputPath: string, scope: string): void;
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+ import { fileSystem } from "@ecopages/file-system";
3
+ import { RESOLVED_ASSETS_DIR } from "../config/constants.js";
4
+ import { appLogger } from "../global/app-logger.js";
5
+ function encodeHmrDynamicSegments(filepath) {
6
+ return filepath.replace(/\[([^\]]+)\]/g, "_$1_");
7
+ }
8
+ function resolveHmrEntrypointOutputPaths(srcDir, distDir, entrypointPath) {
9
+ const normalizedEntrypoint = path.resolve(entrypointPath);
10
+ const relativePath = path.relative(srcDir, normalizedEntrypoint);
11
+ const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, ".js");
12
+ const encodedPathJs = encodeHmrDynamicSegments(relativePathJs);
13
+ const urlPath = encodedPathJs.split(path.sep).join("/");
14
+ return {
15
+ outputUrl: `/${path.join(RESOLVED_ASSETS_DIR, "_hmr", urlPath).split(path.sep).join("/")}`,
16
+ outputPath: path.join(distDir, urlPath)
17
+ };
18
+ }
19
+ function removeStaleHmrEntrypointOutput(outputPath, scope) {
20
+ if (!fileSystem.exists(outputPath)) {
21
+ return;
22
+ }
23
+ try {
24
+ fileSystem.remove(outputPath);
25
+ } catch (error) {
26
+ appLogger.warn(
27
+ `[${scope}] Failed to remove stale entrypoint output ${outputPath}: ${error instanceof Error ? error.message : String(error)}`
28
+ );
29
+ }
30
+ }
31
+ export {
32
+ encodeHmrDynamicSegments,
33
+ removeStaleHmrEntrypointOutput,
34
+ resolveHmrEntrypointOutputPaths
35
+ };
@@ -98,6 +98,7 @@ export declare class JsHmrStrategy extends HmrStrategy {
98
98
  * Matches if:
99
99
  * 1. There are registered entrypoints to rebuild
100
100
  * 2. The changed file is a JS/TS file in the src directory
101
+ * 3. Registered entrypoints always match, even when they share an integration template extension
101
102
  *
102
103
  * @param filePath - Absolute path to the changed file
103
104
  * @returns True if this file should trigger entrypoint rebuilds
@@ -116,6 +117,8 @@ export declare class JsHmrStrategy extends HmrStrategy {
116
117
  * @returns Action to broadcast update events
117
118
  */
118
119
  process(filePath: string): Promise<HmrAction>;
120
+ private resolveEntrypointOutputPath;
121
+ private removeStaleEntrypointOutput;
119
122
  /**
120
123
  * Bundles one or more entrypoints in a single build invocation.
121
124
  * Uses the source directory as the output base so that the directory structure
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { fileSystem } from "@ecopages/file-system";
3
3
  import { HmrStrategy, HmrStrategyType } from "../hmr-strategy.js";
4
4
  import { appLogger } from "../../global/app-logger.js";
5
+ import { removeStaleHmrEntrypointOutput, resolveHmrEntrypointOutputPaths } from "../hmr-entrypoint-output.js";
5
6
  class JsHmrStrategy extends HmrStrategy {
6
7
  type = HmrStrategyType.SCRIPT;
7
8
  context;
@@ -15,27 +16,29 @@ class JsHmrStrategy extends HmrStrategy {
15
16
  * Matches if:
16
17
  * 1. There are registered entrypoints to rebuild
17
18
  * 2. The changed file is a JS/TS file in the src directory
19
+ * 3. Registered entrypoints always match, even when they share an integration template extension
18
20
  *
19
21
  * @param filePath - Absolute path to the changed file
20
22
  * @returns True if this file should trigger entrypoint rebuilds
21
23
  */
22
24
  matches(filePath) {
23
25
  const watchedFiles = this.context.getWatchedFiles();
24
- const isJsTs = /\.(ts|tsx|js|jsx)$/.test(filePath);
25
- const isInSrc = filePath.startsWith(this.context.getSrcDir());
26
- const isIntegrationTemplate = this.context.getTemplateExtensions().some((extension) => filePath.endsWith(extension));
26
+ const resolvedPath = path.resolve(filePath);
27
+ const isJsTs = /\.(ts|tsx|js|jsx)$/.test(resolvedPath);
28
+ const isInSrc = resolvedPath.startsWith(this.context.getSrcDir());
29
+ const isIntegrationTemplate = this.context.getTemplateExtensions().some((extension) => resolvedPath.endsWith(extension));
27
30
  if (watchedFiles.size === 0) {
28
31
  return false;
29
32
  }
30
33
  if (!isJsTs || !isInSrc) {
31
34
  return false;
32
35
  }
36
+ if (watchedFiles.has(resolvedPath)) {
37
+ return true;
38
+ }
33
39
  if (isIntegrationTemplate) {
34
40
  return false;
35
41
  }
36
- if (watchedFiles.has(filePath)) {
37
- return true;
38
- }
39
42
  return true;
40
43
  }
41
44
  /**
@@ -59,7 +62,7 @@ class JsHmrStrategy extends HmrStrategy {
59
62
  }
60
63
  const dependencyHits = this.context.getEntrypointDependencyGraph().getDependencyEntrypoints(filePath);
61
64
  const hasDependencyHit = dependencyHits.size > 0;
62
- const impactedEntrypoints = hasDependencyHit ? Array.from(dependencyHits).filter((entrypoint) => watchedFiles.has(entrypoint)) : Array.from(watchedFiles.keys());
65
+ const impactedEntrypoints = hasDependencyHit ? Array.from(dependencyHits).filter((entrypoint) => watchedFiles.has(path.resolve(entrypoint))) : Array.from(watchedFiles.keys());
63
66
  const buildableEntrypoints = impactedEntrypoints.filter(
64
67
  (entrypoint) => this.context.shouldProcessEntrypoint?.(entrypoint) ?? true
65
68
  );
@@ -69,6 +72,9 @@ class JsHmrStrategy extends HmrStrategy {
69
72
  if (buildableEntrypoints.length === 0) {
70
73
  return { type: "none" };
71
74
  }
75
+ for (const entrypoint of buildableEntrypoints) {
76
+ this.removeStaleEntrypointOutput(this.resolveEntrypointOutputPath(entrypoint));
77
+ }
72
78
  const buildResult = await this.bundleEntrypoints(buildableEntrypoints);
73
79
  if (!buildResult.success) {
74
80
  return { type: "none" };
@@ -82,10 +88,7 @@ class JsHmrStrategy extends HmrStrategy {
82
88
  const entrypointDeps = buildResult.dependencies.get(path.resolve(entrypoint)) ?? [];
83
89
  this.context.getEntrypointDependencyGraph().setEntrypointDependencies(entrypoint, entrypointDeps);
84
90
  }
85
- const srcDir = this.context.getSrcDir();
86
- const relativePath = path.relative(srcDir, entrypoint);
87
- const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, ".js");
88
- const outputPath = path.join(this.context.getDistDir(), relativePathJs);
91
+ const outputPath = this.resolveEntrypointOutputPath(entrypoint);
89
92
  const result = await this.processOutput(outputPath, outputUrl);
90
93
  if (result.success) {
91
94
  updates.push(outputUrl);
@@ -113,6 +116,12 @@ class JsHmrStrategy extends HmrStrategy {
113
116
  }
114
117
  return { type: "none" };
115
118
  }
119
+ resolveEntrypointOutputPath(entrypointPath) {
120
+ return resolveHmrEntrypointOutputPaths(this.context.getSrcDir(), this.context.getDistDir(), entrypointPath).outputPath;
121
+ }
122
+ removeStaleEntrypointOutput(outputPath) {
123
+ removeStaleHmrEntrypointOutput(outputPath, "JsHmrStrategy");
124
+ }
116
125
  /**
117
126
  * Bundles one or more entrypoints in a single build invocation.
118
127
  * Uses the source directory as the output base so that the directory structure
@@ -120,6 +129,30 @@ class JsHmrStrategy extends HmrStrategy {
120
129
  */
121
130
  async bundleEntrypoints(entrypoints) {
122
131
  try {
132
+ if (entrypoints.length === 1) {
133
+ const entrypoint = entrypoints[0];
134
+ const outputPath = this.resolveEntrypointOutputPath(entrypoint);
135
+ const naming = path.relative(this.context.getDistDir(), outputPath).split(path.sep).join("/");
136
+ const result2 = await this.context.getBrowserBundleService().bundle({
137
+ profile: "hmr-entrypoint",
138
+ entrypoints: [entrypoint],
139
+ outdir: this.context.getDistDir(),
140
+ naming,
141
+ plugins: this.context.getPlugins(),
142
+ minify: false
143
+ });
144
+ if (!result2.success) {
145
+ appLogger.error("[JsHmrStrategy] Entrypoint build failed:", result2.logs);
146
+ return { success: false };
147
+ }
148
+ const dependencies2 = /* @__PURE__ */ new Map();
149
+ if (result2.dependencyGraph?.entrypoints) {
150
+ for (const [resolvedEntrypoint, deps] of Object.entries(result2.dependencyGraph.entrypoints)) {
151
+ dependencies2.set(path.resolve(resolvedEntrypoint), deps);
152
+ }
153
+ }
154
+ return { success: true, dependencies: dependencies2 };
155
+ }
123
156
  const result = await this.context.getBrowserBundleService().bundle({
124
157
  profile: "hmr-entrypoint",
125
158
  entrypoints,