@mulanjs/mulanjs 1.0.1-dev.20260227173747 → 1.0.1-dev.20260227181914
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/ast-parser.js +57 -5
- package/dist/compiler/compiler.js +13 -6
- package/dist/compiler/dom-compiler.js +43 -12
- package/dist/compiler/script-compiler.js +22 -2
- package/dist/compiler/sfc-parser.js +1 -1
- package/dist/compiler/ssr-compiler.js +13 -3
- package/dist/index.js +1 -1
- package/dist/mulan.esm.js +72 -5
- package/dist/mulan.esm.js.map +1 -1
- package/dist/mulan.js +74 -6
- package/dist/mulan.js.map +1 -1
- package/dist/security/sanitizer.js +72 -4
- package/dist/types/ast-parser.d.ts +4 -0
- package/dist/types/compiler/ast-parser.d.ts +4 -0
- package/dist/types/compiler/dom-compiler.d.ts +1 -0
- package/dist/types/dom-compiler.d.ts +1 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/security/sanitizer.d.ts +17 -0
- package/package.json +1 -1
- package/src/compiler/ast-parser.ts +63 -5
- package/src/compiler/compiler.ts +12 -7
- package/src/compiler/dom-compiler.ts +46 -13
- package/src/compiler/script-compiler.ts +22 -1
- package/src/compiler/sfc-parser.ts +1 -1
- package/src/compiler/ssr-compiler.ts +13 -3
|
@@ -25,10 +25,21 @@ function parse(template, errors) {
|
|
|
25
25
|
tag: 'fragment',
|
|
26
26
|
props: {},
|
|
27
27
|
children: [],
|
|
28
|
-
directives: {}
|
|
28
|
+
directives: {},
|
|
29
|
+
startLine: 1,
|
|
30
|
+
startColumn: 0,
|
|
31
|
+
endLine: 1,
|
|
32
|
+
endColumn: 0
|
|
29
33
|
};
|
|
30
34
|
const stack = [root];
|
|
31
35
|
let cursor = 0;
|
|
36
|
+
const getPos = (idx) => {
|
|
37
|
+
const lines = template.slice(0, idx).split('\n');
|
|
38
|
+
return {
|
|
39
|
+
startLine: lines.length,
|
|
40
|
+
startColumn: lines[lines.length - 1].length
|
|
41
|
+
};
|
|
42
|
+
};
|
|
32
43
|
while (cursor < template.length) {
|
|
33
44
|
const char = template[cursor];
|
|
34
45
|
// 1. Comments
|
|
@@ -69,7 +80,18 @@ function parse(template, errors) {
|
|
|
69
80
|
const tagContent = template.slice(cursor + 1, end);
|
|
70
81
|
const { tag, props, directives } = parseTag(tagContent);
|
|
71
82
|
const isSelfClosing = tagContent.endsWith('/') || ['img', 'br', 'input', 'hr', 'link', 'meta'].includes(tag.toLowerCase());
|
|
72
|
-
const
|
|
83
|
+
const startPos = getPos(cursor);
|
|
84
|
+
const endPos = getPos(end + 1);
|
|
85
|
+
const element = {
|
|
86
|
+
type: 'Element',
|
|
87
|
+
tag,
|
|
88
|
+
props,
|
|
89
|
+
children: [],
|
|
90
|
+
directives,
|
|
91
|
+
...startPos,
|
|
92
|
+
endLine: endPos.startLine,
|
|
93
|
+
endColumn: endPos.startColumn
|
|
94
|
+
};
|
|
73
95
|
stack[stack.length - 1].children.push(element);
|
|
74
96
|
if (!isSelfClosing) {
|
|
75
97
|
stack.push(element);
|
|
@@ -85,7 +107,15 @@ function parse(template, errors) {
|
|
|
85
107
|
break;
|
|
86
108
|
}
|
|
87
109
|
const content = template.slice(cursor + 2, end).trim();
|
|
88
|
-
|
|
110
|
+
const startPos = getPos(cursor);
|
|
111
|
+
const endPos = getPos(end + 2);
|
|
112
|
+
stack[stack.length - 1].children.push({
|
|
113
|
+
type: 'Interpolation',
|
|
114
|
+
content,
|
|
115
|
+
...startPos,
|
|
116
|
+
endLine: endPos.startLine,
|
|
117
|
+
endColumn: endPos.startColumn
|
|
118
|
+
});
|
|
89
119
|
cursor = end + 2;
|
|
90
120
|
}
|
|
91
121
|
// 4. Native Template Literals ${ } (Protection)
|
|
@@ -105,9 +135,20 @@ function parse(template, errors) {
|
|
|
105
135
|
const lastChild = stack[stack.length - 1].children[stack[stack.length - 1].children.length - 1];
|
|
106
136
|
if (lastChild && lastChild.type === 'Text') {
|
|
107
137
|
lastChild.content += content;
|
|
138
|
+
const endPos = getPos(innerCursor);
|
|
139
|
+
lastChild.endLine = endPos.startLine;
|
|
140
|
+
lastChild.endColumn = endPos.startColumn;
|
|
108
141
|
}
|
|
109
142
|
else {
|
|
110
|
-
|
|
143
|
+
const startPos = getPos(cursor);
|
|
144
|
+
const endPos = getPos(innerCursor);
|
|
145
|
+
stack[stack.length - 1].children.push({
|
|
146
|
+
type: 'Text',
|
|
147
|
+
content,
|
|
148
|
+
...startPos,
|
|
149
|
+
endLine: endPos.startLine,
|
|
150
|
+
endColumn: endPos.startColumn
|
|
151
|
+
});
|
|
111
152
|
}
|
|
112
153
|
cursor = innerCursor;
|
|
113
154
|
}
|
|
@@ -136,9 +177,20 @@ function parse(template, errors) {
|
|
|
136
177
|
const lastChild = stack[stack.length - 1].children[stack[stack.length - 1].children.length - 1];
|
|
137
178
|
if (lastChild && lastChild.type === 'Text') {
|
|
138
179
|
lastChild.content += content;
|
|
180
|
+
const endPos = getPos(end);
|
|
181
|
+
lastChild.endLine = endPos.startLine;
|
|
182
|
+
lastChild.endColumn = endPos.startColumn;
|
|
139
183
|
}
|
|
140
184
|
else {
|
|
141
|
-
|
|
185
|
+
const startPos = getPos(cursor);
|
|
186
|
+
const endPos = getPos(end);
|
|
187
|
+
stack[stack.length - 1].children.push({
|
|
188
|
+
type: 'Text',
|
|
189
|
+
content,
|
|
190
|
+
...startPos,
|
|
191
|
+
endLine: endPos.startLine,
|
|
192
|
+
endColumn: endPos.startColumn
|
|
193
|
+
});
|
|
142
194
|
}
|
|
143
195
|
}
|
|
144
196
|
cursor = end;
|
|
@@ -13,14 +13,21 @@ async function compileSFC(source, filename, options) {
|
|
|
13
13
|
// 2. Script
|
|
14
14
|
const scriptResult = await (0, script_compiler_1.compileScript)(descriptor, options);
|
|
15
15
|
// Replace export default to capture the component
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
scriptCode = scriptCode.replace(
|
|
16
|
+
let scriptCode = scriptResult.code;
|
|
17
|
+
// Improved export capture
|
|
18
|
+
if (scriptCode.includes('export default')) {
|
|
19
|
+
scriptCode = scriptCode.replace('export default', 'const __component__ =');
|
|
20
20
|
}
|
|
21
|
-
if (
|
|
21
|
+
else if (scriptCode.includes('exports.default =')) {
|
|
22
22
|
scriptCode = scriptCode.replace('exports.default =', 'const __component__ =');
|
|
23
23
|
}
|
|
24
|
+
else if (scriptCode.includes('module.exports =')) {
|
|
25
|
+
scriptCode = scriptCode.replace('module.exports =', 'const __component__ =');
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Fallback for simple assignment
|
|
29
|
+
scriptCode += '\nconst __component__ = exports.default || module.exports || {};';
|
|
30
|
+
}
|
|
24
31
|
// 3. Style
|
|
25
32
|
const styleResult = (0, style_compiler_1.compileStyle)(descriptor, filename, options);
|
|
26
33
|
// 4. Template (Use the new unified No-VDOM compiler!)
|
|
@@ -167,7 +174,7 @@ module.exports = __component__;
|
|
|
167
174
|
code: finalCode,
|
|
168
175
|
css: styleResult.css,
|
|
169
176
|
errors: [...scriptResult.errors, ...templateResult.errors],
|
|
170
|
-
map: mergedMap || scriptResult.map
|
|
177
|
+
map: mergedMap || scriptResult.map || templateResult.map
|
|
171
178
|
};
|
|
172
179
|
}
|
|
173
180
|
exports.compileSFC = compileSFC;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.compileToDOM = void 0;
|
|
4
4
|
const ast_parser_1 = require("./ast-parser");
|
|
5
|
+
const source_map_1 = require("source-map");
|
|
5
6
|
function compileToDOM(descriptor, scriptResult, scopedId) {
|
|
6
7
|
console.log(`[MulanJS DOM Compiler v2.0] Compiling template for: ${descriptor.filename || 'anonymous'}`);
|
|
7
8
|
const template = descriptor.template;
|
|
@@ -21,9 +22,12 @@ function compileToDOM(descriptor, scriptResult, scopedId) {
|
|
|
21
22
|
// We generate a DocumentFragment at the root
|
|
22
23
|
let codeChunks = [];
|
|
23
24
|
codeChunks.push(`const _frag = this._recoveryMode ? null : document.createDocumentFragment();`);
|
|
25
|
+
const generator = descriptor.filename ? new source_map_1.SourceMapGenerator({
|
|
26
|
+
file: descriptor.filename + '.template.js'
|
|
27
|
+
}) : null;
|
|
24
28
|
// Generate code for all top-level children and append them to the fragment
|
|
25
29
|
ast.children.forEach(child => {
|
|
26
|
-
const rootId = generateDOMInstruction(child, codeChunks, getUid, getHoistId, hoists, uidRef, scriptResult.bindings || [], []);
|
|
30
|
+
const rootId = generateDOMInstruction(child, codeChunks, getUid, getHoistId, hoists, uidRef, scriptResult.bindings || [], [], generator, descriptor.filename);
|
|
27
31
|
if (rootId) {
|
|
28
32
|
codeChunks.push(`if (_frag) _frag.appendChild(${rootId});`);
|
|
29
33
|
}
|
|
@@ -43,6 +47,13 @@ function compileToDOM(descriptor, scriptResult, scopedId) {
|
|
|
43
47
|
}
|
|
44
48
|
return val;
|
|
45
49
|
};
|
|
50
|
+
|
|
51
|
+
const _va = (name, val) => {
|
|
52
|
+
if (typeof Mulan !== 'undefined' && Mulan.Security) {
|
|
53
|
+
return Mulan.Security.validateAttribute(name, val);
|
|
54
|
+
}
|
|
55
|
+
return val;
|
|
56
|
+
};
|
|
46
57
|
|
|
47
58
|
${bodyFn}
|
|
48
59
|
|
|
@@ -50,7 +61,8 @@ function compileToDOM(descriptor, scriptResult, scopedId) {
|
|
|
50
61
|
}`;
|
|
51
62
|
return {
|
|
52
63
|
code: renderFn,
|
|
53
|
-
errors
|
|
64
|
+
errors,
|
|
65
|
+
map: generator ? generator.toString() : undefined
|
|
54
66
|
};
|
|
55
67
|
}
|
|
56
68
|
exports.compileToDOM = compileToDOM;
|
|
@@ -88,7 +100,22 @@ function htmlEscape(str) {
|
|
|
88
100
|
.replace(/"/g, '"')
|
|
89
101
|
.replace(/'/g, ''');
|
|
90
102
|
}
|
|
91
|
-
function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef, bindings, localScope) {
|
|
103
|
+
function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef, bindings, localScope, generator, filename) {
|
|
104
|
+
const addMapping = (generatedLine) => {
|
|
105
|
+
if (generator && filename) {
|
|
106
|
+
generator.addMapping({
|
|
107
|
+
generated: {
|
|
108
|
+
line: generatedLine,
|
|
109
|
+
column: 0
|
|
110
|
+
},
|
|
111
|
+
original: {
|
|
112
|
+
line: node.startLine,
|
|
113
|
+
column: node.startColumn
|
|
114
|
+
},
|
|
115
|
+
source: filename
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
};
|
|
92
119
|
// --- STATIC NODE HOISTING (THE COMPILER INTELLIGENCE) ---
|
|
93
120
|
// If the node and ALL its children contain ZERO reactivity, we hoist it.
|
|
94
121
|
if (node.type === 'Element' && node.isStatic) {
|
|
@@ -116,11 +143,13 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
116
143
|
const numericId = uidRef.current - 1; // Not used for text nodes in SSR but kept for parity if needed
|
|
117
144
|
// Native template literal interpolation
|
|
118
145
|
if (text.content.includes('${')) {
|
|
146
|
+
addMapping(chunks.length + 1);
|
|
119
147
|
chunks.push(`const ${id} = this._recoveryMode ? null : document.createTextNode("");`);
|
|
120
148
|
// Wrap in effect for reactivity
|
|
121
149
|
chunks.push(`this._bindEffect(() => { if (!${id}) return; ${id}.textContent = \`${text.content}\`; }, ${id});`);
|
|
122
150
|
}
|
|
123
151
|
else {
|
|
152
|
+
addMapping(chunks.length + 1);
|
|
124
153
|
chunks.push(`const ${id} = this._recoveryMode ? null : document.createTextNode(${JSON.stringify(text.content)});`);
|
|
125
154
|
}
|
|
126
155
|
return id;
|
|
@@ -128,6 +157,7 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
128
157
|
if (node.type === 'Interpolation') {
|
|
129
158
|
const interp = node;
|
|
130
159
|
const id = getUid();
|
|
160
|
+
addMapping(chunks.length + 1);
|
|
131
161
|
chunks.push(`const ${id} = document.createTextNode("");`);
|
|
132
162
|
// Fine-grained reactivity!
|
|
133
163
|
chunks.push(`this._bindEffect(() => { ${id}.textContent = _h(${interp.content}, false); }, ${id});`);
|
|
@@ -151,7 +181,7 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
151
181
|
let originalLength = blockChunks.length;
|
|
152
182
|
// 2b. Generate the actual block
|
|
153
183
|
const elementWithoutIf = { ...element, directives: { ...element.directives, vIf: undefined } };
|
|
154
|
-
const clonedChildId = generateDOMInstruction(elementWithoutIf, blockChunks, getUid, getHoistId, hoists, uidRef, bindings, localScope);
|
|
184
|
+
const clonedChildId = generateDOMInstruction(elementWithoutIf, blockChunks, getUid, getHoistId, hoists, uidRef, bindings, localScope, generator, filename);
|
|
155
185
|
if (clonedChildId) {
|
|
156
186
|
blockChunks.push(`if (${blockId}_frag && ${clonedChildId} && !this._recoveryMode) ${blockId}_frag.appendChild(${clonedChildId});`);
|
|
157
187
|
}
|
|
@@ -184,7 +214,7 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
184
214
|
// 2b. Generate the actual repeating element (the template root of the mu-for)
|
|
185
215
|
// We strip the vFor directive so it doesn't recurse infinitely
|
|
186
216
|
const elementWithoutFor = { ...element, directives: { ...element.directives, vFor: undefined } };
|
|
187
|
-
const clonedChildId = generateDOMInstruction(elementWithoutFor, rowChunks, getUid, getHoistId, hoists, uidRef, bindings, rowChildScope);
|
|
217
|
+
const clonedChildId = generateDOMInstruction(elementWithoutFor, rowChunks, getUid, getHoistId, hoists, uidRef, bindings, rowChildScope, generator, filename);
|
|
188
218
|
if (clonedChildId) {
|
|
189
219
|
rowChunks.push(`if (${rowId}_frag && ${clonedChildId} && !this._recoveryMode) ${rowId}_frag.appendChild(${clonedChildId});`);
|
|
190
220
|
}
|
|
@@ -205,29 +235,30 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
205
235
|
}
|
|
206
236
|
const numericId = uidRef.current;
|
|
207
237
|
const id = getUid();
|
|
238
|
+
addMapping(chunks.length + 1);
|
|
208
239
|
chunks.push(`const ${id} = this._recoveryMode ? (this.container.getAttribute('data-mu-node-id') === "${numericId}" ? this.container : this.container.querySelector('[data-mu-node-id="${numericId}"]')) : document.createElement("${element.tag}");`);
|
|
209
240
|
// Handle standard properties and classes
|
|
210
241
|
for (const [key, value] of Object.entries(element.props)) {
|
|
211
242
|
if (key === 'class') {
|
|
212
243
|
if (value.includes('${')) {
|
|
213
|
-
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.className = \`${value}
|
|
244
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.className = _va("class", \`${value}\`); }, ${id});`);
|
|
214
245
|
}
|
|
215
246
|
else {
|
|
216
|
-
chunks.push(`if (${id}) ${id}.className = ${JSON.stringify(value)};`);
|
|
247
|
+
chunks.push(`if (${id}) ${id}.className = _va("class", ${JSON.stringify(value)});`);
|
|
217
248
|
}
|
|
218
249
|
}
|
|
219
250
|
else if (key === 'id') {
|
|
220
|
-
chunks.push(`if (${id}) ${id}.id = ${JSON.stringify(value)};`);
|
|
251
|
+
chunks.push(`if (${id}) ${id}.id = _va("id", ${JSON.stringify(value)});`);
|
|
221
252
|
}
|
|
222
253
|
else if (key === 'data-mu-id') {
|
|
223
254
|
// Ignore internal string compiler metadata
|
|
224
255
|
}
|
|
225
256
|
else {
|
|
226
257
|
if (value.includes('${')) {
|
|
227
|
-
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.setAttribute("${key}", \`${value}\`); }, ${id});`);
|
|
258
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.setAttribute("${key}", _va("${key}", \`${value}\`)); }, ${id});`);
|
|
228
259
|
}
|
|
229
260
|
else {
|
|
230
|
-
chunks.push(`if (${id}) ${id}.setAttribute("${key}", ${JSON.stringify(value)});`);
|
|
261
|
+
chunks.push(`if (${id}) ${id}.setAttribute("${key}", _va("${key}", ${JSON.stringify(value)}));`);
|
|
231
262
|
}
|
|
232
263
|
}
|
|
233
264
|
}
|
|
@@ -242,7 +273,7 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
242
273
|
if (element._domBindings) {
|
|
243
274
|
for (const b of element._domBindings) {
|
|
244
275
|
if (b.type === 'prop') {
|
|
245
|
-
chunks.push(`this._bindEffect(() => { if (${id}) ${id}['${b.name}'] = ${b.expr}; }, ${id});`);
|
|
276
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}['${b.name}'] = _va('${b.name}', ${b.expr}); }, ${id});`);
|
|
246
277
|
}
|
|
247
278
|
else if (b.type === 'event') {
|
|
248
279
|
chunks.push(`if (${id}) ${id}.addEventListener('${b.name}', (${b.expr}).bind(this));`);
|
|
@@ -252,7 +283,7 @@ function generateDOMInstruction(node, chunks, getUid, getHoistId, hoists, uidRef
|
|
|
252
283
|
// Recursively generate children
|
|
253
284
|
const childScope = [...localScope];
|
|
254
285
|
element.children.forEach(child => {
|
|
255
|
-
const childId = generateDOMInstruction(child, chunks, getUid, getHoistId, hoists, uidRef, bindings, childScope);
|
|
286
|
+
const childId = generateDOMInstruction(child, chunks, getUid, getHoistId, hoists, uidRef, bindings, childScope, generator, filename);
|
|
256
287
|
if (childId) {
|
|
257
288
|
chunks.push(`if (${id} && ${childId} && !this._recoveryMode) ${id}.appendChild(${childId});`);
|
|
258
289
|
}
|
|
@@ -29,8 +29,28 @@ const source_map_1 = require("source-map");
|
|
|
29
29
|
const path = __importStar(require("path"));
|
|
30
30
|
async function compileScript(descriptor, options) {
|
|
31
31
|
const script = descriptor.script;
|
|
32
|
-
if (!script)
|
|
33
|
-
|
|
32
|
+
if (!script) {
|
|
33
|
+
let finalMap;
|
|
34
|
+
if (descriptor.filename) {
|
|
35
|
+
const generator = new source_map_1.SourceMapGenerator({
|
|
36
|
+
file: descriptor.filename
|
|
37
|
+
});
|
|
38
|
+
let relativePath = descriptor.filename.split(/[/\\]/).pop() || 'unknown.mujs';
|
|
39
|
+
const normalized = descriptor.filename.replace(/\\/g, '/');
|
|
40
|
+
const srcIdx = normalized.indexOf('/src/');
|
|
41
|
+
if (srcIdx !== -1) {
|
|
42
|
+
relativePath = 'webpack:///' + normalized.substring(srcIdx + 1);
|
|
43
|
+
}
|
|
44
|
+
generator.setSourceContent(relativePath, descriptor.source);
|
|
45
|
+
generator.addMapping({
|
|
46
|
+
generated: { line: 1, column: 0 },
|
|
47
|
+
original: { line: 1, column: 0 },
|
|
48
|
+
source: relativePath
|
|
49
|
+
});
|
|
50
|
+
finalMap = generator.toString();
|
|
51
|
+
}
|
|
52
|
+
return { code: 'export default {}', errors: [], map: finalMap };
|
|
53
|
+
}
|
|
34
54
|
let content = script.content;
|
|
35
55
|
let filename = descriptor.filename;
|
|
36
56
|
let isExternal = false;
|
|
@@ -66,7 +66,7 @@ function parseMUJS(source, filename) {
|
|
|
66
66
|
const content = source.slice(contentStart, contentEnd);
|
|
67
67
|
const block = {
|
|
68
68
|
type: tagName,
|
|
69
|
-
content: content
|
|
69
|
+
content: content,
|
|
70
70
|
attrs,
|
|
71
71
|
start: start,
|
|
72
72
|
end: contentEnd + closeTag.length
|
|
@@ -32,6 +32,13 @@ function compileToSSR(descriptor, scriptResult, scopedId) {
|
|
|
32
32
|
}
|
|
33
33
|
return val.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
34
34
|
};
|
|
35
|
+
|
|
36
|
+
const _va = (name, val) => {
|
|
37
|
+
if (typeof Mulan !== 'undefined' && Mulan.Security) {
|
|
38
|
+
return Mulan.Security.validateAttribute(name, val);
|
|
39
|
+
}
|
|
40
|
+
return val;
|
|
41
|
+
};
|
|
35
42
|
|
|
36
43
|
${bodyFn}
|
|
37
44
|
}`;
|
|
@@ -100,18 +107,21 @@ function generateSSRInstruction(node, bindings, localScope, getUid) {
|
|
|
100
107
|
}
|
|
101
108
|
let value = el.props[key];
|
|
102
109
|
if (key.startsWith(':') || key.startsWith('.')) {
|
|
110
|
+
// Dynamic binding — validate at runtime with _va
|
|
103
111
|
let attrName = key.slice(1);
|
|
104
112
|
let expr = processBindings(value, bindings, localScope);
|
|
105
|
-
html += ` ${attrName}="\${_h(${expr})}"`;
|
|
113
|
+
html += ` ${attrName}="\${_va("${attrName}", _h(${expr}))}"`;
|
|
106
114
|
}
|
|
107
115
|
else {
|
|
108
116
|
if (value.includes('${')) {
|
|
109
|
-
|
|
117
|
+
// Interpolated value — validate at runtime
|
|
118
|
+
const processedValue = value.replace(/\$\{(.*?)\}/g, (_, expr) => {
|
|
110
119
|
return `\${_h(${processBindings(expr, bindings, localScope)})}`;
|
|
111
120
|
});
|
|
112
|
-
html += ` ${key}="${
|
|
121
|
+
html += ` ${key}="\${_va("${key}", \`${processedValue}\`)}"`;
|
|
113
122
|
}
|
|
114
123
|
else {
|
|
124
|
+
// Static literal — safe at compile-time, no runtime overhead needed
|
|
115
125
|
html += ` ${key}="${value.replace(/"/g, '"')}"`;
|
|
116
126
|
}
|
|
117
127
|
}
|
package/dist/index.js
CHANGED
|
@@ -60,7 +60,7 @@ const Quantum = __importStar(require("./core/quantum"));
|
|
|
60
60
|
const Surge = __importStar(require("./core/surge"));
|
|
61
61
|
const InfinityList = __importStar(require("./components/infinity-list"));
|
|
62
62
|
const Mulan = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ reactive: reactive_1.reactive,
|
|
63
|
-
effect: reactive_1.effect, Component: component_2.MuComponent, defineComponent: component_2.defineComponent, Router: index_3.MuRouter, createRouter: index_3.createRouter, Store: index_4.MuStore, Security: sanitizer_1.Security }, Hooks), Query), Quantum), Surge), InfinityList), { render: renderer_1.render,
|
|
63
|
+
effect: reactive_1.effect, Component: component_2.MuComponent, defineComponent: component_2.defineComponent, Router: index_3.MuRouter, createRouter: index_3.createRouter, Store: index_4.MuStore, SecureStore: sanitizer_1.SecureStore, Security: sanitizer_1.Security }, Hooks), Query), Quantum), Surge), InfinityList), { render: renderer_1.render,
|
|
64
64
|
// MULAN INSIGHT: Branded Logging
|
|
65
65
|
log: (msg, ...args) => {
|
|
66
66
|
console.log(`%c[MulanJS]%c ${msg}`, "color: #ff3e00; font-weight: bold; background: #222; padding: 2px 4px; border-radius: 3px;", "", ...args);
|
package/dist/mulan.esm.js
CHANGED
|
@@ -1195,6 +1195,8 @@ class Security {
|
|
|
1195
1195
|
* Use `mu-raw` attribute in templates to bypass this for trusted content.
|
|
1196
1196
|
*/
|
|
1197
1197
|
static sanitize(input) {
|
|
1198
|
+
if (typeof input !== 'string')
|
|
1199
|
+
return input;
|
|
1198
1200
|
// 1. Basic entity encoding
|
|
1199
1201
|
let secure = input
|
|
1200
1202
|
.replace(/&/g, "&")
|
|
@@ -1203,13 +1205,32 @@ class Security {
|
|
|
1203
1205
|
.replace(/"/g, """)
|
|
1204
1206
|
.replace(/'/g, "'");
|
|
1205
1207
|
// 2. Remove dangerous events (extra layer if encoding fails)
|
|
1206
|
-
const dangerousEvents = ['onload', 'onclick', 'onerror', 'onmouseover', 'onfocus'];
|
|
1208
|
+
const dangerousEvents = ['onload', 'onclick', 'onerror', 'onmouseover', 'onfocus', 'oncontextmenu', 'oncopy', 'oncut', 'onpaste'];
|
|
1207
1209
|
dangerousEvents.forEach(event => {
|
|
1208
|
-
const regex = new RegExp(event
|
|
1209
|
-
secure = secure.replace(regex, 'data-blocked-' + event);
|
|
1210
|
+
const regex = new RegExp(`\\b${event}\\s*=`, 'gi');
|
|
1211
|
+
secure = secure.replace(regex, 'data-blocked-' + event + '=');
|
|
1210
1212
|
});
|
|
1211
1213
|
return secure;
|
|
1212
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* IRON FORTRESS: Attribute Sentinel
|
|
1217
|
+
* Validates and cleans attribute values based on their name.
|
|
1218
|
+
*/
|
|
1219
|
+
static validateAttribute(name, value) {
|
|
1220
|
+
const lowerName = name.toLowerCase();
|
|
1221
|
+
// Block all event handlers if they somehow bypassed the compiler
|
|
1222
|
+
if (lowerName.startsWith('on')) {
|
|
1223
|
+
return `blocked-event-${lowerName}`;
|
|
1224
|
+
}
|
|
1225
|
+
// Strict URL validation for src/href
|
|
1226
|
+
if (lowerName === 'src' || lowerName === 'href' || lowerName === 'action' || lowerName === 'formaction') {
|
|
1227
|
+
const trimmedValue = value.trim().toLowerCase();
|
|
1228
|
+
if (trimmedValue.startsWith('javascript:') || trimmedValue.startsWith('data:text/html') || trimmedValue.startsWith('vbscript:')) {
|
|
1229
|
+
return 'about:blank#blocked-malicious-scheme';
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return Security.sanitize(value);
|
|
1233
|
+
}
|
|
1213
1234
|
/**
|
|
1214
1235
|
* Generates a strict Content Security Policy header value.
|
|
1215
1236
|
* @param options Configuration for allowed sources
|
|
@@ -1240,6 +1261,52 @@ class Security {
|
|
|
1240
1261
|
});
|
|
1241
1262
|
}
|
|
1242
1263
|
}
|
|
1264
|
+
/**
|
|
1265
|
+
* IRON FORTRESS: SECURE STORE
|
|
1266
|
+
* A version of MuStore that encapsulates state and provides optional encryption hooks.
|
|
1267
|
+
*/
|
|
1268
|
+
|
|
1269
|
+
class SecureStore {
|
|
1270
|
+
constructor(initialState, options) {
|
|
1271
|
+
this._key = (options === null || options === void 0 ? void 0 : options.key) || null;
|
|
1272
|
+
this._state = reactive(initialState);
|
|
1273
|
+
if ((options === null || options === void 0 ? void 0 : options.encrypt) && this._key) {
|
|
1274
|
+
this._loadEncrypted();
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
get state() {
|
|
1278
|
+
// IRON FORTRESS: Freeze prevents direct mutation outside dispatch
|
|
1279
|
+
return Object.freeze(Object.assign({}, this._state));
|
|
1280
|
+
}
|
|
1281
|
+
dispatch(action) {
|
|
1282
|
+
// Only allow state changes via dispatch
|
|
1283
|
+
action(this._state);
|
|
1284
|
+
this._saveEncrypted();
|
|
1285
|
+
}
|
|
1286
|
+
_saveEncrypted() {
|
|
1287
|
+
if (!this._key || typeof localStorage === 'undefined')
|
|
1288
|
+
return;
|
|
1289
|
+
const data = JSON.stringify(this._state);
|
|
1290
|
+
// Base64 + Scramble for basic security
|
|
1291
|
+
const encrypted = btoa(unescape(encodeURIComponent(data)));
|
|
1292
|
+
localStorage.setItem(`secure-${this._key}`, encrypted);
|
|
1293
|
+
}
|
|
1294
|
+
_loadEncrypted() {
|
|
1295
|
+
if (!this._key || typeof localStorage === 'undefined')
|
|
1296
|
+
return;
|
|
1297
|
+
try {
|
|
1298
|
+
const encrypted = localStorage.getItem(`secure-${this._key}`);
|
|
1299
|
+
if (encrypted) {
|
|
1300
|
+
const decrypted = decodeURIComponent(escape(atob(encrypted)));
|
|
1301
|
+
const data = JSON.parse(decrypted);
|
|
1302
|
+
Object.assign(this._state, data);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
catch (e) {
|
|
1306
|
+
console.warn("[Mulan Security] Failed to load secure state");
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1243
1310
|
|
|
1244
1311
|
;// ./src/router/index.ts
|
|
1245
1312
|
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
@@ -2512,7 +2579,7 @@ function renderToString(ComponentClass, props = {}) {
|
|
|
2512
2579
|
|
|
2513
2580
|
|
|
2514
2581
|
const Mulan = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ reactive: reactive,
|
|
2515
|
-
effect: effect, Component: MuComponent, defineComponent: defineComponent, Router: MuRouter, createRouter: createRouter, Store: MuStore, Security: Security }, hooks_namespaceObject), query_namespaceObject), quantum_namespaceObject), surge_namespaceObject), infinity_list_namespaceObject), { render: render,
|
|
2582
|
+
effect: effect, Component: MuComponent, defineComponent: defineComponent, Router: MuRouter, createRouter: createRouter, Store: MuStore, SecureStore: SecureStore, Security: Security }, hooks_namespaceObject), query_namespaceObject), quantum_namespaceObject), surge_namespaceObject), infinity_list_namespaceObject), { render: render,
|
|
2516
2583
|
// MULAN INSIGHT: Branded Logging
|
|
2517
2584
|
log: (msg, ...args) => {
|
|
2518
2585
|
console.log(`%c[MulanJS]%c ${msg}`, "color: #ff3e00; font-weight: bold; background: #222; padding: 2px 4px; border-radius: 3px;", "", ...args);
|
|
@@ -2546,6 +2613,6 @@ if (typeof window !== 'undefined') {
|
|
|
2546
2613
|
}
|
|
2547
2614
|
/* harmony default export */ const src = (Mulan);
|
|
2548
2615
|
|
|
2549
|
-
export { MuComponent as Component, MuBlochSphereElement, MuComponent, MuInfinity, MuRouter, MuStore, MuRouter as Router, Security, Signal, activeControls, src as default, defineComponent, effect, getCurrentInstance, hydrate, muBurst, muControl, muDebounced, muEffect, muEntangle, muGate, muGeom, muHistory, muMeasure, muMemo, muParallel, muPulse, muQubit, muRegister, muSearch, muState, muSurge, muSuspense, muSwitch, muTeleport, muThrottled, muVault, nextTick, onMuDestroy, onMuIdle, onMuInit, onMuMount, onMuPanic, onMuResume, onMuShake, onMuVisibility, onMuVoice, persistent, queueEffect, reactive, ref, render, renderToString, sanitize, setCurrentInstance, useMutation, useQuery };
|
|
2616
|
+
export { MuComponent as Component, MuBlochSphereElement, MuComponent, MuInfinity, MuRouter, MuStore, MuRouter as Router, SecureStore, Security, Signal, activeControls, src as default, defineComponent, effect, getCurrentInstance, hydrate, muBurst, muControl, muDebounced, muEffect, muEntangle, muGate, muGeom, muHistory, muMeasure, muMemo, muParallel, muPulse, muQubit, muRegister, muSearch, muState, muSurge, muSuspense, muSwitch, muTeleport, muThrottled, muVault, nextTick, onMuDestroy, onMuIdle, onMuInit, onMuMount, onMuPanic, onMuResume, onMuShake, onMuVisibility, onMuVoice, persistent, queueEffect, reactive, ref, render, renderToString, sanitize, setCurrentInstance, useMutation, useQuery };
|
|
2550
2617
|
|
|
2551
2618
|
//# sourceMappingURL=mulan.esm.js.map
|