@ecopages/core
Advanced tools
| /** | ||
| * Per-app registry for the shared HMR manager used by host-owned dev clients. | ||
| */ | ||
| import type { EcoPagesAppConfig } from '../types/public-types.js'; | ||
| import type { IHmrManager } from '../types/public-types.js'; | ||
| export declare function setAppHmrManager(appConfig: EcoPagesAppConfig, hmrManager: IHmrManager): void; | ||
| export declare function getAppHmrManager(appConfig: EcoPagesAppConfig): IHmrManager | undefined; | ||
| export declare function clearAppHmrManager(appConfig: EcoPagesAppConfig): void; |
| const appHmrManagers = /* @__PURE__ */ new WeakMap(); | ||
| function setAppHmrManager(appConfig, hmrManager) { | ||
| appHmrManagers.set(appConfig, hmrManager); | ||
| } | ||
| function getAppHmrManager(appConfig) { | ||
| return appHmrManagers.get(appConfig); | ||
| } | ||
| function clearAppHmrManager(appConfig) { | ||
| appHmrManagers.delete(appConfig); | ||
| } | ||
| export { | ||
| clearAppHmrManager, | ||
| getAppHmrManager, | ||
| setAppHmrManager | ||
| }; |
| export declare function encodeHmrDynamicSegments(filepath: string): string; | ||
| /** | ||
| * Resolves the on-disk and browser URL targets for one HMR script entrypoint. | ||
| */ | ||
| export declare function resolveHmrEntrypointOutputPaths(srcDir: string, distDir: string, entrypointPath: string): { | ||
| outputPath: string; | ||
| outputUrl: string; | ||
| }; | ||
| /** | ||
| * @remarks | ||
| * Rebuilds must delete the previous artifact so browser-hmr does not reuse a | ||
| * stale bundle when the source file changed. | ||
| */ | ||
| export declare function removeStaleHmrEntrypointOutput(outputPath: string, scope: string): void; |
| import path from "node:path"; | ||
| import { fileSystem } from "@ecopages/file-system"; | ||
| import { RESOLVED_ASSETS_DIR } from "../config/constants.js"; | ||
| import { appLogger } from "../global/app-logger.js"; | ||
| function encodeHmrDynamicSegments(filepath) { | ||
| return filepath.replace(/\[([^\]]+)\]/g, "_$1_"); | ||
| } | ||
| function resolveHmrEntrypointOutputPaths(srcDir, distDir, entrypointPath) { | ||
| const normalizedEntrypoint = path.resolve(entrypointPath); | ||
| const relativePath = path.relative(srcDir, normalizedEntrypoint); | ||
| const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, ".js"); | ||
| const encodedPathJs = encodeHmrDynamicSegments(relativePathJs); | ||
| const urlPath = encodedPathJs.split(path.sep).join("/"); | ||
| return { | ||
| outputUrl: `/${path.join(RESOLVED_ASSETS_DIR, "_hmr", urlPath).split(path.sep).join("/")}`, | ||
| outputPath: path.join(distDir, urlPath) | ||
| }; | ||
| } | ||
| function removeStaleHmrEntrypointOutput(outputPath, scope) { | ||
| if (!fileSystem.exists(outputPath)) { | ||
| return; | ||
| } | ||
| try { | ||
| fileSystem.remove(outputPath); | ||
| } catch (error) { | ||
| appLogger.warn( | ||
| `[${scope}] Failed to remove stale entrypoint output ${outputPath}: ${error instanceof Error ? error.message : String(error)}` | ||
| ); | ||
| } | ||
| } | ||
| export { | ||
| encodeHmrDynamicSegments, | ||
| removeStaleHmrEntrypointOutput, | ||
| resolveHmrEntrypointOutputPaths | ||
| }; |
+10
-2
| { | ||
| "name": "@ecopages/core", | ||
| "version": "0.2.0-beta.17", | ||
| "version": "0.2.0-beta.18", | ||
| "description": "Core package for Ecopages", | ||
@@ -20,3 +20,3 @@ "keywords": [ | ||
| "dependencies": { | ||
| "@ecopages/file-system": "0.2.0-beta.17", | ||
| "@ecopages/file-system": "0.2.0-beta.18", | ||
| "@ecopages/logger": "^0.2.3", | ||
@@ -137,2 +137,6 @@ "@ecopages/scripts-injector": "^0.1.5", | ||
| }, | ||
| "./dev/hmr-manager-registry": { | ||
| "types": "./src/dev/hmr-manager-registry.d.ts", | ||
| "default": "./src/dev/hmr-manager-registry.js" | ||
| }, | ||
| "./dev/dev-client-ownership": { | ||
@@ -343,2 +347,6 @@ "types": "./src/dev/dev-client-ownership.d.ts", | ||
| }, | ||
| "./dev/hmr-manager-registry.ts": { | ||
| "types": "./src/dev/hmr-manager-registry.d.ts", | ||
| "default": "./src/dev/hmr-manager-registry.js" | ||
| }, | ||
| "./dev/dev-client-ownership.ts": { | ||
@@ -345,0 +353,0 @@ "types": "./src/dev/dev-client-ownership.d.ts", |
@@ -27,2 +27,3 @@ import path from "node:path"; | ||
| import { setAppDevClientBridge } from "../../dev/client-bridge-registry.js"; | ||
| import { setAppHmrManager } from "../../dev/hmr-manager-registry.js"; | ||
| import { HmrManager } from "./hmr-manager.js"; | ||
@@ -604,2 +605,3 @@ import { BunStaticPreviewHost } from "./static-preview-host.js"; | ||
| setAppDevClientBridge(params.appConfig, bridge); | ||
| setAppHmrManager(params.appConfig, hmrManager); | ||
| const previewHost = params.previewHost ?? new BunStaticPreviewHost(); | ||
@@ -606,0 +608,0 @@ const adapter = new BunServerAdapter({ |
| import { WebSocketServer } from "ws"; | ||
| import { setAppDevClientBridge } from "../../dev/client-bridge-registry.js"; | ||
| import { setAppHmrManager } from "../../dev/hmr-manager-registry.js"; | ||
| import { NodeClientBridge } from "./node-client-bridge.js"; | ||
@@ -11,2 +12,3 @@ import { NodeHmrManager } from "./node-hmr-manager.js"; | ||
| setAppDevClientBridge(options.appConfig, bridge); | ||
| setAppHmrManager(options.appConfig, hmrManager); | ||
| return { | ||
@@ -13,0 +15,0 @@ websocketServer, |
@@ -53,4 +53,2 @@ /** | ||
| private getEntrypointOutput; | ||
| private removeStaleEntrypointOutput; | ||
| private encodeDynamicSegments; | ||
| } |
| import path from "node:path"; | ||
| import { fileSystem } from "@ecopages/file-system"; | ||
| import { appLogger } from "../../global/app-logger.js"; | ||
| import { RESOLVED_ASSETS_DIR } from "../../config/constants.js"; | ||
| import { removeStaleHmrEntrypointOutput, resolveHmrEntrypointOutputPaths } from "../../hmr/hmr-entrypoint-output.js"; | ||
| class HmrEntrypointRegistrar { | ||
@@ -39,3 +38,3 @@ options; | ||
| this.options.watchedFiles.set(entrypointPath, outputUrl); | ||
| this.removeStaleEntrypointOutput(outputPath); | ||
| removeStaleHmrEntrypointOutput(outputPath, "HMR"); | ||
| await registrationOptions.emit(entrypointPath, outputPath); | ||
@@ -61,26 +60,4 @@ if (!fileSystem.exists(outputPath)) { | ||
| getEntrypointOutput(entrypointPath) { | ||
| const relativePath = path.relative(this.options.srcDir, entrypointPath); | ||
| const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, ".js"); | ||
| const encodedPathJs = this.encodeDynamicSegments(relativePathJs); | ||
| const urlPath = encodedPathJs.split(path.sep).join("/"); | ||
| return { | ||
| outputUrl: `/${path.join(RESOLVED_ASSETS_DIR, "_hmr", urlPath)}`, | ||
| outputPath: path.join(this.options.distDir, urlPath) | ||
| }; | ||
| return resolveHmrEntrypointOutputPaths(this.options.srcDir, this.options.distDir, entrypointPath); | ||
| } | ||
| removeStaleEntrypointOutput(outputPath) { | ||
| if (!fileSystem.exists(outputPath)) { | ||
| return; | ||
| } | ||
| try { | ||
| fileSystem.remove(outputPath); | ||
| } catch (error) { | ||
| appLogger.warn( | ||
| `[HMR] Failed to remove stale entrypoint output ${outputPath}: ${error instanceof Error ? error.message : String(error)}` | ||
| ); | ||
| } | ||
| } | ||
| encodeDynamicSegments(filepath) { | ||
| return filepath.replace(/\[([^\]]+)\]/g, "_$1_"); | ||
| } | ||
| } | ||
@@ -87,0 +64,0 @@ export { |
@@ -7,2 +7,4 @@ import path from "node:path"; | ||
| import { copyRuntimePublicDirIfChanged } from "./copy-runtime-public-dir.js"; | ||
| import { clearAppDevClientBridge } from "../../dev/client-bridge-registry.js"; | ||
| import { clearAppHmrManager } from "../../dev/hmr-manager-registry.js"; | ||
| import { injectHmrRuntimeIntoHtmlResponse, isHtmlResponse, shouldInjectHmrHtmlResponse } from "./hmr-html-response.js"; | ||
@@ -33,2 +35,4 @@ function prepareRuntimePublicDir(appConfig) { | ||
| options.bridge?.destroy(); | ||
| clearAppDevClientBridge(options.appConfig); | ||
| clearAppHmrManager(options.appConfig); | ||
| await options.previewHost.stop(); | ||
@@ -35,0 +39,0 @@ } |
@@ -50,6 +50,8 @@ import fs from 'node:fs'; | ||
| /** | ||
| * Returns the emitted HMR script output when the artifact already exists on disk. | ||
| * Returns the emitted HMR script output when the entrypoint is already registered | ||
| * and its browser bundle exists on disk. | ||
| * | ||
| * SSR must not block on entrypoint registration when a previous build already | ||
| * produced the browser bundle. | ||
| * @remarks | ||
| * Disk artifacts alone are not enough: a fresh dev session must still register | ||
| * the entrypoint so file watchers can rebuild it on change. | ||
| */ | ||
@@ -56,0 +58,0 @@ getResolvedScriptOutput(entrypointPath: string): { |
@@ -5,2 +5,3 @@ import fs from "node:fs"; | ||
| import { RESOLVED_ASSETS_DIR } from "../../config/constants.js"; | ||
| import { resolveHmrEntrypointOutputPaths } from "../../hmr/hmr-entrypoint-output.js"; | ||
| import { requireBuildRuntime } from "../../build/build-runtime.js"; | ||
@@ -188,17 +189,23 @@ import { fileSystem } from "@ecopages/file-system"; | ||
| /** | ||
| * Returns the emitted HMR script output when the artifact already exists on disk. | ||
| * Returns the emitted HMR script output when the entrypoint is already registered | ||
| * and its browser bundle exists on disk. | ||
| * | ||
| * SSR must not block on entrypoint registration when a previous build already | ||
| * produced the browser bundle. | ||
| * @remarks | ||
| * Disk artifacts alone are not enough: a fresh dev session must still register | ||
| * the entrypoint so file watchers can rebuild it on change. | ||
| */ | ||
| getResolvedScriptOutput(entrypointPath) { | ||
| const normalizedEntrypoint = path.resolve(entrypointPath); | ||
| const relativePath = path.relative(this.appConfig.absolutePaths.srcDir, normalizedEntrypoint); | ||
| const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, ".js").replace(/\[([^\]]+)\]/g, "_$1_"); | ||
| const urlPath = relativePathJs.split(path.sep).join("/"); | ||
| const outputPath = path.join(this.distDir, urlPath); | ||
| if (!this.watchedFiles.has(normalizedEntrypoint)) { | ||
| return void 0; | ||
| } | ||
| const { outputPath, outputUrl: derivedOutputUrl } = resolveHmrEntrypointOutputPaths( | ||
| this.appConfig.absolutePaths.srcDir, | ||
| this.distDir, | ||
| normalizedEntrypoint | ||
| ); | ||
| if (!fileSystem.exists(outputPath)) { | ||
| return void 0; | ||
| } | ||
| const outputUrl = this.watchedFiles.get(normalizedEntrypoint) ?? `/${path.join(RESOLVED_ASSETS_DIR, "_hmr", urlPath).split(path.sep).join("/")}`; | ||
| const outputUrl = this.watchedFiles.get(normalizedEntrypoint) ?? derivedOutputUrl; | ||
| return { outputUrl, outputPath }; | ||
@@ -205,0 +212,0 @@ } |
@@ -101,2 +101,3 @@ /** | ||
| * 2. The changed file is a JS/TS file in the src directory | ||
| * 3. Registered entrypoints always match, even when they share an integration template extension | ||
| * | ||
@@ -120,2 +121,4 @@ * @param filePath - Absolute path to the changed file | ||
| process(filePath: string): Promise<HmrAction>; | ||
| private resolveEntrypointOutputPath; | ||
| private removeStaleEntrypointOutput; | ||
| /** | ||
@@ -122,0 +125,0 @@ * Bundles one or more entrypoints in a single build invocation. |
@@ -5,2 +5,3 @@ import path from "node:path"; | ||
| import { appLogger } from "../../global/app-logger.js"; | ||
| import { removeStaleHmrEntrypointOutput, resolveHmrEntrypointOutputPaths } from "../hmr-entrypoint-output.js"; | ||
| class JsHmrStrategy extends HmrStrategy { | ||
@@ -19,2 +20,3 @@ type = HmrStrategyType.SCRIPT; | ||
| * 2. The changed file is a JS/TS file in the src directory | ||
| * 3. Registered entrypoints always match, even when they share an integration template extension | ||
| * | ||
@@ -26,5 +28,6 @@ * @param filePath - Absolute path to the changed file | ||
| const watchedFiles = this.context.getWatchedFiles(); | ||
| const isJsTs = /\.(ts|tsx|js|jsx)$/.test(filePath); | ||
| const isInSrc = filePath.startsWith(this.context.getSrcDir()); | ||
| const isIntegrationTemplate = this.context.getTemplateExtensions().some((extension) => filePath.endsWith(extension)); | ||
| const resolvedPath = path.resolve(filePath); | ||
| const isJsTs = /\.(ts|tsx|js|jsx)$/.test(resolvedPath); | ||
| const isInSrc = resolvedPath.startsWith(this.context.getSrcDir()); | ||
| const isIntegrationTemplate = this.context.getTemplateExtensions().some((extension) => resolvedPath.endsWith(extension)); | ||
| if (watchedFiles.size === 0) { | ||
@@ -36,8 +39,8 @@ return false; | ||
| } | ||
| if (watchedFiles.has(resolvedPath)) { | ||
| return true; | ||
| } | ||
| if (isIntegrationTemplate) { | ||
| return false; | ||
| } | ||
| if (watchedFiles.has(filePath)) { | ||
| return true; | ||
| } | ||
| return true; | ||
@@ -66,3 +69,3 @@ } | ||
| const hasDependencyHit = dependencyHits.size > 0; | ||
| const impactedEntrypoints = hasDependencyHit ? Array.from(dependencyHits).filter((entrypoint) => watchedFiles.has(entrypoint)) : Array.from(watchedFiles.keys()); | ||
| const impactedEntrypoints = hasDependencyHit ? Array.from(dependencyHits).filter((entrypoint) => watchedFiles.has(path.resolve(entrypoint))) : Array.from(watchedFiles.keys()); | ||
| const buildableEntrypoints = impactedEntrypoints.filter( | ||
@@ -77,2 +80,5 @@ (entrypoint) => this.context.shouldProcessEntrypoint?.(entrypoint) ?? true | ||
| } | ||
| for (const entrypoint of buildableEntrypoints) { | ||
| this.removeStaleEntrypointOutput(this.resolveEntrypointOutputPath(entrypoint)); | ||
| } | ||
| const buildResult = await this.bundleEntrypoints(buildableEntrypoints); | ||
@@ -91,6 +97,3 @@ if (!buildResult.success) { | ||
| } | ||
| const srcDir = this.context.getSrcDir(); | ||
| const relativePath = path.relative(srcDir, entrypoint); | ||
| const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, ".js"); | ||
| const outputPath = path.join(this.context.getDistDir(), relativePathJs); | ||
| const outputPath = this.resolveEntrypointOutputPath(entrypoint); | ||
| const result = await this.processOutput(outputPath, outputUrl); | ||
@@ -123,2 +126,8 @@ if (result.success) { | ||
| } | ||
| resolveEntrypointOutputPath(entrypointPath) { | ||
| return resolveHmrEntrypointOutputPaths(this.context.getSrcDir(), this.context.getDistDir(), entrypointPath).outputPath; | ||
| } | ||
| removeStaleEntrypointOutput(outputPath) { | ||
| removeStaleHmrEntrypointOutput(outputPath, "JsHmrStrategy"); | ||
| } | ||
| /** | ||
@@ -131,2 +140,26 @@ * Bundles one or more entrypoints in a single build invocation. | ||
| try { | ||
| if (entrypoints.length === 1) { | ||
| const entrypoint = entrypoints[0]; | ||
| const outputPath = this.resolveEntrypointOutputPath(entrypoint); | ||
| const naming = path.relative(this.context.getDistDir(), outputPath).split(path.sep).join("/"); | ||
| const result2 = await this.context.getBrowserBundleService().bundle({ | ||
| profile: "hmr-entrypoint", | ||
| entrypoints: [entrypoint], | ||
| outdir: this.context.getDistDir(), | ||
| naming, | ||
| plugins: this.context.getPlugins(), | ||
| minify: false | ||
| }); | ||
| if (!result2.success) { | ||
| appLogger.error("[JsHmrStrategy] Entrypoint build failed:", result2.logs); | ||
| return { success: false }; | ||
| } | ||
| const dependencies2 = /* @__PURE__ */ new Map(); | ||
| if (result2.dependencyGraph?.entrypoints) { | ||
| for (const [resolvedEntrypoint, deps] of Object.entries(result2.dependencyGraph.entrypoints)) { | ||
| dependencies2.set(path.resolve(resolvedEntrypoint), deps); | ||
| } | ||
| } | ||
| return { success: true, dependencies: dependencies2 }; | ||
| } | ||
| const result = await this.context.getBrowserBundleService().bundle({ | ||
@@ -133,0 +166,0 @@ profile: "hmr-entrypoint", |
1299361
0.36%479
0.84%31685
0.32%+ Added
- Removed