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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @meno/core/client exports
2
+ * meno-core/client exports
3
3
  * Core client-side utilities for page rendering and routing
4
4
  * Does NOT include editor-specific code (that's in @meno/studio)
5
5
  */
@@ -8,13 +8,11 @@ describe("ScriptExecutor", () => {
8
8
  let componentRegistry: ComponentRegistry;
9
9
  let elementRegistry: ElementRegistry;
10
10
  let scriptExecutor: ScriptExecutor;
11
- let injectedScripts: Map<string, { id: string; textContent: string }>;
12
- let originalDocument: typeof document;
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 injected script tags
26
- injectedScripts = new Map();
27
-
28
- // Mock document for script tag injection
29
- global.document = {
30
- createElement: mock((tag: string) => {
31
- const element = {
32
- tagName: tag.toUpperCase(),
33
- id: '',
34
- type: '',
35
- textContent: '',
36
- };
37
- return element;
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
- injectedScripts.clear();
55
- // Restore original document
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(injectedScripts.size).toBe(0);
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(injectedScripts.size).toBe(1);
101
- const script = injectedScripts.get("meno-script-TestComponent");
102
- expect(script).toBeDefined();
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 = originalDocument.createElement("div");
122
- const mockElement2 = originalDocument.createElement("div");
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 script tag
133
- expect(injectedScripts.size).toBe(2);
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 = originalDocument.createElement("div");
150
- const mockElement2 = originalDocument.createElement("div");
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 inject one script (no templates - executes once per component type)
161
- expect(injectedScripts.size).toBe(1);
162
- const script = injectedScripts.get("meno-script-TestComponent");
163
- expect(script).toBeDefined();
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 = originalDocument.createElement("div");
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 script tag (no templates)
198
- // Component2: 1 script tag (with templates, 1 instance)
199
- expect(injectedScripts.size).toBe(2);
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(injectedScripts.size).toBe(1);
330
- const script = injectedScripts.get("meno-script-TestComponent");
331
- expect(script).toBeDefined();
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 injected
363
- expect(injectedScripts.size).toBe(1);
364
- const script = injectedScripts.get("meno-script-Component1");
365
- expect(script).toBeDefined();
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 = originalDocument.createElement("div");
383
- const mockElement2 = originalDocument.createElement("div");
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 script tag
394
- expect(injectedScripts.size).toBe(2);
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(injectedScripts.size).toBe(0);
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
- * Inject JavaScript as a separate script tag for browser-level isolation
54
- * Each component gets its own script tag - syntax errors in one don't affect others
55
- * @param jsCode - JavaScript code to inject
56
- * @param componentName - Component name for identification
57
- * @param instanceId - Optional instance ID for per-instance scripts
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 injectScriptTag(jsCode: string, componentName: string, instanceId?: string): void {
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 scriptId = instanceId
65
- ? `meno-script-${componentName}-${instanceId}`
66
- : `meno-script-${componentName}`;
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
- // Remove existing script with same ID to allow re-execution
69
- const existing = document.getElementById(scriptId);
70
- if (existing) {
71
- existing.remove();
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, rootPath);
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, rootPath);
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 initJS = `var elements = document.querySelectorAll('[data-component="${componentName}"]');
148
- elements.forEach(function(el) {
149
- var propsStr = el.getAttribute('data-props');
150
- var props = propsStr ? JSON.parse(propsStr) : {};
151
- (function(el, props) {
152
- ${destructure}
153
- ${js}
154
- })(el, props);
155
- });`;
156
-
157
- this.injectScriptTag(initJS, componentName + '-init');
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
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Server Factory
3
- * Creates a configurable Bun server for @meno/core
3
+ * Creates a configurable Bun server for meno-core
4
4
  * Can be extended by @meno/studio for editor functionality
5
5
  */
6
6
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @meno/core/server exports
2
+ * meno-core/server exports
3
3
  * Core server-side utilities for SSR and read APIs
4
4
  * Does NOT include editor-specific routes (that's in @meno/studio)
5
5
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Core API Routes (Read-only)
3
- * These routes are part of @meno/core and handle read operations
3
+ * These routes are part of meno-core and handle read operations
4
4
  */
5
5
 
6
6
  import { API_ROUTES } from '../../../shared/constants';
@@ -50,7 +50,7 @@ export async function handleApiRoutes(
50
50
  }
51
51
 
52
52
  /**
53
- * Handle only core API routes (for @meno/core without editor)
53
+ * Handle only core API routes (for meno-core without editor)
54
54
  */
55
55
  export async function handleCoreOnlyApiRoutes(
56
56
  req: Request,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @meno/core shared exports
2
+ * meno-core shared exports
3
3
  * Re-exports all types, constants, and utilities from shared modules
4
4
  */
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meno-core",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "meno": "./bin/cli.ts"
package/tsconfig.json CHANGED
@@ -5,8 +5,8 @@
5
5
  "outDir": "./dist",
6
6
  "baseUrl": ".",
7
7
  "paths": {
8
- "@meno/core": ["./lib/shared/index.ts"],
9
- "@meno/core/*": ["./lib/*"]
8
+ "meno-core": ["./lib/shared/index.ts"],
9
+ "meno-core/*": ["./lib/*"]
10
10
  }
11
11
  },
12
12
  "include": ["lib/**/*", "entries/**/*", "bin/**/*"],
package/vite.config.ts CHANGED
@@ -37,7 +37,7 @@ export default defineConfig({
37
37
  },
38
38
  resolve: {
39
39
  alias: {
40
- '@meno/core': path.resolve(__dirname, 'lib/shared/index.ts'),
40
+ 'meno-core': path.resolve(__dirname, 'lib/shared/index.ts'),
41
41
  },
42
42
  },
43
43
  })