@ecopages/core 0.2.0-alpha.1 → 0.2.0-alpha.3

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/CHANGELOG.md CHANGED
@@ -65,6 +65,8 @@ All notable changes to `@ecopages/core` are documented here.
65
65
  - Fixed invariant checks for route paths with improved error messaging in `AbstractApplicationAdapter` (`9c2a6242`).
66
66
  - Fixed dependency import name extraction in `extractEcopagesVirtualImports` (`39bbc472`).
67
67
  - Removed an invalid npm export entry that pointed to a non-existent `utils/ecopages-url-resolver` declaration target.
68
+ - Kept source module HMR active when stylesheet processors also watch TSX and JSX files for Tailwind-driven CSS rebuilds.
69
+ - Triggered HMR current-page refreshes instead of fallback reload suppression for processor-watched TSX and JSX changes that are not client entrypoints.
68
70
 
69
71
  ### Tests
70
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-alpha.1",
3
+ "version": "0.2.0-alpha.3",
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-alpha.1",
20
+ "@ecopages/file-system": "0.2.0-alpha.3",
21
21
  "@ecopages/logger": "latest",
22
22
  "@ecopages/scripts-injector": "^0.1.3",
23
23
  "@worker-tools/html-rewriter": "0.1.0-pre.19",
@@ -58,6 +58,7 @@ export declare class HmrManager implements IHmrManager {
58
58
  buildRuntime(): Promise<void>;
59
59
  getRuntimePath(): string;
60
60
  broadcast(event: ClientBridgeEvent): void;
61
+ canHandleFileChange(filePath: string): boolean;
61
62
  /**
62
63
  * Handles file changes using registered HMR strategies.
63
64
  * Strategies are evaluated in priority order until one matches.
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { RESOLVED_ASSETS_DIR } from "../../constants";
4
4
  import { defaultBuildAdapter } from "../../build/build-adapter.js";
5
5
  import { fileSystem } from "@ecopages/file-system";
6
+ import { HmrStrategyType } from "../../hmr/hmr-strategy";
6
7
  import { DefaultHmrStrategy } from "../../hmr/strategies/default-hmr-strategy";
7
8
  import { JsHmrStrategy } from "../../hmr/strategies/js-hmr-strategy";
8
9
  import { appLogger } from "../../global/app-logger";
@@ -121,6 +122,18 @@ class HmrManager {
121
122
  );
122
123
  this.bridge.broadcast(event);
123
124
  }
125
+ canHandleFileChange(filePath) {
126
+ const sorted = [...this.strategies].sort((a, b) => b.priority - a.priority);
127
+ const strategy = sorted.find((candidate) => {
128
+ try {
129
+ return candidate.matches(filePath);
130
+ } catch (err) {
131
+ appLogger.error(`[HmrManager] Error checking match for ${candidate.constructor.name}:`, err);
132
+ return false;
133
+ }
134
+ });
135
+ return strategy !== void 0 && strategy.type !== HmrStrategyType.FALLBACK;
136
+ }
124
137
  /**
125
138
  * Handles file changes using registered HMR strategies.
126
139
  * Strategies are evaluated in priority order until one matches.
@@ -7,6 +7,7 @@ import type { DefaultHmrContext, EcoPagesAppConfig, IHmrManager } from '../../in
7
7
  import type { EcoBuildPlugin } from '../../build/build-types.ts';
8
8
  import { fileSystem } from '@ecopages/file-system';
9
9
  import type { HmrStrategy } from '../../hmr/hmr-strategy';
10
+ import { HmrStrategyType } from '../../hmr/hmr-strategy';
10
11
  import { DefaultHmrStrategy } from '../../hmr/strategies/default-hmr-strategy';
11
12
  import { JsHmrStrategy } from '../../hmr/strategies/js-hmr-strategy';
12
13
  import { appLogger } from '../../global/app-logger';
@@ -158,6 +159,20 @@ export class HmrManager implements IHmrManager {
158
159
  this.bridge.broadcast(event);
159
160
  }
160
161
 
162
+ public canHandleFileChange(filePath: string): boolean {
163
+ const sorted = [...this.strategies].sort((a, b) => b.priority - a.priority);
164
+ const strategy = sorted.find((candidate) => {
165
+ try {
166
+ return candidate.matches(filePath);
167
+ } catch (err) {
168
+ appLogger.error(`[HmrManager] Error checking match for ${candidate.constructor.name}:`, err as Error);
169
+ return false;
170
+ }
171
+ });
172
+
173
+ return strategy !== undefined && strategy.type !== HmrStrategyType.FALLBACK;
174
+ }
175
+
161
176
  /**
162
177
  * Handles file changes using registered HMR strategies.
163
178
  * Strategies are evaluated in priority order until one matches.
@@ -41,6 +41,7 @@ export declare class NodeHmrManager implements IHmrManager {
41
41
  buildRuntime(): Promise<void>;
42
42
  getRuntimePath(): string;
43
43
  broadcast(event: ClientBridgeEvent): void;
44
+ canHandleFileChange(filePath: string): boolean;
44
45
  handleFileChange(filePath: string): Promise<void>;
45
46
  getOutputUrl(entrypointPath: string): string | undefined;
46
47
  getWatchedFiles(): Map<string, string>;
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { RESOLVED_ASSETS_DIR } from "../../constants.js";
5
5
  import { defaultBuildAdapter } from "../../build/build-adapter.js";
6
6
  import { fileSystem } from "@ecopages/file-system";
7
+ import { HmrStrategyType } from "../../hmr/hmr-strategy.js";
7
8
  import { DefaultHmrStrategy } from "../../hmr/strategies/default-hmr-strategy.js";
8
9
  import { JsHmrStrategy } from "../../hmr/strategies/js-hmr-strategy.js";
9
10
  import { appLogger } from "../../global/app-logger.js";
@@ -94,6 +95,21 @@ class NodeHmrManager {
94
95
  );
95
96
  this.bridge.broadcast(event);
96
97
  }
98
+ canHandleFileChange(filePath) {
99
+ const sorted = [...this.strategies].sort((a, b) => b.priority - a.priority);
100
+ const strategy = sorted.find((candidate) => {
101
+ try {
102
+ return candidate.matches(filePath);
103
+ } catch (err) {
104
+ appLogger.error(
105
+ `[NodeHmrManager] Error checking match for ${candidate.constructor.name}:`,
106
+ err
107
+ );
108
+ return false;
109
+ }
110
+ });
111
+ return strategy !== void 0 && strategy.type !== HmrStrategyType.FALLBACK;
112
+ }
97
113
  async handleFileChange(filePath) {
98
114
  const sorted = [...this.strategies].sort((a, b) => b.priority - a.priority);
99
115
  const strategy = sorted.find((s) => {
@@ -7,6 +7,7 @@ import type { DefaultHmrContext, EcoPagesAppConfig, IHmrManager, IClientBridge }
7
7
  import type { EcoBuildPlugin } from '../../build/build-types.ts';
8
8
  import { fileSystem } from '@ecopages/file-system';
9
9
  import type { HmrStrategy } from '../../hmr/hmr-strategy.ts';
10
+ import { HmrStrategyType } from '../../hmr/hmr-strategy.ts';
10
11
  import { DefaultHmrStrategy } from '../../hmr/strategies/default-hmr-strategy.ts';
11
12
  import { JsHmrStrategy } from '../../hmr/strategies/js-hmr-strategy.ts';
12
13
  import { appLogger } from '../../global/app-logger.ts';
@@ -121,6 +122,23 @@ export class NodeHmrManager implements IHmrManager {
121
122
  this.bridge.broadcast(event);
122
123
  }
123
124
 
125
+ public canHandleFileChange(filePath: string): boolean {
126
+ const sorted = [...this.strategies].sort((a, b) => b.priority - a.priority);
127
+ const strategy = sorted.find((candidate) => {
128
+ try {
129
+ return candidate.matches(filePath);
130
+ } catch (err) {
131
+ appLogger.error(
132
+ `[NodeHmrManager] Error checking match for ${candidate.constructor.name}:`,
133
+ err as Error,
134
+ );
135
+ return false;
136
+ }
137
+ });
138
+
139
+ return strategy !== undefined && strategy.type !== HmrStrategyType.FALLBACK;
140
+ }
141
+
124
142
  public async handleFileChange(filePath: string): Promise<void> {
125
143
  const sorted = [...this.strategies].sort((a, b) => b.priority - a.priority);
126
144
  const strategy = sorted.find((s) => {
@@ -26,7 +26,16 @@ class JsHmrStrategy extends HmrStrategy {
26
26
  if (watchedFiles.size === 0) {
27
27
  return false;
28
28
  }
29
- return isJsTs && isInSrc;
29
+ if (!isJsTs || !isInSrc) {
30
+ return false;
31
+ }
32
+ if (watchedFiles.has(filePath)) {
33
+ return true;
34
+ }
35
+ if (this.context.getDependencyEntrypoints) {
36
+ return this.context.getDependencyEntrypoints(filePath).size > 0;
37
+ }
38
+ return true;
30
39
  }
31
40
  /**
32
41
  * Processes a file change by rebuilding affected entrypoints.
@@ -121,7 +121,19 @@ export class JsHmrStrategy extends HmrStrategy {
121
121
  return false;
122
122
  }
123
123
 
124
- return isJsTs && isInSrc;
124
+ if (!isJsTs || !isInSrc) {
125
+ return false;
126
+ }
127
+
128
+ if (watchedFiles.has(filePath)) {
129
+ return true;
130
+ }
131
+
132
+ if (this.context.getDependencyEntrypoints) {
133
+ return this.context.getDependencyEntrypoints(filePath).size > 0;
134
+ }
135
+
136
+ return true;
125
137
  }
126
138
 
127
139
  /**
@@ -178,6 +178,10 @@ export interface IHmrManager {
178
178
  * Returns whether HMR is enabled.
179
179
  */
180
180
  isEnabled(): boolean;
181
+ /**
182
+ * Returns true when a changed file matches a non-fallback HMR strategy.
183
+ */
184
+ canHandleFileChange(path: string): boolean;
181
185
  /**
182
186
  * Broadcasts an HMR event to connected clients.
183
187
  */
@@ -209,6 +209,11 @@ export interface IHmrManager {
209
209
  */
210
210
  isEnabled(): boolean;
211
211
 
212
+ /**
213
+ * Returns true when a changed file matches a non-fallback HMR strategy.
214
+ */
215
+ canHandleFileChange(path: string): boolean;
216
+
212
217
  /**
213
218
  * Broadcasts an HMR event to connected clients.
214
219
  */
@@ -77,6 +77,11 @@ export declare class ProjectWatcher {
77
77
  * Checks if file path matches any additionalWatchPaths patterns.
78
78
  */
79
79
  private matchesAdditionalWatchPaths;
80
+ /**
81
+ * Checks whether a file is watched by any processor, even if that processor
82
+ * does not own the file as a primary asset.
83
+ */
84
+ private isWatchedByProcessor;
80
85
  /**
81
86
  * Checks if a file is handled by a processor.
82
87
  * Processors that declare extensions own those file types.
@@ -97,6 +97,10 @@ class ProjectWatcher {
97
97
  if (this.isHandledByProcessor(filePath)) {
98
98
  return;
99
99
  }
100
+ if (this.isWatchedByProcessor(filePath) && !this.hmrManager.canHandleFileChange(filePath)) {
101
+ this.hmrManager.broadcast({ type: "layout-update" });
102
+ return;
103
+ }
100
104
  await this.hmrManager.handleFileChange(filePath);
101
105
  } catch (error) {
102
106
  if (error instanceof Error) {
@@ -127,12 +131,35 @@ class ProjectWatcher {
127
131
  }
128
132
  return false;
129
133
  }
134
+ /**
135
+ * Checks whether a file is watched by any processor, even if that processor
136
+ * does not own the file as a primary asset.
137
+ */
138
+ isWatchedByProcessor(filePath) {
139
+ for (const processor of this.appConfig.processors.values()) {
140
+ const watchConfig = processor.getWatchConfig();
141
+ if (!watchConfig) continue;
142
+ const { extensions = [] } = watchConfig;
143
+ if (extensions.length && extensions.some((ext) => filePath.endsWith(ext))) {
144
+ return true;
145
+ }
146
+ }
147
+ return false;
148
+ }
130
149
  /**
131
150
  * Checks if a file is handled by a processor.
132
151
  * Processors that declare extensions own those file types.
133
152
  */
134
153
  isHandledByProcessor(filePath) {
135
154
  for (const processor of this.appConfig.processors.values()) {
155
+ const capabilities = processor.getAssetCapabilities?.() ?? [];
156
+ if (capabilities.length > 0) {
157
+ const matchesConfiguredAsset = typeof processor.matchesFileFilter !== "function" || processor.matchesFileFilter(filePath);
158
+ if (matchesConfiguredAsset && capabilities.some((capability) => processor.canProcessAsset?.(capability.kind, filePath))) {
159
+ return true;
160
+ }
161
+ continue;
162
+ }
136
163
  const watchConfig = processor.getWatchConfig();
137
164
  if (!watchConfig) continue;
138
165
  const { extensions = [] } = watchConfig;
@@ -14,6 +14,7 @@ const createMockHmrManager = () => ({
14
14
  registerStrategy: vi.fn(() => {
15
15
  }),
16
16
  isEnabled: vi.fn(() => true),
17
+ canHandleFileChange: vi.fn(() => true),
17
18
  getOutputUrl: vi.fn(() => void 0),
18
19
  getWatchedFiles: vi.fn(() => /* @__PURE__ */ new Map()),
19
20
  getSpecifierMap: vi.fn(() => /* @__PURE__ */ new Map()),
@@ -12,6 +12,7 @@ export const createMockHmrManager = (): IHmrManager =>
12
12
  registerSpecifierMap: vi.fn(() => {}),
13
13
  registerStrategy: vi.fn(() => {}),
14
14
  isEnabled: vi.fn(() => true),
15
+ canHandleFileChange: vi.fn(() => true),
15
16
  getOutputUrl: vi.fn(() => undefined),
16
17
  getWatchedFiles: vi.fn(() => new Map()),
17
18
  getSpecifierMap: vi.fn(() => new Map()),
@@ -144,6 +144,11 @@ export class ProjectWatcher {
144
144
  return;
145
145
  }
146
146
 
147
+ if (this.isWatchedByProcessor(filePath) && !this.hmrManager.canHandleFileChange(filePath)) {
148
+ this.hmrManager.broadcast({ type: 'layout-update' });
149
+ return;
150
+ }
151
+
147
152
  await this.hmrManager.handleFileChange(filePath);
148
153
  } catch (error) {
149
154
  if (error instanceof Error) {
@@ -178,12 +183,45 @@ export class ProjectWatcher {
178
183
  return false;
179
184
  }
180
185
 
186
+ /**
187
+ * Checks whether a file is watched by any processor, even if that processor
188
+ * does not own the file as a primary asset.
189
+ */
190
+ private isWatchedByProcessor(filePath: string): boolean {
191
+ for (const processor of this.appConfig.processors.values()) {
192
+ const watchConfig = processor.getWatchConfig();
193
+ if (!watchConfig) continue;
194
+
195
+ const { extensions = [] } = watchConfig;
196
+ if (extensions.length && extensions.some((ext) => filePath.endsWith(ext))) {
197
+ return true;
198
+ }
199
+ }
200
+
201
+ return false;
202
+ }
203
+
181
204
  /**
182
205
  * Checks if a file is handled by a processor.
183
206
  * Processors that declare extensions own those file types.
184
207
  */
185
208
  private isHandledByProcessor(filePath: string): boolean {
186
209
  for (const processor of this.appConfig.processors.values()) {
210
+ const capabilities = processor.getAssetCapabilities?.() ?? [];
211
+ if (capabilities.length > 0) {
212
+ const matchesConfiguredAsset =
213
+ typeof processor.matchesFileFilter !== 'function' || processor.matchesFileFilter(filePath);
214
+
215
+ if (
216
+ matchesConfiguredAsset &&
217
+ capabilities.some((capability) => processor.canProcessAsset?.(capability.kind, filePath))
218
+ ) {
219
+ return true;
220
+ }
221
+
222
+ continue;
223
+ }
224
+
187
225
  const watchConfig = processor.getWatchConfig();
188
226
  if (!watchConfig) continue;
189
227