@ecopages/core 0.2.0-beta.20 → 0.2.0-beta.21

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 (31) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/bun/hmr-manager.d.ts +1 -2
  3. package/src/adapters/bun/hmr-manager.js +2 -2
  4. package/src/adapters/bun/server-adapter.js +5 -4
  5. package/src/adapters/node/node-hmr-manager.d.ts +1 -2
  6. package/src/adapters/node/node-hmr-manager.js +2 -2
  7. package/src/adapters/node/server-adapter.js +5 -6
  8. package/src/adapters/shared/hmr-entrypoint-registrar.d.ts +21 -28
  9. package/src/adapters/shared/hmr-entrypoint-registrar.js +49 -31
  10. package/src/adapters/shared/runtime-server-lifecycle.d.ts +9 -1
  11. package/src/adapters/shared/runtime-server-lifecycle.js +10 -3
  12. package/src/adapters/shared/server-static-builder.js +4 -1
  13. package/src/adapters/shared/shared-hmr-manager.d.ts +29 -23
  14. package/src/adapters/shared/shared-hmr-manager.js +144 -108
  15. package/src/build/app-build-manifest-runtime.d.ts +1 -2
  16. package/src/build/app-build-manifest-runtime.js +0 -3
  17. package/src/hmr/hmr-entrypoint-output.d.ts +18 -3
  18. package/src/hmr/hmr-entrypoint-output.js +6 -11
  19. package/src/hmr/hmr-strategy.d.ts +15 -0
  20. package/src/hmr/hmr-strategy.js +19 -0
  21. package/src/hmr/strategies/js-hmr-strategy.d.ts +0 -5
  22. package/src/hmr/strategies/js-hmr-strategy.js +0 -2
  23. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.d.ts +0 -1
  24. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.js +13 -32
  25. package/src/services/assets/browser-bundle.service.js +1 -1
  26. package/src/services/module-loading/page-module-import.service.js +0 -1
  27. package/src/services/module-loading/route-module-dependency-hasher.js +1 -0
  28. package/src/types/public-types.d.ts +10 -18
  29. package/src/watchers/project-watcher.d.ts +2 -2
  30. package/src/watchers/project-watcher.js +23 -28
  31. package/src/watchers/project-watcher.test-helpers.js +5 -5
@@ -2,7 +2,11 @@ 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
+ import {
6
+ removeStaleHmrEntrypointOutput,
7
+ isRegisteredScriptEntrypoint,
8
+ isBrowserOnlyRegisteredScriptEntrypoint
9
+ } from "../../hmr/hmr-entrypoint-output.js";
6
10
  import { requireBuildRuntime } from "../../build/build-runtime.js";
7
11
  import { fileSystem } from "@ecopages/file-system";
8
12
  import { HmrStrategyType } from "../../hmr/hmr-strategy.js";
@@ -19,52 +23,31 @@ import {
19
23
  setAppEntrypointDependencyGraph
20
24
  } from "../../services/runtime-state/entrypoint-dependency-graph.service.js";
21
25
  import { resolveInternalExecutionDir, resolveInternalWorkDir } from "../../utils/resolve-work-dir.js";
