@mulanjs/mulanjs 1.0.1-dev.20260220155535 → 1.0.1-dev.20260226191318

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.
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.compileSFC = void 0;
4
4
  const sfc_parser_1 = require("./sfc-parser");
5
5
  const script_compiler_1 = require("./script-compiler");
6
- const template_compiler_1 = require("./template-compiler");
6
+ const dom_compiler_1 = require("./dom-compiler");
7
7
  const style_compiler_1 = require("./style-compiler");
8
8
  const source_map_1 = require("source-map");
9
9
  async function compileSFC(source, filename, options) {
@@ -19,8 +19,8 @@ async function compileSFC(source, filename, options) {
19
19
  }
20
20
  // 3. Style
21
21
  const styleResult = (0, style_compiler_1.compileStyle)(descriptor, filename, options);
22
- // 4. Template
23
- const templateResult = (0, template_compiler_1.compileTemplate)(descriptor, scriptResult, styleResult.scopedId);
22
+ // 4. Template (Use the new No-VDOM compiler!)
23
+ const templateResult = (0, dom_compiler_1.compileToDOM)(descriptor, scriptResult, styleResult.scopedId);
24
24
  // Calculate offsets for source map merging
25
25
  // 1. Script Code
26
26
  // 2. Padding (newlines)
@@ -0,0 +1,393 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compileToDOM = void 0;
4
+ function compileToDOM(descriptor, scriptResult, scopedId) {
5
+ console.log(`[MulanJS DOM Compiler v2.0] Compiling template for: ${descriptor.filename || 'anonymous'}`);
6
+ const template = descriptor.template;
7
+ if (!template)
8
+ return { code: 'function render() { return document.createDocumentFragment(); }', errors: [] };
9
+ let html = template.content;
10
+ const errors = [];
11
+ // 1. Parsing Phase (HTML -> AST) - We can reuse the parser logic, but for now we duplicate it to keep it isolated from the old string compiler
12
+ const ast = parse(html, errors);
13
+ // 2. Transform Phase (Scopes, Bindings)
14
+ transform(ast, scriptResult, scopedId, [], descriptor.filename);
15
+ // 3. Codegen Phase (AST -> JS DOM Instructions)
16
+ let uidRef = { current: 0 };
17
+ const getUid = () => `_el${uidRef.current++}`;
18
+ // We generate a DocumentFragment at the root
19
+ let codeChunks = [];
20
+ codeChunks.push(`const _frag = document.createDocumentFragment();`);
21
+ // Generate code for all top-level children and append them to the fragment
22
+ ast.children.forEach(child => {
23
+ const rootId = generateDOMInstruction(child, codeChunks, getUid, uidRef, scriptResult.bindings || [], []);
24
+ if (rootId) {
25
+ codeChunks.push(`_frag.appendChild(${rootId});`);
26
+ }
27
+ });
28
+ const bodyFn = codeChunks.join('\n ');
29
+ const renderFn = `function render() {
30
+ const _s = (v) => (v && typeof v === 'object' && 'value' in v) ? v.value : v;
31
+ const _h = (v, raw) => {
32
+ const val = _s(v);
33
+ if (raw || typeof val !== 'string') return val;
34
+ // IRON FORTRESS: Automatic XSS Shield
35
+ if (typeof Mulan !== 'undefined' && Mulan.Security) {
36
+ return Mulan.Security.sanitize(val);
37
+ }
38
+ return val;
39
+ };
40
+
41
+ ${bodyFn}
42
+
43
+ return _frag;
44
+ }`;
45
+ return {
46
+ code: renderFn,
47
+ errors
48
+ };
49
+ }
50
+ exports.compileToDOM = compileToDOM;
51
+ // --- Code Generator (AST -> Instructions) ---
52
+ function generateDOMInstruction(node, chunks, getUid, uidRef, bindings, localScope) {
53
+ if (node.type === 'Text') {
54
+ const text = node;
55
+ const id = getUid();
56
+ // Native template literal interpolation
57
+ if (text.content.includes('${')) {
58
+ chunks.push(`const ${id} = document.createTextNode("");`);
59
+ // Wrap in effect for reactivity
60
+ chunks.push(`this._bindEffect(() => { ${id}.textContent = \`${text.content}\`; }, ${id});`);
61
+ }
62
+ else {
63
+ chunks.push(`const ${id} = document.createTextNode(${JSON.stringify(text.content)});`);
64
+ }
65
+ return id;
66
+ }
67
+ if (node.type === 'Interpolation') {
68
+ const interp = node;
69
+ const id = getUid();
70
+ chunks.push(`const ${id} = document.createTextNode("");`);
71
+ // Fine-grained reactivity!
72
+ chunks.push(`this._bindEffect(() => { ${id}.textContent = _h(${interp.content}, false); }, ${id});`);
73
+ return id;
74
+ }
75
+ if (node.type === 'Element') {
76
+ const element = node;
77
+ const id = getUid();
78
+ // Handle mu-if (Phase 4 - Dynamic Fragments)
79
+ if (element.directives.vIf) {
80
+ const condition = `_s(${processBindings(element.directives.vIf, bindings, localScope)})`;
81
+ // 1. Create the anchor Comment node where the block belongs
82
+ chunks.push(`const ${id} = document.createComment("mu-if:${element.directives.vIf}");`);
83
+ // 2. Generate the inner block rendering function
84
+ const blockId = `_if_${uidRef.current++}`;
85
+ const blockChunks = [];
86
+ blockChunks.push(`const ${blockId}_frag = document.createDocumentFragment();`);
87
+ blockChunks.push(`const _block_effects = [];`);
88
+ // 2a. Override bindings for inner effects
89
+ const localBindMacro = `(fn, targetNode) => { const stop = this._bindEffect(fn, targetNode); _block_effects.push(stop); return stop; }`;
90
+ let originalLength = blockChunks.length;
91
+ // 2b. Generate the actual block
92
+ const elementWithoutIf = { ...element, directives: { ...element.directives, vIf: undefined } };
93
+ const clonedChildId = generateDOMInstruction(elementWithoutIf, blockChunks, getUid, uidRef, bindings, localScope);
94
+ if (clonedChildId) {
95
+ blockChunks.push(`${blockId}_frag.appendChild(${clonedChildId});`);
96
+ }
97
+ for (let i = originalLength; i < blockChunks.length; i++) {
98
+ blockChunks[i] = blockChunks[i].replace(/this\._bindEffect/g, `(${localBindMacro})`);
99
+ }
100
+ blockChunks.push(`return { fragment: ${blockId}_frag, effects: _block_effects };`);
101
+ const renderBlockFn = `() => {\n ${blockChunks.join('\n ')}\n }`;
102
+ // 3. Register the macro-effect that calls the Reconciler
103
+ const ifIdHash = `if_${Math.random().toString(36).substr(2, 6)}`;
104
+ chunks.push(`this._bindEffect(() => { this._reconcileIf("${ifIdHash}", ${id}, !!(${condition}), ${renderBlockFn}); }, ${id});`);
105
+ return id;
106
+ }
107
+ // Handle mu-for (Phase 3 - The Reconciler)
108
+ if (element.directives.vFor) {
109
+ const { item, list } = element.directives.vFor;
110
+ const boundList = `_s(${processBindings(list, bindings, localScope)})`;
111
+ // 1. Create the anchor Comment node where the list begins
112
+ chunks.push(`const ${id} = document.createComment("mu-for:${list}");`);
113
+ // 2. Generate the inner row rendering function
114
+ const rowChildScope = [...localScope, item];
115
+ const rowId = `_row_${uidRef.current++}`;
116
+ const rowChunks = [];
117
+ rowChunks.push(`const ${rowId}_frag = document.createDocumentFragment();`);
118
+ rowChunks.push(`const _row_effects = [];`);
119
+ // 2a. Override the main chunk array temporarily to capture inner bindings
120
+ const originalBindEffect = 'this._bindEffect';
121
+ // We use a local array to capture effects created *inside* the row, so we can clean them up if the row is removed
122
+ const localBindMacro = `(fn, targetNode) => { const stop = this._bindEffect(fn, targetNode); _row_effects.push(stop); return stop; }`;
123
+ // In the compiler, when traversing children of a mu-for, we need to instruct _bindEffect calls to use the localMacro
124
+ // For simplicity in this AST generator, we use a string replacement trick on the generated chunks for this subtree
125
+ let originalLength = rowChunks.length;
126
+ // 2b. Generate the actual repeating element (the template root of the mu-for)
127
+ // We strip the vFor directive so it doesn't recurse infinitely
128
+ const elementWithoutFor = { ...element, directives: { ...element.directives, vFor: undefined } };
129
+ const clonedChildId = generateDOMInstruction(elementWithoutFor, rowChunks, getUid, uidRef, bindings, rowChildScope);
130
+ if (clonedChildId) {
131
+ rowChunks.push(`${rowId}_frag.appendChild(${clonedChildId});`);
132
+ }
133
+ // Note: In a robust compiler, we'd pass an `effectRegistry` flag. Here we just regex patch the internal effects.
134
+ for (let i = originalLength; i < rowChunks.length; i++) {
135
+ rowChunks[i] = rowChunks[i].replace(/this\._bindEffect/g, `(${localBindMacro})`);
136
+ }
137
+ rowChunks.push(`return { fragment: ${rowId}_frag, effects: _row_effects };`);
138
+ const renderRowFn = `(${item}, _index) => {\n ${rowChunks.join('\n ')}\n }`;
139
+ // 3. Register the macro-effect that calls the Reconciler when the array changes
140
+ // The reconciler will only create new rows or move them natively
141
+ const listIdHash = `list_${Math.random().toString(36).substr(2, 6)}`;
142
+ // We assume mu-key is passed as a prop, e.g., mu-key="id"
143
+ let keyProp = 'null';
144
+ if (element.props['mu-key']) {
145
+ keyProp = `"${element.props['mu-key']}"`;
146
+ }
147
+ chunks.push(`this._bindEffect(() => { this._reconcileList("${listIdHash}", ${id}, ${boundList}, ${keyProp}, ${renderRowFn}); }, ${id});`);
148
+ return id;
149
+ }
150
+ chunks.push(`const ${id} = document.createElement("${element.tag}");`);
151
+ // Handle standard properties and classes
152
+ for (const [key, value] of Object.entries(element.props)) {
153
+ if (key === 'class') {
154
+ if (value.includes('${')) {
155
+ chunks.push(`this._bindEffect(() => { ${id}.className = \`${value}\`; }, ${id});`);
156
+ }
157
+ else {
158
+ chunks.push(`${id}.className = ${JSON.stringify(value)};`);
159
+ }
160
+ }
161
+ else if (key === 'id') {
162
+ chunks.push(`${id}.id = ${JSON.stringify(value)};`);
163
+ }
164
+ else if (key === 'data-mu-id') {
165
+ // Ignore internal string compiler metadata
166
+ }
167
+ else {
168
+ if (value.includes('${')) {
169
+ chunks.push(`this._bindEffect(() => { ${id}.setAttribute("${key}", \`${value}\`); }, ${id});`);
170
+ }
171
+ else {
172
+ chunks.push(`${id}.setAttribute("${key}", ${JSON.stringify(value)});`);
173
+ }
174
+ }
175
+ }
176
+ // Apply property side effects (.prop bindings) generated by transform
177
+ if (element._propertySideEffects) {
178
+ const effects = element._propertySideEffects;
179
+ for (const effect of effects) {
180
+ // effect strings look like: `this._b('mu_abc', 'style', ...)`
181
+ // We need to translate them to direct assignments, but the original transform already stripped the 'data-mu-id'.
182
+ // Let's rely on the DOM-specific transform logic we will write below.
183
+ }
184
+ }
185
+ // Handle DOM-specific Bindings
186
+ if (element._domBindings) {
187
+ for (const b of element._domBindings) {
188
+ if (b.type === 'prop') {
189
+ chunks.push(`this._bindEffect(() => { ${id}['${b.name}'] = ${b.expr}; }, ${id});`);
190
+ }
191
+ else if (b.type === 'event') {
192
+ chunks.push(`${id}.addEventListener('${b.name}', ${b.expr}.bind(this));`);
193
+ }
194
+ }
195
+ }
196
+ // Recursively generate children
197
+ const childScope = [...localScope];
198
+ element.children.forEach(child => {
199
+ const childId = generateDOMInstruction(child, chunks, getUid, uidRef, bindings, childScope);
200
+ if (childId) {
201
+ chunks.push(`${id}.appendChild(${childId});`);
202
+ }
203
+ });
204
+ return id;
205
+ }
206
+ return null;
207
+ }
208
+ // --- Copied Parser and Transformer ---
209
+ // For this standalone compiler test, we duplicate the parser functions.
210
+ // In the final refactor, these will be extracted to a shared `parse.ts` utility.
211
+ function parse(template, errors) {
212
+ const root = {
213
+ type: 'Element', tag: 'fragment', props: {}, children: [], directives: {}
214
+ };
215
+ const stack = [root];
216
+ let cursor = 0;
217
+ while (cursor < template.length) {
218
+ const char = template[cursor];
219
+ if (template.startsWith('<!--', cursor)) {
220
+ const end = template.indexOf('-->', cursor);
221
+ if (end === -1)
222
+ break;
223
+ cursor = end + 3;
224
+ }
225
+ else if (char === '<') {
226
+ if (template[cursor + 1] === '/') {
227
+ const end = template.indexOf('>', cursor);
228
+ if (end === -1)
229
+ break;
230
+ stack.pop();
231
+ cursor = end + 1;
232
+ }
233
+ else {
234
+ const end = template.indexOf('>', cursor);
235
+ if (end === -1)
236
+ break;
237
+ let tagContent = template.slice(cursor + 1, end);
238
+ const isSelfClosing = tagContent.endsWith('/') || ['img', 'br', 'input', 'hr'].includes(tagContent.split(' ')[0]);
239
+ if (tagContent.endsWith('/'))
240
+ tagContent = tagContent.slice(0, -1).trim();
241
+ const { tag, props, directives } = parseTag(tagContent);
242
+ const element = { type: 'Element', tag, props, children: [], directives };
243
+ stack[stack.length - 1].children.push(element);
244
+ if (!isSelfClosing)
245
+ stack.push(element);
246
+ cursor = end + 1;
247
+ }
248
+ }
249
+ else if (char === '{' && template[cursor + 1] === '{') {
250
+ const end = template.indexOf('}}', cursor);
251
+ if (end === -1)
252
+ break;
253
+ const content = template.slice(cursor + 2, end).trim();
254
+ stack[stack.length - 1].children.push({ type: 'Interpolation', content });
255
+ cursor = end + 2;
256
+ }
257
+ else {
258
+ let nextTag = template.indexOf('<', cursor);
259
+ let nextInterp = template.indexOf('{{', cursor);
260
+ let end = template.length;
261
+ if (nextTag !== -1 && nextTag < end)
262
+ end = nextTag;
263
+ if (nextInterp !== -1 && nextInterp < end)
264
+ end = nextInterp;
265
+ const content = template.slice(cursor, end);
266
+ if (content.trim() || content === ' ') {
267
+ stack[stack.length - 1].children.push({ type: 'Text', content });
268
+ }
269
+ cursor = end;
270
+ }
271
+ }
272
+ return root;
273
+ }
274
+ function parseTag(content) {
275
+ const parts = content.split(' ');
276
+ const tag = parts[0];
277
+ const props = {};
278
+ const directives = {};
279
+ const attrStr = content.slice(tag.length).trim();
280
+ let i = 0;
281
+ while (i < attrStr.length) {
282
+ if (/\s/.test(attrStr[i])) {
283
+ i++;
284
+ continue;
285
+ }
286
+ const keyStart = i;
287
+ while (i < attrStr.length && !/\s|=/.test(attrStr[i]))
288
+ i++;
289
+ const key = attrStr.slice(keyStart, i);
290
+ let value = 'true';
291
+ let peek = i;
292
+ while (peek < attrStr.length && /\s/.test(attrStr[peek]))
293
+ peek++;
294
+ if (peek < attrStr.length && attrStr[peek] === '=') {
295
+ i = peek + 1;
296
+ while (i < attrStr.length && /\s/.test(attrStr[i]))
297
+ i++;
298
+ if (i < attrStr.length && (attrStr[i] === '"' || attrStr[i] === "'")) {
299
+ const quote = attrStr[i];
300
+ i++;
301
+ const valStart = i;
302
+ while (i < attrStr.length && attrStr[i] !== quote) {
303
+ if (attrStr[i] === '\\' && attrStr[i + 1] === quote)
304
+ i += 2;
305
+ else
306
+ i++;
307
+ }
308
+ value = attrStr.slice(valStart, i);
309
+ i++;
310
+ }
311
+ else {
312
+ const valStart = i;
313
+ while (i < attrStr.length && !/\s/.test(attrStr[i]))
314
+ i++;
315
+ value = attrStr.slice(valStart, i);
316
+ }
317
+ }
318
+ if (key === 'v-if' || key === 'mu-if')
319
+ directives.vIf = value;
320
+ else if (key === 'v-for' || key === 'mu-for') {
321
+ const parts = value.split(' in ');
322
+ directives.vFor = { item: parts[0].trim(), list: parts.slice(1).join(' in ').trim() };
323
+ }
324
+ else if (key)
325
+ props[key] = value;
326
+ }
327
+ return { tag, props, directives };
328
+ }
329
+ const JS_KEYWORDS = new Set(['true', 'false', 'null', 'undefined', 'this', 'window', 'if', 'else', 'for', 'while', 'return', 'let', 'const', 'typeof', 'instanceof', 'Math', 'Object', 'Array', 'JSON', 'console']);
330
+ function processBindings(exp, bindings, localScope) {
331
+ const isOptionsAPI = !bindings || bindings.length === 0;
332
+ const bindingSet = new Set(bindings || []);
333
+ return exp.replace(/\b([a-zA-Z_$][\w$]*)\b/g, (match, id, offset, str) => {
334
+ if (offset > 0 && str[offset - 1] === '.')
335
+ return match;
336
+ let i = offset + match.length;
337
+ while (i < str.length && /\s/.test(str[i]))
338
+ i++;
339
+ if (str[i] === ':' && str[i + 1] !== '=')
340
+ return match;
341
+ if (JS_KEYWORDS.has(id) || localScope.includes(id))
342
+ return match;
343
+ if (isOptionsAPI)
344
+ return `this.${id}`;
345
+ if (bindingSet.has(id))
346
+ return `this.${id}`;
347
+ return match;
348
+ });
349
+ }
350
+ function transform(node, scriptResult, scopedId, localScope = [], filename) {
351
+ if (node.type === 'Element') {
352
+ const element = node;
353
+ if (scopedId && element.tag !== 'fragment')
354
+ element.props[scopedId] = '';
355
+ const domBindings = [];
356
+ element._domBindings = domBindings;
357
+ const childScope = [...localScope];
358
+ if (element.directives.vFor)
359
+ childScope.push(element.directives.vFor.item);
360
+ for (const key in element.props) {
361
+ if (key.startsWith('.')) {
362
+ const propName = key.slice(1);
363
+ const rawValue = element.props[key];
364
+ let expr = "''";
365
+ if (rawValue.startsWith('${') && rawValue.endsWith('}')) {
366
+ expr = processBindings(rawValue.slice(2, -1), scriptResult.bindings, localScope);
367
+ }
368
+ else {
369
+ expr = JSON.stringify(rawValue);
370
+ }
371
+ domBindings.push({ type: 'prop', name: propName, expr });
372
+ delete element.props[key];
373
+ }
374
+ else if (key.startsWith('@') || key.startsWith('v-on:') || (key.startsWith('on') && key.length > 2)) {
375
+ let eventName = key.startsWith('@') ? key.slice(1) : key.startsWith('v-on:') ? key.slice(5) : key.slice(2);
376
+ let bound = processBindings(element.props[key], scriptResult.bindings, localScope);
377
+ const expr = (bound.includes('(') || bound.includes('=') || bound.includes('++')) ? `($event) => { ${bound.replace(/`/g, '\\`')} }` : bound;
378
+ domBindings.push({ type: 'event', name: eventName, expr });
379
+ delete element.props[key];
380
+ }
381
+ }
382
+ element.children.forEach(child => transform(child, scriptResult, scopedId, childScope, filename));
383
+ }
384
+ else if (node.type === 'Interpolation') {
385
+ node.content = processBindings(node.content, scriptResult.bindings, localScope);
386
+ }
387
+ else if (node.type === 'Text') {
388
+ const text = node;
389
+ if (text.content.includes('${')) {
390
+ text.content = text.content.replace(/\$\{(.*?)\}/g, (_, expr) => '${_h(' + processBindings(expr, scriptResult.bindings, localScope) + ', false)}');
391
+ }
392
+ }
393
+ }
@@ -7,9 +7,16 @@ export class MuComponent {
7
7
  constructor(container) {
8
8
  this._hooks = {};
9
9
  this._effects = [];
10
+ this._domEffects = [];
10
11
  this._isDestroyed = false;
11
12
  this._propsQueue = [];
12
13
  this._eventQueue = [];
14
+ // --- MulanJS List Reconciliation Engine (No-VDOM) ---
15
+ // Cache to store generated DOM rows by mu-key
16
+ this._listCaches = new Map();
17
+ // --- MulanJS Conditional Engine (No-VDOM) ---
18
+ // Cache to store conditionally toggled blocks
19
+ this._ifCaches = new Map();
13
20
  this.container = container;
14
21
  this.state = {};
15
22
  this.$uid = 'mu_' + Math.random().toString(36).substr(2, 9);
@@ -49,6 +56,9 @@ export class MuComponent {
49
56
  console.log(`[Mulan Cycle] Stopping ${this._effects.length} effects for ${this.$uid}`);
50
57
  this._effects.forEach(stop => stop());
51
58
  this._effects = [];
59
+ console.log(`[Mulan Cycle] Stopping ${this._domEffects.length} DOM effects for ${this.$uid}`);
60
+ this._domEffects.forEach(stop => stop());
61
+ this._domEffects = [];
52
62
  // Mulan Cycle: Destroy hooks
53
63
  (_c = this._hooks.onMuDestroy) === null || _c === void 0 ? void 0 : _c.forEach(fn => fn());
54
64
  }
@@ -70,13 +80,20 @@ export class MuComponent {
70
80
  }
71
81
  mount() {
72
82
  console.log(`[Mulan Cycle] Mounting component ${this.$uid}`);
83
+ // The main render effect (legacy strings fallback / macro structure)
73
84
  const stop = effect(() => {
74
- console.log(`[Mulan Reactivity] Triggering update for ${this.$uid}`);
85
+ console.log(`[Mulan Reactivity] Triggering macro-update for ${this.$uid}`);
75
86
  this.update();
76
87
  });
77
88
  this._effects.push(stop);
78
89
  this.onMount();
79
90
  }
91
+ // New helper for the compiler to register a fine-grained DOM property effect
92
+ _bindEffect(fn, targetNode) {
93
+ const stop = effect(fn, targetNode);
94
+ this._domEffects.push(stop);
95
+ return stop;
96
+ }
80
97
  update() {
81
98
  if (this._isDestroyed) {
82
99
  console.warn(`[Mulan Warning] Update called on destroyed component ${this.$uid}. Blocking render.`);
@@ -113,6 +130,143 @@ export class MuComponent {
113
130
  }
114
131
  this._eventQueue = [];
115
132
  }
133
+ /**
134
+ * Reconciles a list of bound data to an existing DOM parent segment.
135
+ * @param listId Unique ID for this specific mu-for directive Block
136
+ * @param container The DOM Element or Fragment where children are attached
137
+ * @param array The current state array
138
+ * @param keyProp The property to extract from each item to use as a unique key (e.g. 'id')
139
+ * @param renderRow A function that takes an item and returns a new { fragment, effects }
140
+ */
141
+ _reconcileList(listId, anchorToken, array, keyProp, renderRow) {
142
+ if (!this._listCaches.has(listId)) {
143
+ this._listCaches.set(listId, new Map());
144
+ }
145
+ const cache = this._listCaches.get(listId);
146
+ const newKeys = new Set();
147
+ const parent = anchorToken.parentNode;
148
+ // 1. Array Iteration: Match against cache or Create
149
+ // Use a DocumentFragment to batch new node insertions natively
150
+ const collectorFrag = document.createDocumentFragment();
151
+ const newOrderNodes = [];
152
+ const newOrderKeys = [];
153
+ let requiresSwap = false;
154
+ let lastCachedIndex = -1;
155
+ for (let i = 0; i < array.length; i++) {
156
+ const item = array[i];
157
+ const key = keyProp ? item[keyProp] : i; // fallback to index if no key
158
+ newKeys.add(key);
159
+ newOrderKeys.push(key);
160
+ let cached = cache.get(key);
161
+ if (!cached) {
162
+ // Not in cache, Render a brand new row structure
163
+ const { fragment, effects } = renderRow(item, i);
164
+ // Track actual DOM nodes
165
+ const createdNodes = Array.from(fragment.childNodes);
166
+ cached = { nodes: createdNodes, effects, index: i }; // Store index for fast swap detection
167
+ cache.set(key, cached);
168
+ // Batch append to our in-memory fragment!
169
+ // This eliminates thousands of live DOM reflows on creation.
170
+ collectorFrag.appendChild(fragment);
171
+ newOrderNodes.push(...createdNodes);
172
+ }
173
+ else {
174
+ // Exists in cache
175
+ // Batch append to the in-memory array for index tracking
176
+ newOrderNodes.push(...cached.nodes);
177
+ // Fast-Path Swap Detection: If the cached index is physically out of numerical sequence,
178
+ // we know the array order was unsynchronized and needs reconciliation.
179
+ if (cached.index < lastCachedIndex) {
180
+ requiresSwap = true;
181
+ }
182
+ cached.index = i;
183
+ lastCachedIndex = i;
184
+ }
185
+ }
186
+ // 2. Fragment Batch Insertion (O(1) Live DOM Reflow for Creations)
187
+ if (collectorFrag.childNodes.length > 0) {
188
+ parent.insertBefore(collectorFrag, anchorToken.nextSibling);
189
+ }
190
+ // 3. Bidirectional Swap Sync (O(K) isolated shifts instead of O(N) cascades)
191
+ if (requiresSwap) {
192
+ let currentSibling = anchorToken.nextSibling;
193
+ // We iterate through our guaranteed physical newOrderNodes list
194
+ for (let j = 0; j < newOrderNodes.length; j++) {
195
+ const targetNode = newOrderNodes[j];
196
+ if (targetNode !== currentSibling) {
197
+ // The physical live DOM is out of sync. Move targetNode to where it belongs immediately.
198
+ parent.insertBefore(targetNode, currentSibling);
199
+ }
200
+ else {
201
+ // Node is correctly positioned, simply advance the sliding logical pointer
202
+ currentSibling = currentSibling.nextSibling;
203
+ }
204
+ }
205
+ }
206
+ // 4. Cleanup Phase: Rapidly detach unmounted nodes
207
+ if (cache.size > newKeys.size) {
208
+ for (const [key, cached] of cache.entries()) {
209
+ if (!newKeys.has(key)) {
210
+ // Remove from DOM
211
+ cached.nodes.forEach((node) => {
212
+ if (node.parentNode === parent) {
213
+ parent.removeChild(node);
214
+ }
215
+ });
216
+ // Stop any targeted effects tied to these nodes
217
+ cached.effects.forEach((stop) => stop());
218
+ // Delete from cache tracking
219
+ cache.delete(key);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ /**
225
+ * Reconciles a conditional block (mu-if) to an existing DOM parent segment.
226
+ */
227
+ _reconcileIf(ifId, anchorToken, condition, renderBlock) {
228
+ if (!this._ifCaches.has(ifId)) {
229
+ this._ifCaches.set(ifId, { isMounted: false, nodes: [], effects: [] });
230
+ }
231
+ let cached = this._ifCaches.get(ifId);
232
+ if (condition) {
233
+ // Should be mounted
234
+ if (!cached.isMounted) {
235
+ // We need to render and mount it
236
+ let nodesToMount;
237
+ if (cached.nodes.length === 0) {
238
+ // First time render
239
+ const { fragment, effects } = renderBlock();
240
+ nodesToMount = Array.from(fragment.childNodes);
241
+ cached.nodes = nodesToMount;
242
+ cached.effects = effects;
243
+ }
244
+ else {
245
+ // Re-mounting previously cached nodes
246
+ nodesToMount = cached.nodes;
247
+ }
248
+ cached.isMounted = true;
249
+ const parent = anchorToken.parentNode;
250
+ const targetSibling = anchorToken.nextSibling;
251
+ nodesToMount.forEach(node => {
252
+ parent.insertBefore(node, targetSibling);
253
+ });
254
+ }
255
+ }
256
+ else {
257
+ // Should NOT be mounted
258
+ if (cached.isMounted) {
259
+ // Unmount nodes but keep them in cache
260
+ cached.nodes.forEach(node => {
261
+ if (node.parentNode) {
262
+ node.parentNode.removeChild(node);
263
+ }
264
+ });
265
+ cached.isMounted = false;
266
+ // Note: We deliberately do NOT destroy the effects here, they stay active in memory!
267
+ }
268
+ }
269
+ }
116
270
  }
117
271
  // Helper to create functional components wrapped in the class system
118
272
  // Helper to create functional components wrapped in the class system