ezfw-core 1.0.78 → 1.0.80

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.
@@ -11,6 +11,7 @@ export interface RenderContext {
11
11
  islands: IslandData[];
12
12
  props?: Record<string, unknown>;
13
13
  serverData?: Record<string, unknown>;
14
+ insideIsland?: boolean;
14
15
  }
15
16
  export interface IslandData {
16
17
  id: string;
@@ -108,7 +109,8 @@ export declare class StaticHtmlRenderer {
108
109
  */
109
110
  private serializableProps;
110
111
  /**
111
- * Reset island counter (for testing)
112
+ * Reset island counter and clear analyzer cache
113
+ * Called before each SSR render to ensure fresh analysis
112
114
  */
113
115
  resetIslandCounter(): void;
114
116
  }
@@ -36,7 +36,6 @@ let islandIdCounter = 0;
36
36
  * Main static renderer class
37
37
  */
38
38
  export class StaticHtmlRenderer {
39
- analyzer;
40
39
  constructor() {
41
40
  this.analyzer = analyzer;
42
41
  }
@@ -76,8 +75,15 @@ export class StaticHtmlRenderer {
76
75
  // Try to render as native HTML element
77
76
  return this.renderNativeElement(name, props, ctx);
78
77
  }
78
+ // If we're inside an island or template-based component, render children as static
79
+ // (they will be re-rendered when the parent hydrates or when SPA takes over)
80
+ if (ctx.insideIsland || ctx.insideTemplate) {
81
+ console.log(`[SSR] ${name} rendered as static (inside ${ctx.insideIsland ? 'island' : 'template'})`);
82
+ return this.renderStaticComponent(name, definition, props, ctx);
83
+ }
79
84
  // Analyze the component
80
85
  const analysis = this.analyzer.analyze(name, definition);
86
+ console.log(`[SSR] ${name} analyzed as: ${analysis.type}`);
81
87
  if (analysis.type === 'island') {
82
88
  // Render as island placeholder
83
89
  return this.renderIslandPlaceholder(name, definition, props, ctx);
@@ -94,6 +100,13 @@ export class StaticHtmlRenderer {
94
100
  ctx.styles.add(definition.css);
95
101
  }
96
102
  let config;
103
+ // If this component has a template, mark context so children render as static
104
+ // Template-based components are re-rendered by SPA, so their children don't need island markers
105
+ const hasTemplate = !!definition.template;
106
+ const childCtx = hasTemplate ? { ...ctx, insideTemplate: true } : ctx;
107
+ if (hasTemplate) {
108
+ console.log(`[SSR] ${name} has template, setting insideTemplate=true for children`);
109
+ }
97
110
  // Get component config from template or items
98
111
  if (definition.template) {
99
112
  // Get controller state for SSR
@@ -138,8 +151,8 @@ export class StaticHtmlRenderer {
138
151
  config[key] = definition[key];
139
152
  }
140
153
  }
141
- // Render the config tree
142
- return this.renderConfig(config, ctx);
154
+ // Render the config tree using childCtx (which has insideTemplate=true if this has a template)
155
+ return this.renderConfig(config, childCtx);
143
156
  }
144
157
  /**
145
158
  * Render an island as a placeholder with hydration data
@@ -158,7 +171,10 @@ export class StaticHtmlRenderer {
158
171
  };
159
172
  ctx.islands.push(islandData);
160
173
  // Render the full static content (for SEO)
161
- const staticContent = await this.renderStaticComponent(name, definition, props, ctx);
174
+ // Mark that we're inside an island so child components don't become islands
175
+ // (they will be re-rendered when this island hydrates on the client)
176
+ const islandCtx = { ...ctx, insideIsland: true };
177
+ const staticContent = await this.renderStaticComponent(name, definition, props, islandCtx);
162
178
  // Wrap in island container with data attributes for hydration
163
179
  const propsJson = this.escapeAttr(JSON.stringify(islandData.props));
164
180
  return `<div data-ez-island="${name}" data-ez-island-id="${islandId}" data-ez-props="${propsJson}">${staticContent}</div>`;
@@ -447,10 +463,12 @@ hydrateIslands(window.__EZ_ISLANDS__);
447
463
  return result;
448
464
  }
449
465
  /**
450
- * Reset island counter (for testing)
466
+ * Reset island counter and clear analyzer cache
467
+ * Called before each SSR render to ensure fresh analysis
451
468
  */
452
469
  resetIslandCounter() {
453
470
  islandIdCounter = 0;
471
+ this.analyzer.clearCache();
454
472
  }
455
473
  }
456
474
  // Export singleton
@@ -19,6 +19,12 @@ export interface RenderContext {
19
19
  props?: Record<string, unknown>;
20
20
  // Server-side data loaded via load()
21
21
  serverData?: Record<string, unknown>;
22
+ // True when rendering inside an island - children should not be islands
23
+ // (they will be re-rendered when the parent island hydrates)
24
+ insideIsland?: boolean;
25
+ // True when rendering inside a template-based component
26
+ // Template components are re-rendered by SPA, so children don't need islands
27
+ insideTemplate?: boolean;
22
28
  }
23
29
 
24
30
  export interface IslandData {
@@ -150,8 +156,16 @@ export class StaticHtmlRenderer {
150
156
  return this.renderNativeElement(name, props as ComponentConfig, ctx);
151
157
  }
152
158
 
159
+ // If we're inside an island or template-based component, render children as static
160
+ // (they will be re-rendered when the parent hydrates or when SPA takes over)
161
+ if (ctx.insideIsland || ctx.insideTemplate) {
162
+ console.log(`[SSR] ${name} rendered as static (inside ${ctx.insideIsland ? 'island' : 'template'})`);
163
+ return this.renderStaticComponent(name, definition, props, ctx);
164
+ }
165
+
153
166
  // Analyze the component
154
167
  const analysis = this.analyzer.analyze(name, definition);
168
+ console.log(`[SSR] ${name} analyzed as: ${analysis.type}`);
155
169
 
156
170
  if (analysis.type === 'island') {
157
171
  // Render as island placeholder
@@ -178,6 +192,14 @@ export class StaticHtmlRenderer {
178
192
 
179
193
  let config: ComponentConfig;
180
194
 
195
+ // If this component has a template, mark context so children render as static
196
+ // Template-based components are re-rendered by SPA, so their children don't need island markers
197
+ const hasTemplate = !!definition.template;
198
+ const childCtx = hasTemplate ? { ...ctx, insideTemplate: true } : ctx;
199
+ if (hasTemplate) {
200
+ console.log(`[SSR] ${name} has template, setting insideTemplate=true for children`);
201
+ }
202
+
181
203
  // Get component config from template or items
182
204
  if (definition.template) {
183
205
  // Get controller state for SSR
@@ -224,8 +246,8 @@ export class StaticHtmlRenderer {
224
246
  }
225
247
  }
226
248
 
227
- // Render the config tree
228
- return this.renderConfig(config, ctx);
249
+ // Render the config tree using childCtx (which has insideTemplate=true if this has a template)
250
+ return this.renderConfig(config, childCtx);
229
251
  }
230
252
 
231
253
  /**
@@ -252,7 +274,10 @@ export class StaticHtmlRenderer {
252
274
  ctx.islands.push(islandData);
253
275
 
254
276
  // Render the full static content (for SEO)
255
- const staticContent = await this.renderStaticComponent(name, definition, props, ctx);
277
+ // Mark that we're inside an island so child components don't become islands
278
+ // (they will be re-rendered when this island hydrates on the client)
279
+ const islandCtx = { ...ctx, insideIsland: true };
280
+ const staticContent = await this.renderStaticComponent(name, definition, props, islandCtx);
256
281
 
257
282
  // Wrap in island container with data attributes for hydration
258
283
  const propsJson = this.escapeAttr(JSON.stringify(islandData.props));
@@ -554,10 +579,12 @@ hydrateIslands(window.__EZ_ISLANDS__);
554
579
  }
555
580
 
556
581
  /**
557
- * Reset island counter (for testing)
582
+ * Reset island counter and clear analyzer cache
583
+ * Called before each SSR render to ensure fresh analysis
558
584
  */
559
585
  resetIslandCounter(): void {
560
586
  islandIdCounter = 0;
587
+ this.analyzer.clearCache();
561
588
  }
562
589
  }
563
590
 
@@ -665,6 +665,8 @@ async function renderPageDev(page, server, opts, renderer, islands, root) {
665
665
  islands: [],
666
666
  props: {}
667
667
  };
668
+ // Reset island counter before each render to ensure consistent IDs
669
+ renderer.resetIslandCounter();
668
670
  // Render body
669
671
  const body = await renderer.renderPage(page.name, definition, ctx);
670
672
  // Generate hydration script (pass CSS modules collected during render)
@@ -754,6 +756,8 @@ async function renderPageBuild(page, opts, renderer, analyzer, root, islands) {
754
756
  islands: [],
755
757
  props: {}
756
758
  };
759
+ // Reset island counter before each render to ensure consistent IDs
760
+ renderer.resetIslandCounter();
757
761
  // Render body
758
762
  const body = await renderer.renderPage(page.name, definition, ctx);
759
763
  // Generate hydration script (pass CSS modules collected during render)
@@ -857,6 +857,9 @@ async function renderPageDev(
857
857
  props: {}
858
858
  };
859
859
 
860
+ // Reset island counter before each render to ensure consistent IDs
861
+ renderer.resetIslandCounter();
862
+
860
863
  // Render body
861
864
  const body = await renderer.renderPage(page.name, definition, ctx);
862
865
 
@@ -965,6 +968,9 @@ async function renderPageBuild(
965
968
  props: {}
966
969
  };
967
970
 
971
+ // Reset island counter before each render to ensure consistent IDs
972
+ renderer.resetIslandCounter();
973
+
968
974
  // Render body
969
975
  const body = await renderer.renderPage(page.name, definition, ctx);
970
976
 
@@ -47,7 +47,8 @@ export interface ComponentDefinition {
47
47
  export declare class ComponentAnalyzer {
48
48
  private cache;
49
49
  /**
50
- * Analyze a component definition and determine its type
50
+ * Analyze a component definition and determine its type.
51
+ * Cache is cleared by StaticHtmlRenderer.resetIslandCounter() before each SSR render.
51
52
  */
52
53
  analyze(name: string, definition: ComponentDefinition): AnalysisResult;
53
54
  /**
@@ -58,9 +58,12 @@ const BROWSER_API_PATTERNS = [
58
58
  * Main analyzer class
59
59
  */
60
60
  export class ComponentAnalyzer {
61
- cache = new Map();
61
+ constructor() {
62
+ this.cache = new Map();
63
+ }
62
64
  /**
63
- * Analyze a component definition and determine its type
65
+ * Analyze a component definition and determine its type.
66
+ * Cache is cleared by StaticHtmlRenderer.resetIslandCounter() before each SSR render.
64
67
  */
65
68
  analyze(name, definition) {
66
69
  // Check cache first
@@ -95,7 +95,8 @@ export class ComponentAnalyzer {
95
95
  private cache: Map<string, AnalysisResult> = new Map();
96
96
 
97
97
  /**
98
- * Analyze a component definition and determine its type
98
+ * Analyze a component definition and determine its type.
99
+ * Cache is cleared by StaticHtmlRenderer.resetIslandCounter() before each SSR render.
99
100
  */
100
101
  analyze(name: string, definition: ComponentDefinition): AnalysisResult {
101
102
  // Check cache first
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezfw-core",
3
- "version": "1.0.78",
3
+ "version": "1.0.80",
4
4
  "description": "Ez Framework - A declarative component framework for building modern web applications",
5
5
  "type": "module",
6
6
  "main": "./core/ez.ts",