@ecopages/core 0.2.0-beta.20 → 0.2.0-beta.22
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 +2 -2
- package/src/adapters/bun/hmr-manager.d.ts +1 -2
- package/src/adapters/bun/hmr-manager.js +2 -2
- package/src/adapters/bun/server-adapter.js +5 -4
- package/src/adapters/node/node-hmr-manager.d.ts +1 -2
- package/src/adapters/node/node-hmr-manager.js +2 -2
- package/src/adapters/node/server-adapter.js +5 -6
- package/src/adapters/shared/hmr-entrypoint-registrar.d.ts +21 -28
- package/src/adapters/shared/hmr-entrypoint-registrar.js +49 -31
- package/src/adapters/shared/runtime-server-lifecycle.d.ts +9 -1
- package/src/adapters/shared/runtime-server-lifecycle.js +10 -3
- package/src/adapters/shared/server-static-builder.js +4 -1
- package/src/adapters/shared/shared-hmr-manager.d.ts +29 -23
- package/src/adapters/shared/shared-hmr-manager.js +144 -108
- package/src/build/app-build-manifest-runtime.d.ts +1 -2
- package/src/build/app-build-manifest-runtime.js +0 -3
- package/src/hmr/hmr-entrypoint-output.d.ts +24 -3
- package/src/hmr/hmr-entrypoint-output.js +13 -11
- package/src/hmr/hmr-strategy.d.ts +15 -0
- package/src/hmr/hmr-strategy.js +19 -0
- package/src/hmr/strategies/js-hmr-strategy.d.ts +0 -5
- package/src/hmr/strategies/js-hmr-strategy.js +7 -4
- package/src/services/assets/asset-processing-service/processors/script/file-script.processor.d.ts +0 -1
- package/src/services/assets/asset-processing-service/processors/script/file-script.processor.js +13 -32
- package/src/services/assets/browser-bundle.service.js +1 -1
- package/src/services/module-loading/page-module-import.service.js +0 -1
- package/src/services/module-loading/route-module-dependency-hasher.js +1 -0
- package/src/types/public-types.d.ts +10 -18
- package/src/watchers/project-watcher.d.ts +2 -2
- package/src/watchers/project-watcher.js +23 -28
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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.
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
255
|
-
return { outputUrl, outputPath };
|
|
299
|
+
return registered;
|
|
256
300
|
}
|
|
257
301
|
getWatchedFiles() {
|
|
258
|
-
return this.
|
|
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.
|
|
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.
|
|
334
|
+
this.entrypointRegistrar.clearRegistration(entrypointPath);
|
|
286
335
|
this.entrypointDependencyGraph.clearEntrypointDependencies(entrypointPath);
|
|
287
336
|
}
|
|
288
337
|
async registerEntrypoint(entrypointPath) {
|
|
289
|
-
|
|
290
|
-
emit: async (normalizedEntrypoint) => await this.
|
|
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
|
-
|
|
317
|
-
|
|
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
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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.
|
|
@@ -26,3 +41,9 @@ export declare function removeStaleHmrEntrypointOutput(outputPath: string, scope
|
|
|
26
41
|
* fresher artifact than the edited source file.
|
|
27
42
|
*/
|
|
28
43
|
export declare function isHmrOutputFresh(outputPath: string, sourcePath: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* @remarks
|
|
46
|
+
* Missing output is treated as a benign rebuild race. This helper isolates the
|
|
47
|
+
* post-build case where a bundle exists but is still older than its source.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isHmrOutputOlderThanSource(outputPath: string, sourcePath: string): boolean;
|
|
@@ -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(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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_");
|
|
@@ -49,9 +43,17 @@ function isHmrOutputFresh(outputPath, sourcePath) {
|
|
|
49
43
|
const sourceMtimeMs = fs.statSync(sourcePath).mtimeMs;
|
|
50
44
|
return outputMtimeMs >= sourceMtimeMs;
|
|
51
45
|
}
|
|
46
|
+
function isHmrOutputOlderThanSource(outputPath, sourcePath) {
|
|
47
|
+
if (!fileSystem.exists(outputPath) || !fileSystem.exists(sourcePath)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return fs.statSync(outputPath).mtimeMs < fs.statSync(sourcePath).mtimeMs;
|
|
51
|
+
}
|
|
52
52
|
export {
|
|
53
53
|
encodeHmrDynamicSegments,
|
|
54
|
+
isBrowserOnlyRegisteredScriptEntrypoint,
|
|
54
55
|
isHmrOutputFresh,
|
|
56
|
+
isHmrOutputOlderThanSource,
|
|
55
57
|
isRegisteredScriptEntrypoint,
|
|
56
58
|
removeStaleHmrEntrypointOutput,
|
|
57
59
|
resolveHmrEntrypointOutputPaths
|
|
@@ -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
|
*
|
package/src/hmr/hmr-strategy.js
CHANGED
|
@@ -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
|
*/
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
removeStaleHmrEntrypointOutput,
|
|
7
7
|
resolveHmrEntrypointOutputPaths,
|
|
8
8
|
isRegisteredScriptEntrypoint,
|
|
9
|
-
isHmrOutputFresh
|
|
9
|
+
isHmrOutputFresh,
|
|
10
|
+
isHmrOutputOlderThanSource
|
|
10
11
|
} from "../hmr-entrypoint-output.js";
|
|
11
12
|
class JsHmrStrategy extends HmrStrategy {
|
|
12
13
|
type = HmrStrategyType.SCRIPT;
|
|
@@ -151,7 +152,6 @@ class JsHmrStrategy extends HmrStrategy {
|
|
|
151
152
|
entrypoints: [entrypoint],
|
|
152
153
|
outdir: this.context.getDistDir(),
|
|
153
154
|
naming,
|
|
154
|
-
plugins: this.context.getPlugins(),
|
|
155
155
|
minify: false
|
|
156
156
|
});
|
|
157
157
|
if (!result2.success) {
|
|
@@ -172,7 +172,6 @@ class JsHmrStrategy extends HmrStrategy {
|
|
|
172
172
|
outdir: this.context.getDistDir(),
|
|
173
173
|
outbase: this.context.getSrcDir(),
|
|
174
174
|
naming: "[dir]/[name]",
|
|
175
|
-
plugins: this.context.getPlugins(),
|
|
176
175
|
minify: false
|
|
177
176
|
});
|
|
178
177
|
if (!result.success) {
|
|
@@ -202,7 +201,11 @@ class JsHmrStrategy extends HmrStrategy {
|
|
|
202
201
|
async processOutput(filepath, url, sourcePath) {
|
|
203
202
|
try {
|
|
204
203
|
if (sourcePath && !isHmrOutputFresh(filepath, sourcePath)) {
|
|
205
|
-
|
|
204
|
+
if (isHmrOutputOlderThanSource(filepath, sourcePath)) {
|
|
205
|
+
appLogger.warn(
|
|
206
|
+
`[JsHmrStrategy] HMR output is older than source after rebuild; skipping broadcast for ${url}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
206
209
|
return { success: false, requiresReload: false };
|
|
207
210
|
}
|
|
208
211
|
const code = await fileSystem.readFile(filepath);
|
package/src/services/assets/asset-processing-service/processors/script/file-script.processor.d.ts
CHANGED
|
@@ -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
|
}
|