meno-core 1.0.4 → 1.0.5
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/lib/client/index.ts +1 -1
- package/lib/client/scripts/ScriptExecutor.test.ts +49 -72
- package/lib/client/scripts/ScriptExecutor.ts +43 -71
- package/lib/server/createServer.ts +1 -1
- package/lib/server/index.ts +1 -1
- package/lib/server/routes/api/core-routes.ts +1 -1
- package/lib/server/routes/api/index.ts +1 -1
- package/lib/shared/index.ts +1 -1
- package/package.json +1 -1
- package/tsconfig.json +2 -2
- package/vite.config.ts +1 -1
package/lib/client/index.ts
CHANGED
|
@@ -8,13 +8,11 @@ describe("ScriptExecutor", () => {
|
|
|
8
8
|
let componentRegistry: ComponentRegistry;
|
|
9
9
|
let elementRegistry: ElementRegistry;
|
|
10
10
|
let scriptExecutor: ScriptExecutor;
|
|
11
|
-
let
|
|
12
|
-
let
|
|
11
|
+
let executedCode: string[];
|
|
12
|
+
let originalFunction: typeof Function;
|
|
13
|
+
let functionSpy: ReturnType<typeof spyOn>;
|
|
13
14
|
|
|
14
15
|
beforeEach(() => {
|
|
15
|
-
// Save original document
|
|
16
|
-
originalDocument = global.document;
|
|
17
|
-
|
|
18
16
|
componentRegistry = new ComponentRegistry();
|
|
19
17
|
elementRegistry = new ElementRegistry();
|
|
20
18
|
scriptExecutor = new ScriptExecutor({
|
|
@@ -22,38 +20,25 @@ describe("ScriptExecutor", () => {
|
|
|
22
20
|
elementRegistry,
|
|
23
21
|
});
|
|
24
22
|
|
|
25
|
-
// Track
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
getElementById: mock((id: string) => {
|
|
40
|
-
const script = injectedScripts.get(id);
|
|
41
|
-
return script ? { id: script.id, remove: () => injectedScripts.delete(id) } : null;
|
|
42
|
-
}),
|
|
43
|
-
body: {
|
|
44
|
-
appendChild: mock((script: any) => {
|
|
45
|
-
if (script.tagName === 'SCRIPT') {
|
|
46
|
-
injectedScripts.set(script.id, { id: script.id, textContent: script.textContent });
|
|
47
|
-
}
|
|
48
|
-
}),
|
|
49
|
-
},
|
|
50
|
-
} as any;
|
|
23
|
+
// Track executed code by intercepting Function constructor
|
|
24
|
+
executedCode = [];
|
|
25
|
+
originalFunction = global.Function;
|
|
26
|
+
functionSpy = spyOn(global, "Function").mockImplementation(((...args: any[]) => {
|
|
27
|
+
if (args.length > 0 && typeof args[args.length - 1] === 'string') {
|
|
28
|
+
const code = args[args.length - 1];
|
|
29
|
+
// Only track ScriptExecutor calls (they contain "Component:" comment)
|
|
30
|
+
if (code.includes('Component:')) {
|
|
31
|
+
executedCode.push(code);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Return a function that executes the code
|
|
35
|
+
return originalFunction(...args) as any;
|
|
36
|
+
}) as unknown as FunctionConstructor);
|
|
51
37
|
});
|
|
52
38
|
|
|
53
39
|
afterEach(() => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
global.document = originalDocument;
|
|
40
|
+
functionSpy.mockRestore();
|
|
41
|
+
executedCode = [];
|
|
57
42
|
});
|
|
58
43
|
|
|
59
44
|
describe("constructor", () => {
|
|
@@ -80,7 +65,7 @@ describe("ScriptExecutor", () => {
|
|
|
80
65
|
|
|
81
66
|
scriptExecutor.execute();
|
|
82
67
|
|
|
83
|
-
expect(
|
|
68
|
+
expect(executedCode.length).toBe(0);
|
|
84
69
|
});
|
|
85
70
|
|
|
86
71
|
test("should execute JavaScript without templates once", () => {
|
|
@@ -97,11 +82,9 @@ describe("ScriptExecutor", () => {
|
|
|
97
82
|
|
|
98
83
|
scriptExecutor.execute();
|
|
99
84
|
|
|
100
|
-
expect(
|
|
101
|
-
|
|
102
|
-
expect(
|
|
103
|
-
expect(script!.textContent).toContain("Component: TestComponent");
|
|
104
|
-
expect(script!.textContent).toContain("console.log('test');");
|
|
85
|
+
expect(executedCode.length).toBe(1);
|
|
86
|
+
expect(executedCode[0]).toContain("Component: TestComponent");
|
|
87
|
+
expect(executedCode[0]).toContain("console.log('test');");
|
|
105
88
|
});
|
|
106
89
|
|
|
107
90
|
// Template-based tests require full ElementRegistry from @meno/studio
|
|
@@ -118,8 +101,8 @@ describe("ScriptExecutor", () => {
|
|
|
118
101
|
componentRegistry.register("TestComponent", componentDef);
|
|
119
102
|
|
|
120
103
|
// Register component instances with props
|
|
121
|
-
const mockElement1 =
|
|
122
|
-
const mockElement2 =
|
|
104
|
+
const mockElement1 = document.createElement("div");
|
|
105
|
+
const mockElement2 = document.createElement("div");
|
|
123
106
|
elementRegistry.register([0], mockElement1, "TestComponent", true, {
|
|
124
107
|
name: "Instance1",
|
|
125
108
|
});
|
|
@@ -129,8 +112,8 @@ describe("ScriptExecutor", () => {
|
|
|
129
112
|
|
|
130
113
|
scriptExecutor.execute();
|
|
131
114
|
|
|
132
|
-
// Each instance gets its own
|
|
133
|
-
expect(
|
|
115
|
+
// Each instance gets its own execution
|
|
116
|
+
expect(executedCode.length).toBe(2);
|
|
134
117
|
});
|
|
135
118
|
|
|
136
119
|
test("should execute JavaScript without templates only once for multiple instances", () => {
|
|
@@ -146,8 +129,8 @@ describe("ScriptExecutor", () => {
|
|
|
146
129
|
componentRegistry.register("TestComponent", componentDef);
|
|
147
130
|
|
|
148
131
|
// Register multiple instances
|
|
149
|
-
const mockElement1 =
|
|
150
|
-
const mockElement2 =
|
|
132
|
+
const mockElement1 = document.createElement("div");
|
|
133
|
+
const mockElement2 = document.createElement("div");
|
|
151
134
|
elementRegistry.register([0], mockElement1, "TestComponent", true, {
|
|
152
135
|
name: "Instance1",
|
|
153
136
|
});
|
|
@@ -157,12 +140,10 @@ describe("ScriptExecutor", () => {
|
|
|
157
140
|
|
|
158
141
|
scriptExecutor.execute();
|
|
159
142
|
|
|
160
|
-
// Should only
|
|
161
|
-
expect(
|
|
162
|
-
|
|
163
|
-
expect(
|
|
164
|
-
expect(script!.textContent).toContain("Component: TestComponent");
|
|
165
|
-
expect(script!.textContent).toContain("console.log('test');");
|
|
143
|
+
// Should only execute once (no templates - executes once per component type)
|
|
144
|
+
expect(executedCode.length).toBe(1);
|
|
145
|
+
expect(executedCode[0]).toContain("Component: TestComponent");
|
|
146
|
+
expect(executedCode[0]).toContain("console.log('test');");
|
|
166
147
|
});
|
|
167
148
|
|
|
168
149
|
test.skip("should handle multiple components with mixed template/non-template JavaScript (requires @meno/studio ElementRegistry)", () => {
|
|
@@ -187,16 +168,16 @@ describe("ScriptExecutor", () => {
|
|
|
187
168
|
componentRegistry.register("Component1", componentDef1);
|
|
188
169
|
componentRegistry.register("Component2", componentDef2);
|
|
189
170
|
|
|
190
|
-
const mockElement =
|
|
171
|
+
const mockElement = document.createElement("div");
|
|
191
172
|
elementRegistry.register([0], mockElement, "Component2", true, {
|
|
192
173
|
name: "Component2Instance",
|
|
193
174
|
});
|
|
194
175
|
|
|
195
176
|
scriptExecutor.execute();
|
|
196
177
|
|
|
197
|
-
// Component1: 1
|
|
198
|
-
// Component2: 1
|
|
199
|
-
expect(
|
|
178
|
+
// Component1: 1 execution (no templates)
|
|
179
|
+
// Component2: 1 execution (with templates, 1 instance)
|
|
180
|
+
expect(executedCode.length).toBe(2);
|
|
200
181
|
});
|
|
201
182
|
|
|
202
183
|
test("should handle fallback for components without instances", () => {
|
|
@@ -326,11 +307,9 @@ describe("ScriptExecutor", () => {
|
|
|
326
307
|
|
|
327
308
|
scriptExecutor.execute();
|
|
328
309
|
|
|
329
|
-
expect(
|
|
330
|
-
|
|
331
|
-
expect(
|
|
332
|
-
expect(script!.textContent).toContain("(function()");
|
|
333
|
-
expect(script!.textContent).toContain("})();");
|
|
310
|
+
expect(executedCode.length).toBe(1);
|
|
311
|
+
expect(executedCode[0]).toContain("(function()");
|
|
312
|
+
expect(executedCode[0]).toContain("})();");
|
|
334
313
|
});
|
|
335
314
|
});
|
|
336
315
|
|
|
@@ -359,12 +338,10 @@ describe("ScriptExecutor", () => {
|
|
|
359
338
|
|
|
360
339
|
scriptExecutor.executeForComponent("Component1");
|
|
361
340
|
|
|
362
|
-
// Only Component1 should be
|
|
363
|
-
expect(
|
|
364
|
-
|
|
365
|
-
expect(
|
|
366
|
-
expect(script!.textContent).toContain("Component: Component1");
|
|
367
|
-
expect(script!.textContent).toContain("console.log('Component1');");
|
|
341
|
+
// Only Component1 should be executed
|
|
342
|
+
expect(executedCode.length).toBe(1);
|
|
343
|
+
expect(executedCode[0]).toContain("Component: Component1");
|
|
344
|
+
expect(executedCode[0]).toContain("console.log('Component1');");
|
|
368
345
|
});
|
|
369
346
|
|
|
370
347
|
test.skip("should execute JavaScript with templates for specific component (requires @meno/studio ElementRegistry)", () => {
|
|
@@ -379,8 +356,8 @@ describe("ScriptExecutor", () => {
|
|
|
379
356
|
|
|
380
357
|
componentRegistry.register("TestComponent", componentDef);
|
|
381
358
|
|
|
382
|
-
const mockElement1 =
|
|
383
|
-
const mockElement2 =
|
|
359
|
+
const mockElement1 = document.createElement("div");
|
|
360
|
+
const mockElement2 = document.createElement("div");
|
|
384
361
|
elementRegistry.register([0], mockElement1, "TestComponent", true, {
|
|
385
362
|
name: "Instance1",
|
|
386
363
|
});
|
|
@@ -390,8 +367,8 @@ describe("ScriptExecutor", () => {
|
|
|
390
367
|
|
|
391
368
|
scriptExecutor.executeForComponent("TestComponent");
|
|
392
369
|
|
|
393
|
-
// Each instance gets its own
|
|
394
|
-
expect(
|
|
370
|
+
// Each instance gets its own execution
|
|
371
|
+
expect(executedCode.length).toBe(2);
|
|
395
372
|
});
|
|
396
373
|
|
|
397
374
|
test("should skip if component has no JavaScript", () => {
|
|
@@ -407,7 +384,7 @@ describe("ScriptExecutor", () => {
|
|
|
407
384
|
|
|
408
385
|
scriptExecutor.executeForComponent("TestComponent");
|
|
409
386
|
|
|
410
|
-
expect(
|
|
387
|
+
expect(executedCode.length).toBe(0);
|
|
411
388
|
});
|
|
412
389
|
|
|
413
390
|
test("should handle non-existent component gracefully", () => {
|
|
@@ -42,7 +42,6 @@ function generateDestructure(
|
|
|
42
42
|
export class ScriptExecutor {
|
|
43
43
|
private componentRegistry: ComponentRegistry;
|
|
44
44
|
private elementRegistry: ElementRegistry;
|
|
45
|
-
private injectedScripts: Set<string> = new Set();
|
|
46
45
|
|
|
47
46
|
constructor(config: ScriptExecutorConfig) {
|
|
48
47
|
this.componentRegistry = config.componentRegistry;
|
|
@@ -50,54 +49,31 @@ export class ScriptExecutor {
|
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* @param
|
|
57
|
-
* @param
|
|
52
|
+
* Execute JavaScript code via new Function() with error isolation
|
|
53
|
+
* Uses new Function() instead of script tags to avoid CSP violations
|
|
54
|
+
* Each component is wrapped in try-catch for isolation
|
|
55
|
+
* @param jsCode - JavaScript code to execute
|
|
56
|
+
* @param componentName - Component name for error reporting
|
|
58
57
|
* @private
|
|
59
58
|
*/
|
|
60
|
-
private
|
|
59
|
+
private executeWrappedJS(jsCode: string, componentName: string): void {
|
|
61
60
|
if (!jsCode || !jsCode.trim()) return;
|
|
62
|
-
if (typeof document === 'undefined') return;
|
|
63
61
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
const wrappedJS = `(function() {
|
|
63
|
+
// Component: ${componentName}
|
|
64
|
+
try {
|
|
65
|
+
${jsCode}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error('[Meno] Runtime error in ' + ${JSON.stringify(componentName)} + ':', e);
|
|
68
|
+
}
|
|
69
|
+
})();`;
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
try {
|
|
72
|
+
new Function(wrappedJS)();
|
|
73
|
+
} catch (syntaxError) {
|
|
74
|
+
// Syntax errors caught here - doesn't affect other components
|
|
75
|
+
console.error(`[Meno] Syntax error in ${componentName}:`, syntaxError);
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
const script = document.createElement('script');
|
|
75
|
-
script.id = scriptId;
|
|
76
|
-
script.type = 'text/javascript';
|
|
77
|
-
script.textContent = `(function() {
|
|
78
|
-
// Component: ${componentName}${instanceId ? ` (instance: ${instanceId})` : ''}
|
|
79
|
-
try {
|
|
80
|
-
${jsCode}
|
|
81
|
-
} catch (e) {
|
|
82
|
-
console.error('[Meno] Error in ${componentName}:', e);
|
|
83
|
-
}
|
|
84
|
-
})();`;
|
|
85
|
-
|
|
86
|
-
document.body.appendChild(script);
|
|
87
|
-
this.injectedScripts.add(scriptId);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Execute JavaScript code via script tag injection for browser-level isolation
|
|
92
|
-
* @param jsCode - JavaScript code to execute
|
|
93
|
-
* @param componentName - Component name for identification
|
|
94
|
-
* @param rootPath - Optional root path for instance-specific scripts
|
|
95
|
-
* @private
|
|
96
|
-
*/
|
|
97
|
-
private executeWrappedJS(jsCode: string, componentName: string, rootPath?: string): void {
|
|
98
|
-
// Convert rootPath to a safe ID (replace special chars)
|
|
99
|
-
const instanceId = rootPath ? rootPath.replace(/[^a-zA-Z0-9]/g, '-') : undefined;
|
|
100
|
-
this.injectScriptTag(jsCode, componentName, instanceId);
|
|
101
77
|
}
|
|
102
78
|
|
|
103
79
|
/**
|
|
@@ -117,10 +93,10 @@ export class ScriptExecutor {
|
|
|
117
93
|
|
|
118
94
|
try {
|
|
119
95
|
const processedJS = processCodeTemplates(originalJS, props);
|
|
120
|
-
this.executeWrappedJS(processedJS, componentName
|
|
96
|
+
this.executeWrappedJS(processedJS, `${componentName} (instance: ${rootPath})`);
|
|
121
97
|
} catch (templateError) {
|
|
122
98
|
// Fallback to original JS if template processing fails (similar to StyleInjector)
|
|
123
|
-
this.executeWrappedJS(originalJS, componentName
|
|
99
|
+
this.executeWrappedJS(originalJS, `${componentName} (instance: ${rootPath})`);
|
|
124
100
|
}
|
|
125
101
|
} catch (e) {
|
|
126
102
|
logRuntimeError('ScriptExecutor.executeInstanceJS', e, { componentName, rootPath });
|
|
@@ -144,17 +120,28 @@ export class ScriptExecutor {
|
|
|
144
120
|
): void {
|
|
145
121
|
const destructure = generateDestructure(defineVars, interfaceDef);
|
|
146
122
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
123
|
+
const wrappedJS = `(function() {
|
|
124
|
+
// Component: ${componentName} (defineVars)
|
|
125
|
+
try {
|
|
126
|
+
var elements = document.querySelectorAll('[data-component="${componentName}"]');
|
|
127
|
+
elements.forEach(function(el) {
|
|
128
|
+
var propsStr = el.getAttribute('data-props');
|
|
129
|
+
var props = propsStr ? JSON.parse(propsStr) : {};
|
|
130
|
+
(function(el, props) {
|
|
131
|
+
${destructure}
|
|
132
|
+
${js}
|
|
133
|
+
})(el, props);
|
|
134
|
+
});
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.error('[Meno] Runtime error in ${componentName}:', e);
|
|
137
|
+
}
|
|
138
|
+
})();`;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
new Function(wrappedJS)();
|
|
142
|
+
} catch (syntaxError) {
|
|
143
|
+
console.error(`[Meno] Syntax error in ${componentName}:`, syntaxError);
|
|
144
|
+
}
|
|
158
145
|
}
|
|
159
146
|
|
|
160
147
|
/**
|
|
@@ -317,20 +304,5 @@ elements.forEach(function(el) {
|
|
|
317
304
|
logRuntimeError('ScriptExecutor.executeForElement', e, { componentName });
|
|
318
305
|
}
|
|
319
306
|
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Remove all injected script tags (for cleanup on unmount/navigation)
|
|
323
|
-
*/
|
|
324
|
-
cleanup(): void {
|
|
325
|
-
if (typeof document === 'undefined') return;
|
|
326
|
-
|
|
327
|
-
for (const scriptId of this.injectedScripts) {
|
|
328
|
-
const script = document.getElementById(scriptId);
|
|
329
|
-
if (script) {
|
|
330
|
-
script.remove();
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
this.injectedScripts.clear();
|
|
334
|
-
}
|
|
335
307
|
}
|
|
336
308
|
|
package/lib/server/index.ts
CHANGED
package/lib/shared/index.ts
CHANGED
package/package.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"outDir": "./dist",
|
|
6
6
|
"baseUrl": ".",
|
|
7
7
|
"paths": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"meno-core": ["./lib/shared/index.ts"],
|
|
9
|
+
"meno-core/*": ["./lib/*"]
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
"include": ["lib/**/*", "entries/**/*", "bin/**/*"],
|