@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.
- package/dist/compiler/compiler.js +3 -3
- package/dist/compiler/dom-compiler.js +393 -0
- package/dist/core/component.js +155 -1
- package/dist/core/hooks.js +142 -0
- package/dist/core/reactive.js +9 -8
- package/dist/core/renderer.js +18 -6
- package/dist/mulan.esm.js +331 -16
- package/dist/mulan.esm.js.map +1 -1
- package/dist/mulan.js +331 -16
- package/dist/mulan.js.map +1 -1
- package/dist/types/compiler/dom-compiler.d.ts +7 -0
- package/dist/types/core/component.d.ts +65 -3
- package/dist/types/core/hooks.d.ts +37 -0
- package/dist/types/core/reactive.d.ts +1 -1
- package/dist/types/core/renderer.d.ts +2 -2
- package/dist/types/dom-compiler.d.ts +7 -0
- package/dist/types/index.d.ts +16 -0
- package/package.json +1 -1
- package/src/compiler/compiler.ts +5 -4
- package/src/compiler/dom-compiler.ts +438 -0
- package/src/compiler/script-compiler.js +0 -369
- package/src/compiler/sfc-parser.js +0 -93
|
@@ -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
|
|
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,
|
|
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
|
+
}
|
package/dist/core/component.js
CHANGED
|
@@ -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
|