22
- const DEFAULT_HMR_REGISTRATION_TIMEOUT_MS = 4e3;
23
- const DEVELOPMENT_HMR_REGISTRATION_TIMEOUT_MS = 15e3;
24
- function resolveHmrRegistrationTimeoutMs(explicitTimeoutMs) {
25
- if (explicitTimeoutMs !== void 0) {
26
- return explicitTimeoutMs;
27
- }
28
- const envTimeoutMs = process.env.ECOPAGES_HMR_REGISTRATION_TIMEOUT_MS;
29
- if (envTimeoutMs !== void 0 && envTimeoutMs !== "") {
30
- const parsedTimeoutMs = Number(envTimeoutMs);
31
- if (Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0) {
32
- return parsedTimeoutMs;
33
- }
34
- }
35
- if (process.env.NODE_ENV === "development") {
36
- return DEVELOPMENT_HMR_REGISTRATION_TIMEOUT_MS;
37
- }
38
- return DEFAULT_HMR_REGISTRATION_TIMEOUT_MS;
39
- }
40
26
  class SharedHmrManager {
41
27
  appConfig;
42
28
  bridge;
43
29
  watchers = /* @__PURE__ */ new Map();
44
- watchedFiles = /* @__PURE__ */ new Map();
45
- entrypointRegistrations = /* @__PURE__ */ new Map();
46
30
  distDir;
47
- plugins = [];
48
31
  enabled = false;
49
32
  strategies = [];
50
33
  entrypointRegistrar;
51
34
  browserBundleService;
35
+ invalidationService;
52
36
  entrypointDependencyGraph;
53
37
  serverModuleTranspiler;
54
38
  runtimeBuildPromise = null;
55
- constructor({ appConfig, bridge, registrationTimeoutMs }) {
39
+ runtimeReady = false;
40
+ constructor({ appConfig, bridge }) {
56
41
  this.appConfig = appConfig;
57
42
  this.bridge = bridge;
58
43
  this.distDir = path.join(resolveInternalWorkDir(this.appConfig), RESOLVED_ASSETS_DIR, "_hmr");
59
44
  this.entrypointRegistrar = new HmrEntrypointRegistrar({
60
45
  srcDir: this.appConfig.absolutePaths.srcDir,
61
46
  distDir: this.distDir,
62
- entrypointRegistrations: this.entrypointRegistrations,
63
- watchedFiles: this.watchedFiles,
64
- clearFailedRegistration: (entrypointPath) => this.clearFailedEntrypointRegistration(entrypointPath),
65
- registrationTimeoutMs: resolveHmrRegistrationTimeoutMs(registrationTimeoutMs)
47
+ clearFailedRegistration: (entrypointPath) => this.clearFailedEntrypointRegistration(entrypointPath)
66
48
  });
67
49
  this.browserBundleService = new BrowserBundleService(appConfig);
50
+ this.invalidationService = new DevelopmentInvalidationService(appConfig);
68
51
  this.entrypointDependencyGraph = this.createEntrypointDependencyGraph(
69
52
  getAppEntrypointDependencyGraph(appConfig)
70
53
  );
@@ -97,9 +80,8 @@ class SharedHmrManager {
97
80
  }
98
81
  initializeStrategies() {
99
82
  const jsContext = {
100
- getWatchedFiles: () => this.watchedFiles,
83
+ getWatchedFiles: () => this.entrypointRegistrar.getWatchedFiles(),
101
84
  getDistDir: () => this.distDir,
102
- getPlugins: () => this.plugins,
103
85
  getSrcDir: () => this.appConfig.absolutePaths.srcDir,
104
86
  getPagesDir: () => this.appConfig.absolutePaths.pagesDir,
105
87
  getLayoutsDir: () => this.appConfig.absolutePaths.layoutsDir,
@@ -118,9 +100,6 @@ class SharedHmrManager {
118
100
  registerStrategy(strategy) {
119
101
  this.strategies.push(strategy);
120
102
  }
121
- setPlugins(plugins) {
122
- this.plugins = [...plugins];
123
- }
124
103
  setEnabled(enabled) {
125
104
  this.enabled = enabled;
126
105
  }
@@ -128,13 +107,13 @@ class SharedHmrManager {
128
107
  return this.enabled;
129
108
  }
130
109
  isRuntimeReady() {
131
- return fileSystem.exists(this.getRuntimePath());
110
+ return this.runtimeReady && fileSystem.exists(this.getRuntimePath());
132
111
  }
133
112
  /**
134
- * Builds the browser HMR runtime once and reuses the in-flight build for concurrent callers.
113
+ * Builds the browser HMR runtime once per manager session and reuses the in-flight build for concurrent callers.
135
114
  */
136
115
  async ensureRuntimeReady() {
137
- if (this.isRuntimeReady()) {
116
+ if (this.runtimeReady) {
138
117
  return true;
139
118
  }
140
119
  if (this.runtimeBuildPromise) {
@@ -142,7 +121,9 @@ class SharedHmrManager {
142
121
  }
143
122
  this.runtimeBuildPromise = this.buildRuntimeInternal();
144
123
  try {
145
- return await this.runtimeBuildPromise;
124
+ const ready = await this.runtimeBuildPromise;
125
+ this.runtimeReady = ready;
126
+ return ready;
146
127
  } finally {
147
128
  this.runtimeBuildPromise = null;
148
129
  }
@@ -155,44 +136,35 @@ class SharedHmrManager {
155
136
  }
156
137
  async buildRuntimeInternal() {
157
138
  const runtimeSource = fileURLToPath(import.meta.resolve("@ecopages/core/hmr/client/hmr-runtime"));
139
+ const runtimePath = this.getRuntimePath();
140
+ removeStaleHmrEntrypointOutput(runtimePath, "HMR");
158
141
  try {
159
142
  const result = await this.browserBundleService.bundle({
160
143
  profile: "hmr-runtime",
161
144
  entrypoints: [runtimeSource],
162
145
  outdir: this.distDir,
163
146
  naming: "_hmr_runtime.js",
164
- minify: false,
165
- plugins: this.plugins
147
+ minify: false
166
148
  });
167
149
  if (!result.success) {
168
150
  this.onRuntimeBundleFailure(result.logs);
169
151
  return false;
170
152
  }
171
- this.syncRuntimeOutput(result);
172
- return this.isRuntimeReady();
153
+ const emittedRuntime = result.outputs.find(
154
+ (output) => path.resolve(output.path) === path.resolve(runtimePath)
155
+ );
156
+ if (!emittedRuntime || !fileSystem.exists(runtimePath)) {
157
+ this.onRuntimeBundleFailure(
158
+ new Error(`[HMR] Runtime bundle missing expected output at ${runtimePath}`)
159
+ );
160
+ return false;
161
+ }
162
+ return true;
173
163
  } catch (error) {
174
164
  this.onRuntimeBundleFailure(error);
175
165
  return false;
176
166
  }
177
167
  }
178
- /**
179
- * @remarks
180
- * Rolldown may emit the runtime bundle under a derived filename when the
181
- * entrypoint lives outside the app root. The dev server always serves
182
- * `/_hmr_runtime.js` from a stable path under `.eco/assets/_hmr/`.
183
- */
184
- syncRuntimeOutput(result) {
185
- const runtimePath = this.getRuntimePath();
186
- if (fileSystem.exists(runtimePath)) {
187
- return;
188
- }
189
- const emittedRuntime = result.outputs.find((output) => output.path.endsWith(".js"));
190
- if (!emittedRuntime || emittedRuntime.path === runtimePath) {
191
- return;
192
- }
193
- fileSystem.ensureDir(path.dirname(runtimePath));
194
- fileSystem.copyFile(emittedRuntime.path, runtimePath);
195
- }
196
168
  broadcast(event) {
197
169
  appLogger.debug(
198
170
  `[HMR] Broadcasting ${event.type} event, path=${event.path || "all"}, subscribers=${this.bridge.subscriberCount}`
@@ -200,20 +172,16 @@ class SharedHmrManager {
200
172
  this.bridge.broadcast(event);
201
173
  }
202
174
  async handleFileChange(filePath, options = {}) {
175
+ const resolvedFilePath = path.resolve(filePath);
203
176
  if (this.shouldSkipMissingFileChange(filePath) && !fileSystem.exists(filePath)) {
204
177
  appLogger.debug(`[${this.constructor.name}] Skipping missing file change: ${filePath}`);
205
178
  this.clearFailedEntrypointRegistration(filePath);
206
179
  return;
207
180
  }
208
- const sorted = [...this.strategies].sort((a, b) => b.priority - a.priority);
209
- const strategy = sorted.find((candidate) => {
210
- try {
211
- return candidate.matches(filePath);
212
- } catch (error) {
213
- appLogger.error(error);
214
- return false;
215
- }
216
- });
181
+ if (isRegisteredScriptEntrypoint(this.entrypointRegistrar.getRegistered(), resolvedFilePath)) {
182
+ await this.prepareRegisteredScriptChange(resolvedFilePath);
183
+ }
184
+ const strategy = this.selectChangeStrategy(filePath);
217
185
  if (!strategy) {
218
186
  appLogger.warn(`[HMR] No strategy found for ${filePath}`);
219
187
  return;
@@ -227,8 +195,92 @@ class SharedHmrManager {
227
195
  }
228
196
  }
229
197
  }
198
+ getStrategiesByPriority() {
199
+ return [...this.strategies].sort((left, right) => right.priority - left.priority);
200
+ }
201
+ selectChangeStrategy(filePath) {
202
+ return this.getStrategiesByPriority().find((candidate) => {
203
+ try {
204
+ return candidate.matches(filePath);
205
+ } catch (error) {
206
+ appLogger.error(error);
207
+ return false;
208
+ }
209
+ });
210
+ }
211
+ selectEntrypointEmitter(entrypointPath) {
212
+ return this.getStrategiesByPriority().find((candidate) => {
213
+ if (candidate.type !== HmrStrategyType.INTEGRATION) {
214
+ return false;
215
+ }
216
+ try {
217
+ return candidate.canEmitEntrypoint(entrypointPath);
218
+ } catch (error) {
219
+ appLogger.error(error);
220
+ return false;
221
+ }
222
+ });
223
+ }
224
+ createUnownedEntrypointError(entrypointPath) {
225
+ return new Error(
226
+ `[HMR] No integration owns entrypoint ${entrypointPath}. Page entrypoints must be emitted by their owning integration.`
227
+ );
228
+ }
229
+ /**
230
+ * Materializes one integration-owned page entrypoint during cold registration.
231
+ *
232
+ * @remarks
233
+ * Registration uses {@link HmrStrategy.canEmitEntrypoint} / {@link HmrStrategy.emitEntrypoint},
234
+ * not {@link HmrStrategy.matches} / {@link HmrStrategy.process}. File-change handling stays
235
+ * on `handleFileChange()`.
236
+ */
237
+ async emitIntegrationEntrypoint(entrypointPath, outputPath) {
238
+ const emitter = this.selectEntrypointEmitter(entrypointPath);
239
+ if (!emitter) {
240
+ throw this.createUnownedEntrypointError(entrypointPath);
241
+ }
242
+ appLogger.debug(`[${this.constructor.name}] Selected entrypoint emitter: ${emitter.constructor.name}`);
243
+ await emitter.emitEntrypoint(entrypointPath, outputPath);
244
+ }
245
+ /**
246
+ * Runs server invalidation and integration hooks before rebuilding a registered script entrypoint.
247
+ *
248
+ * @remarks
249
+ * Browser-only `*.script.ts` entrypoints skip server module invalidation and bypass-cache
250
+ * import; only integration change handlers and the downstream strategy rebuild run.
251
+ */
252
+ async prepareRegisteredScriptChange(filePath) {
253
+ const isBrowserOnlyRegisteredScript = isBrowserOnlyRegisteredScriptEntrypoint(filePath);
254
+ if (!isBrowserOnlyRegisteredScript) {
255
+ this.invalidationService.invalidateServerModules([filePath]);
256
+ }
257
+ const handlers = this.appConfig.runtime?.registeredScriptEntrypointChangeHandlers ?? [];
258
+ for (const handler of handlers) {
259
+ try {
260
+ await handler(filePath);
261
+ } catch (error) {
262
+ const message = error instanceof Error ? error.message : `Failed to handle registered script entrypoint change for ${filePath}: ${String(error)}`;
263
+ this.broadcast({ type: "error", message });
264
+ throw error;
265
+ }
266
+ }
267
+ if (isBrowserOnlyRegisteredScript) {
268
+ return;
269
+ }
270
+ const appModuleLoader = this.appConfig.runtime?.appModuleLoader;
271
+ if (!appModuleLoader) {
272
+ return;
273
+ }
274
+ await appModuleLoader.importModule({
275
+ filePath,
276
+ rootDir: this.appConfig.rootDir,
277
+ outdir: path.join(resolveInternalExecutionDir(this.appConfig), ".server-modules"),
278
+ externalPackages: true,
279
+ bypassCache: true
280
+ });
281
+ }
230
282
  getOutputUrl(entrypointPath) {
231
- return this.watchedFiles.get(entrypointPath);
283
+ return this.entrypointRegistrar.getRegistered().get(path.resolve(entrypointPath))?.outputUrl;
232
284
  }
233
285
  /**
234
286
  * Returns the emitted HMR script output when the entrypoint is already registered
@@ -240,34 +292,22 @@ class SharedHmrManager {
240
292
  */
241
293
  getResolvedScriptOutput(entrypointPath) {
242
294
  const normalizedEntrypoint = path.resolve(entrypointPath);
243
- if (!this.watchedFiles.has(normalizedEntrypoint)) {
244
- return void 0;
245
- }
246
- const { outputPath, outputUrl: derivedOutputUrl } = resolveHmrEntrypointOutputPaths(
247
- this.appConfig.absolutePaths.srcDir,
248
- this.distDir,
249
- normalizedEntrypoint
250
- );
251
- if (!fileSystem.exists(outputPath)) {
295
+ const registered = this.entrypointRegistrar.getRegistered().get(normalizedEntrypoint);
296
+ if (!registered || !fileSystem.exists(registered.outputPath)) {
252
297
  return void 0;
253
298
  }
254
- const outputUrl = this.watchedFiles.get(normalizedEntrypoint) ?? derivedOutputUrl;
255
- return { outputUrl, outputPath };
299
+ return registered;
256
300
  }
257
301
  getWatchedFiles() {
258
- return this.watchedFiles;
302
+ return this.entrypointRegistrar.getWatchedFiles();
259
303
  }
260
304
  getDistDir() {
261
305
  return this.distDir;
262
306
  }
263
- getPlugins() {
264
- return this.plugins;
265
- }
266
307
  getDefaultContext() {
267
308
  return {
268
- getWatchedFiles: () => this.watchedFiles,
309
+ getWatchedFiles: () => this.entrypointRegistrar.getWatchedFiles(),
269
310
  getDistDir: () => this.distDir,
270
- getPlugins: () => this.plugins,
271
311
  getSrcDir: () => this.appConfig.absolutePaths.srcDir,
272
312
  getLayoutsDir: () => this.appConfig.absolutePaths.layoutsDir,
273
313
  getPagesDir: () => this.appConfig.absolutePaths.pagesDir,
@@ -281,27 +321,34 @@ class SharedHmrManager {
281
321
  })
282
322
  };
283
323
  }
324
+ stop() {
325
+ this.runtimeReady = false;
326
+ this.entrypointRegistrar.clearAll();
327
+ for (const watcher of this.watchers.values()) {
328
+ watcher.close();
329
+ }
330
+ this.watchers.clear();
331
+ this.entrypointDependencyGraph.reset();
332
+ }
284
333
  clearFailedEntrypointRegistration(entrypointPath) {
285
- this.watchedFiles.delete(entrypointPath);
334
+ this.entrypointRegistrar.clearRegistration(entrypointPath);
286
335
  this.entrypointDependencyGraph.clearEntrypointDependencies(entrypointPath);
287
336
  }
288
337
  async registerEntrypoint(entrypointPath) {
289
- return await this.entrypointRegistrar.registerEntrypoint(entrypointPath, {
290
- emit: async (normalizedEntrypoint) => await this.emitStrictEntrypoint(normalizedEntrypoint),
338
+ const resolved = await this.entrypointRegistrar.registerEntrypoint(entrypointPath, {
339
+ emit: async (normalizedEntrypoint, outputPath) => await this.emitIntegrationEntrypoint(normalizedEntrypoint, outputPath),
291
340
  getMissingOutputError: (normalizedEntrypoint, outputPath) => new Error(
292
341
  `[HMR] Integration failed to emit entrypoint ${normalizedEntrypoint} to ${outputPath}. Page entrypoints must be produced by their owning integration.`
293
342
  )
294
343
  });
344
+ return resolved.outputUrl;
295
345
  }
296
346
  async registerScriptEntrypoint(entrypointPath) {
297
347
  return await this.entrypointRegistrar.registerEntrypoint(entrypointPath, {
298
348
  emit: async (normalizedEntrypoint, outputPath) => await this.emitScriptEntrypoint(normalizedEntrypoint, outputPath),
299
- getMissingOutputError: (normalizedEntrypoint) => new Error(`[HMR] Failed to register script entrypoint: ${normalizedEntrypoint}`)
349
+ getMissingOutputError: (normalizedEntrypoint, outputPath) => new Error(`[HMR] Failed to register script entrypoint: ${normalizedEntrypoint} to ${outputPath}`)
300
350
  });
301
351
  }
302
- async emitStrictEntrypoint(entrypointPath) {
303
- await this.handleFileChange(entrypointPath, { broadcast: false });
304
- }
305
352
  async emitScriptEntrypoint(entrypointPath, outputPath) {
306
353
  const naming = path.relative(this.distDir, outputPath).split(path.sep).join("/");
307
354
  const buildResult = await this.browserBundleService.bundle({
@@ -309,12 +356,12 @@ class SharedHmrManager {
309
356
  entrypoints: [entrypointPath],
310
357
  outdir: this.distDir,
311
358
  naming,
312
- minify: false,
313
- plugins: this.plugins
359
+ minify: false
314
360
  });
315
361
  if (!buildResult.success) {
316
- appLogger.error(`[HMR] Generic script entrypoint build failed for ${entrypointPath}:`, buildResult.logs);
317
- return;
362
+ throw new Error(
363
+ `[HMR] Generic script entrypoint build failed for ${entrypointPath}: ${JSON.stringify(buildResult.logs)}`
364
+ );
318
365
  }
319
366
  if (!fileSystem.exists(outputPath) && buildResult.outputs.length > 0) {
320
367
  const resolvedOutputPath = path.resolve(outputPath);
@@ -331,21 +378,10 @@ class SharedHmrManager {
331
378
  this.entrypointDependencyGraph.setEntrypointDependencies(entrypointPath, entrypointDependencies);
332
379
  }
333
380
  }
334
- stop() {
335
- this.entrypointRegistrations.clear();
336
- for (const watcher of this.watchers.values()) {
337
- watcher.close();
338
- }
339
- this.watchers.clear();
340
- this.watchedFiles.clear();
341
- this.entrypointDependencyGraph.reset();
342
- this.plugins = [];
343
- }
344
381
  [Symbol.dispose]() {
345
382
  this.stop();
346
383
  }
347
384
  }
348
385
  export {
349
- SharedHmrManager,
350
- resolveHmrRegistrationTimeoutMs
386
+ SharedHmrManager
351
387
  };
@@ -1,10 +1,9 @@
1
1
  import type { EcoBuildPlugin } from './build-types.js';
2
2
  import type { AppBuildManifest } from './build-manifest.js';
3
- import type { EcoPagesAppConfig, IHmrManager } from '../types/internal-types.js';
3
+ import type { EcoPagesAppConfig } from '../types/internal-types.js';
4
4
  export declare function collectConfiguredAppBuildManifestContributions(appConfig: EcoPagesAppConfig): Promise<Pick<AppBuildManifest, 'runtimePlugins' | 'browserBundlePlugins' | 'browserRuntimeManifest'>>;
5
5
  export declare function setupAppRuntimePlugins(options: {
6
6
  appConfig: EcoPagesAppConfig;
7
7
  runtimeOrigin: string;
8
- hmrManager?: IHmrManager;
9
8
  onRuntimePlugin?: (plugin: EcoBuildPlugin) => void;
10
9
  }): Promise<void>;
@@ -71,9 +71,6 @@ async function setupAppRuntimePlugins(options) {
71
71
  for (const integration of options.appConfig.integrations) {
72
72
  integration.setConfig(options.appConfig);
73
73
  integration.setRuntimeOrigin(options.runtimeOrigin);
74
- if (options.hmrManager) {
75
- integration.setHmrManager(options.hmrManager);
76
- }
77
74
  await integration.setup();
78
75
  for (const plugin of integration.plugins) {
79
76
  options.onRuntimePlugin?.(plugin);
@@ -1,11 +1,26 @@
1
+ /**
2
+ * Verified HMR entrypoint artifact produced by registration.
3
+ */
4
+ export interface ResolvedHmrEntrypoint {
5
+ sourcePath: string;
6
+ outputPath: string;
7
+ outputUrl: string;
8
+ }
1
9
  /**
2
10
  * Returns whether a source path is a registered client script HMR entrypoint.
11
+ */
12
+ export declare function isRegisteredScriptEntrypoint(registered: ReadonlyMap<string, unknown>, filePath: string): boolean;
13
+ /**
14
+ * Returns whether a registered script entrypoint is browser-only and must not be
15
+ * server-imported during HMR invalidation.
3
16
  *
4
17
  * @remarks
5
- * Registration comes from `dependencies.scripts` via `FileScriptProcessor`,
6
- * not from filename conventions such as `.script.tsx`.
18
+ * Declared `*.script.ts` modules run only in the browser bundle. Re-importing them
19
+ * on the server during `prepareRegisteredScriptChange()` executes DOM globals and
20
+ * aborts the watcher before the client artifact rebuilds. Radiant `*.script.tsx`
21
+ * entrypoints are server-rendered and remain on the invalidation path.
7
22
  */
8
- export declare function isRegisteredScriptEntrypoint(watchedFiles: ReadonlyMap<string, string>, filePath: string): boolean;
23
+ export declare function isBrowserOnlyRegisteredScriptEntrypoint(filePath: string): boolean;
9
24
  export declare function encodeHmrDynamicSegments(filepath: string): string;
10
25
  /**
11
26
  * Resolves the on-disk and browser URL targets for one HMR script entrypoint.
@@ -3,17 +3,11 @@ import path from "node:path";
3
3
  import { fileSystem } from "@ecopages/file-system";
4
4
  import { RESOLVED_ASSETS_DIR } from "../config/constants.js";
5
5
  import { appLogger } from "../global/app-logger.js";
6
- function isRegisteredScriptEntrypoint(watchedFiles, filePath) {
7
- const resolvedPath = path.resolve(filePath);
8
- if (watchedFiles.has(resolvedPath)) {
9
- return true;
10
- }
11
- for (const entrypointPath of watchedFiles.keys()) {
12
- if (path.resolve(entrypointPath) === resolvedPath) {
13
- return true;
14
- }
15
- }
16
- return false;
6
+ function isRegisteredScriptEntrypoint(registered, filePath) {
7
+ return registered.has(path.resolve(filePath));
8
+ }
9
+ function isBrowserOnlyRegisteredScriptEntrypoint(filePath) {
10
+ return /\.script\.ts$/u.test(path.resolve(filePath));
17
11
  }
18
12
  function encodeHmrDynamicSegments(filepath) {
19
13
  return filepath.replace(/\[([^\]]+)\]/g, "_$1_");
@@ -51,6 +45,7 @@ function isHmrOutputFresh(outputPath, sourcePath) {
51
45
  }
52
46
  export {
53
47
  encodeHmrDynamicSegments,
48
+ isBrowserOnlyRegisteredScriptEntrypoint,
54
49
  isHmrOutputFresh,
55
50
  isRegisteredScriptEntrypoint,
56
51
  removeStaleHmrEntrypointOutput,
@@ -133,6 +133,21 @@ export declare abstract class HmrStrategy {
133
133
  * ```
134
134
  */
135
135
  abstract matches(filePath: string): boolean;
136
+ /**
137
+ * Returns whether this strategy owns cold registration for the entrypoint.
138
+ *
139
+ * @remarks
140
+ * Registration dispatch uses only integration strategies that claim the
141
+ * entrypoint. File-change dispatch continues to use {@link matches}.
142
+ */
143
+ canEmitEntrypoint(_entrypointPath: string): boolean;
144
+ /**
145
+ * Materializes one integration-owned HMR entrypoint during registration.
146
+ *
147
+ * @remarks
148
+ * Must write the canonical output path and must not broadcast client events.
149
+ */
150
+ emitEntrypoint(_entrypointPath: string, _outputPath: string): Promise<void>;
136
151
  /**
137
152
  * Processes a file change and returns the action to take.
138
153
  *
@@ -37,6 +37,25 @@ class HmrStrategy {
37
37
  get priority() {
38
38
  return this.type + this.priorityOffset;
39
39
  }
40
+ /**
41
+ * Returns whether this strategy owns cold registration for the entrypoint.
42
+ *
43
+ * @remarks
44
+ * Registration dispatch uses only integration strategies that claim the
45
+ * entrypoint. File-change dispatch continues to use {@link matches}.
46
+ */
47
+ canEmitEntrypoint(_entrypointPath) {
48
+ return false;
49
+ }
50
+ /**
51
+ * Materializes one integration-owned HMR entrypoint during registration.
52
+ *
53
+ * @remarks
54
+ * Must write the canonical output path and must not broadcast client events.
55
+ */
56
+ async emitEntrypoint(_entrypointPath, _outputPath) {
57
+ return;
58
+ }
40
59
  }
41
60
  export {
42
61
  HmrStrategy,
@@ -8,7 +8,6 @@
8
8
  * @module
9
9
  */
10
10
  import { HmrStrategy, type HmrAction } from '../hmr-strategy.js';
11
- import type { EcoBuildPlugin } from '../../build/build-types.js';
12
11
  import type { BrowserBundleExecutor } from '../../services/assets/browser-bundle.service.js';
13
12
  import type { EntrypointDependencyGraph } from '../../services/runtime-state/entrypoint-dependency-graph.service.js';
14
13
  /**
@@ -25,10 +24,6 @@ export interface JsHmrContext {
25
24
  * Directory where HMR bundles are written.
26
25
  */
27
26
  getDistDir(): string;
28
- /**
29
- * Build plugins to use during bundling.
30
- */
31
- getPlugins(): EcoBuildPlugin[];
32
27
  /**
33
28
  * Absolute path to the source directory.
34
29
  */
@@ -151,7 +151,6 @@ class JsHmrStrategy extends HmrStrategy {
151
151
  entrypoints: [entrypoint],
152
152
  outdir: this.context.getDistDir(),
153
153
  naming,
154
- plugins: this.context.getPlugins(),
155
154
  minify: false
156
155
  });
157
156
  if (!result2.success) {
@@ -172,7 +171,6 @@ class JsHmrStrategy extends HmrStrategy {
172
171
  outdir: this.context.getDistDir(),
173
172
  outbase: this.context.getSrcDir(),
174
173
  naming: "[dir]/[name]",
175
- plugins: this.context.getPlugins(),
176
174
  minify: false
177
175
  });
178
176
  if (!result.success) {
@@ -3,7 +3,6 @@ import type { FileScriptAsset, ProcessedAsset } from '../../assets.types.js';
3
3
  import { BaseScriptProcessor } from '../base/base-script-processor.js';
4
4
  export declare class FileScriptProcessor extends BaseScriptProcessor<FileScriptAsset> {
5
5
  private hmrManager?;
6
- private resolveHmrOutputFilepath;
7
6
  setHmrManager(hmrManager: IHmrManager): void;
8
7
  process(dep: FileScriptAsset): Promise<ProcessedAsset>;
9
8
  }
@@ -1,21 +1,9 @@
1
1
  import path from "node:path";
2
2
  import { RESOLVED_ASSETS_DIR } from "../../../../../config/constants.js";
3
3
  import { fileSystem } from "@ecopages/file-system";
4
- import { appLogger } from "../../../../../global/app-logger.js";
5
4
  import { BaseScriptProcessor } from "../base/base-script-processor.js";
6
5
  class FileScriptProcessor extends BaseScriptProcessor {
7
6
  hmrManager;
8
- resolveHmrOutputFilepath(entrypointPath) {
9
- if (!this.hmrManager || !("getDistDir" in this.hmrManager)) {
10
- return void 0;
11
- }
12
- const getDistDir = this.hmrManager.getDistDir;
13
- if (typeof getDistDir !== "function") {
14
- return void 0;
15
- }
16
- const relativePathJs = path.relative(this.appConfig.absolutePaths.srcDir, entrypointPath).replace(/\.(tsx?|jsx?|mdx?)$/, ".js").replace(/\[([^\]]+)\]/g, "_$1_");
17
- return path.join(getDistDir.call(this.hmrManager), relativePathJs);
18
- }
19
7
  setHmrManager(hmrManager) {
20
8
  this.hmrManager = hmrManager;
21
9
  }
@@ -36,26 +24,19 @@ class FileScriptProcessor extends BaseScriptProcessor {
36
24
  bundledSourceFilepaths: dep.bundledSourceFilepaths
37
25
  };
38
26
  }
39
- try {
40
- const outputUrl = await this.hmrManager.registerScriptEntrypoint(dep.filepath);
41
- const outputFilepath = this.resolveHmrOutputFilepath(dep.filepath);
42
- return {
43
- filepath: outputFilepath ?? dep.filepath,
44
- sourceFilepath: dep.filepath,
45
- srcUrl: outputUrl,
46
- kind: "script",
47
- position: dep.position,
48
- attributes: dep.attributes,
49
- inline: false,
50
- excludeFromHtml: dep.excludeFromHtml,
51
- packageRole: dep.packageRole,
52
- bundledSourceFilepaths: dep.bundledSourceFilepaths
53
- };
54
- } catch (error) {
55
- appLogger.warn(
56
- `[FileScriptProcessor] HMR script registration failed for ${dep.filepath}, falling back to non-HMR asset: ${error instanceof Error ? error.message : String(error)}`
57
- );
58
- }
27
+ const registered = await this.hmrManager.registerScriptEntrypoint(dep.filepath);
28
+ return {
29
+ filepath: registered.outputPath,
30
+ sourceFilepath: dep.filepath,
31
+ srcUrl: registered.outputUrl,
32
+ kind: "script",
33
+ position: dep.position,
34
+ attributes: dep.attributes,
35
+ inline: false,
36
+ excludeFromHtml: dep.excludeFromHtml,
37
+ packageRole: dep.packageRole,
38
+ bundledSourceFilepaths: dep.bundledSourceFilepaths
39
+ };
59
40
  }
60
41
  const content = fileSystem.readFileSync(dep.filepath);
61
42
  const shouldBundle = this.shouldBundle(dep);
@@ -4,7 +4,7 @@ import { mergeEcoBuildPlugins } from "../../build/build-manifest.js";
4
4
  import { getAppSourceTransforms } from "../../plugins/source-transform.js";
5
5
  function resolveBrowserBundleExecutor(appConfig, profile, executor) {
6
6
  const buildRuntime = requireBuildRuntime(appConfig);
7
- if (executor === "hmr" && profile === "hmr-entrypoint") {
7
+ if (executor === "hmr" && (profile === "hmr-entrypoint" || profile === "hmr-runtime")) {
8
8
  return buildRuntime.getProfile("browser-hmr");
9
9
  }
10
10
  return buildRuntime.getProfile("route-module");