wu-framework 1.1.16 → 1.1.17

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,53 +1,53 @@
1
- import type { AstroIntegration } from 'astro';
2
-
3
- /** Configuration for a single wu micro-app. */
4
- export interface WuAppConfig {
5
- /** Unique name used to reference this micro-app. */
6
- name: string;
7
- /** URL or path from which the micro-app is loaded. */
8
- url: string;
9
- /** Loading strategy. Defaults to the framework default. */
10
- strategy?: 'eager' | 'lazy' | 'manual';
11
- }
12
-
13
- /** Options accepted by the wu Astro integration. */
14
- export interface WuIntegrationOptions {
15
- /** Micro-apps to auto-register via wu.init(). */
16
- apps?: WuAppConfig[];
17
- /** Global sandbox isolation mode. */
18
- sandbox?: 'module' | 'strict' | 'eval';
19
- /** CDN URL for the wu-framework UMD bundle. When omitted the local package is used. */
20
- cdn?: string;
21
- /** Enable debug logging. */
22
- debug?: boolean;
23
- }
24
-
25
- /**
26
- * Astro integration factory for wu-framework.
27
- *
28
- * @example
29
- * import wu from '@wu-framework/astro';
30
- * export default defineConfig({ integrations: [wu({ apps: [...] })] });
31
- */
32
- export default function wuIntegration(
33
- options?: WuIntegrationOptions
34
- ): AstroIntegration;
35
-
36
- /** Props for the WuShell.astro layout component. */
37
- export interface WuShellProps {
38
- apps: WuAppConfig[];
39
- sandbox?: 'module' | 'strict' | 'eval';
40
- debug?: boolean;
41
- }
42
-
43
- /** Props for the WuApp.astro mount component. */
44
- export interface WuAppProps {
45
- /** Name of the registered wu micro-app. */
46
- name: string;
47
- /** Defer mounting until the element is visible (IntersectionObserver). */
48
- lazy?: boolean;
49
- /** CSS class(es) for the container div. */
50
- class?: string;
51
- /** Inline styles for the container div. */
52
- style?: string;
53
- }
1
+ import type { AstroIntegration } from 'astro';
2
+
3
+ /** Configuration for a single wu micro-app. */
4
+ export interface WuAppConfig {
5
+ /** Unique name used to reference this micro-app. */
6
+ name: string;
7
+ /** URL or path from which the micro-app is loaded. */
8
+ url: string;
9
+ /** Loading strategy. Defaults to the framework default. */
10
+ strategy?: 'eager' | 'lazy' | 'manual';
11
+ }
12
+
13
+ /** Options accepted by the wu Astro integration. */
14
+ export interface WuIntegrationOptions {
15
+ /** Micro-apps to auto-register via wu.init(). */
16
+ apps?: WuAppConfig[];
17
+ /** Global sandbox isolation mode. */
18
+ sandbox?: 'module' | 'strict' | 'eval';
19
+ /** CDN URL for the wu-framework UMD bundle. When omitted the local package is used. */
20
+ cdn?: string;
21
+ /** Enable debug logging. */
22
+ debug?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Astro integration factory for wu-framework.
27
+ *
28
+ * @example
29
+ * import wu from '@wu-framework/astro';
30
+ * export default defineConfig({ integrations: [wu({ apps: [...] })] });
31
+ */
32
+ export default function wuIntegration(
33
+ options?: WuIntegrationOptions
34
+ ): AstroIntegration;
35
+
36
+ /** Props for the WuShell.astro layout component. */
37
+ export interface WuShellProps {
38
+ apps: WuAppConfig[];
39
+ sandbox?: 'module' | 'strict' | 'eval';
40
+ debug?: boolean;
41
+ }
42
+
43
+ /** Props for the WuApp.astro mount component. */
44
+ export interface WuAppProps {
45
+ /** Name of the registered wu micro-app. */
46
+ name: string;
47
+ /** Defer mounting until the element is visible (IntersectionObserver). */
48
+ lazy?: boolean;
49
+ /** CSS class(es) for the container div. */
50
+ class?: string;
51
+ /** Inline styles for the container div. */
52
+ style?: string;
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wu-framework",
3
- "version": "1.1.16",
3
+ "version": "1.1.17",
4
4
  "description": "Universal Microfrontends Framework - 8 frameworks, zero config, Shadow DOM isolation",
5
5
  "main": "dist/wu-framework.cjs.js",
6
6
  "module": "src/index.js",
@@ -35,7 +35,10 @@
35
35
  "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
36
36
  "test": "vitest run",
37
37
  "test:watch": "vitest",
38
- "test:coverage": "vitest run --coverage"
38
+ "test:coverage": "vitest run --coverage",
39
+ "lint": "eslint src/",
40
+ "format": "prettier --write src/",
41
+ "format:check": "prettier --check src/"
39
42
  },
40
43
  "keywords": [
41
44
  "microfrontends",
@@ -118,7 +121,9 @@
118
121
  "@rollup/plugin-replace": "^5.0.5",
119
122
  "@rollup/plugin-terser": "^0.4.4",
120
123
  "@vitest/coverage-v8": "^4.0.18",
124
+ "eslint": "^10.0.2",
121
125
  "jsdom": "^28.0.0",
126
+ "prettier": "^3.8.1",
122
127
  "rollup": "^4.9.0",
123
128
  "vitest": "^4.0.18"
124
129
  },
@@ -695,7 +695,7 @@ function createWuSlotConfig() {
695
695
 
696
696
  // Implementación JavaScript pura para usar sin .svelte
697
697
  createInstance: (target, props) => {
698
- let container = document.createElement('div');
698
+ const container = document.createElement('div');
699
699
  container.className = 'wu-slot';
700
700
  container.style.minHeight = '100px';
701
701
  container.style.position = 'relative';
@@ -146,7 +146,7 @@ async function register(appName, config, options = {}) {
146
146
  }
147
147
 
148
148
  // Estado local de la app
149
- let appState = { ...state };
149
+ const appState = { ...state };
150
150
 
151
151
  // Función de mount
152
152
  const mountApp = (container) => {
@@ -32,6 +32,9 @@ export class WuCache {
32
32
  cooldownUntil: 0
33
33
  };
34
34
 
35
+ // Rate limit notification flag (log only once per cooldown)
36
+ this._rateLimitNotified = false;
37
+
35
38
  // Memory cache
36
39
  this.memoryCache = new Map();
37
40
 
@@ -66,6 +69,7 @@ export class WuCache {
66
69
  }
67
70
  // Cooldown terminado
68
71
  this.rateLimiting.inCooldown = false;
72
+ this._rateLimitNotified = false;
69
73
  this.rateLimiting.operations = [];
70
74
  }
71
75
 
@@ -91,7 +95,22 @@ export class WuCache {
91
95
  }
92
96
 
93
97
  /**
94
- * 🔐 GET RATE LIMIT STATUS
98
+ * Handle rate-limited operations: log once per cooldown, return diagnostic object
99
+ * @param {string} operation - The operation that was rate limited ('get' or 'set')
100
+ * @param {string} key - The cache key that was rejected
101
+ * @returns {{ rateLimited: true, operation: string, key: string }}
102
+ */
103
+ _onRateLimited(operation, key) {
104
+ if (!this._rateLimitNotified) {
105
+ const cooldownRemaining = Math.max(0, this.rateLimiting.cooldownUntil - Date.now());
106
+ logger.warn(`[WuCache] Rate limited: ${operation} for key "${key}" rejected. ${cooldownRemaining}ms remaining in cooldown.`);
107
+ this._rateLimitNotified = true;
108
+ }
109
+ return { rateLimited: true, operation, key };
110
+ }
111
+
112
+ /**
113
+ * GET RATE LIMIT STATUS
95
114
  */
96
115
  getRateLimitStatus() {
97
116
  const now = Date.now();
@@ -115,7 +134,8 @@ export class WuCache {
115
134
  get(key) {
116
135
  // 🔐 Check rate limit
117
136
  if (!this._checkRateLimit()) {
118
- return null; // Silently fail on rate limit
137
+ this._onRateLimited('get', key);
138
+ return null;
119
139
  }
120
140
 
121
141
  // 1. Buscar en memoria
@@ -162,7 +182,8 @@ export class WuCache {
162
182
  set(key, value, ttl) {
163
183
  // 🔐 Check rate limit
164
184
  if (!this._checkRateLimit()) {
165
- return false; // Reject on rate limit
185
+ this._onRateLimited('set', key);
186
+ return false;
166
187
  }
167
188
 
168
189
  try {
@@ -333,6 +333,20 @@ export class WuCore {
333
333
  } catch (error) {
334
334
  logger.wuError(`Mount attempt ${attempt + 1} failed for ${appName}:`, error);
335
335
 
336
+ // Cleanup sandbox to prevent orphaned shadow DOMs
337
+ try {
338
+ if (this.sandbox && this.sandbox.sandboxes && this.sandbox.sandboxes.has(appName)) {
339
+ const sb = this.sandbox.sandboxes.get(appName);
340
+ if (sb && sb.proxySandbox) {
341
+ sb.proxySandbox.deactivate();
342
+ }
343
+ this.sandbox.sandboxes.delete(appName);
344
+ logger.wuDebug(`Sandbox cleaned up after mount failure for ${appName}`);
345
+ }
346
+ } catch (cleanupError) {
347
+ logger.wuWarn(`Sandbox cleanup failed for ${appName}:`, cleanupError);
348
+ }
349
+
336
350
  // Use error boundary for intelligent error handling
337
351
  const errorResult = await this.errorBoundary.handle(error, {
338
352
  appName,
@@ -671,7 +685,7 @@ export class WuCore {
671
685
  * Intelligently resolves module paths with real-time validation
672
686
  */
673
687
  async resolveModulePath(app) {
674
- let entryFile = app.manifest?.entry || 'main.js';
688
+ const entryFile = app.manifest?.entry || 'main.js';
675
689
  const baseUrl = app.url.replace(/\/$/, ''); // Remove trailing slash
676
690
 
677
691
  // Normalize path: Remove duplicated directories
@@ -314,19 +314,33 @@ export class WuErrorBoundary {
314
314
  * @param {Object} context - Contexto
315
315
  */
316
316
  logError(error, context) {
317
+ // Truncate stack to first 5 lines to prevent retaining large object references
318
+ const stack = error.stack ? error.stack.split('\n').slice(0, 5).join('\n') : '';
319
+
320
+ // Shallow-copy context to avoid retaining references to live objects
321
+ const safeContext = {};
322
+ for (const key of Object.keys(context)) {
323
+ const val = context[key];
324
+ if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean' || val === null) {
325
+ safeContext[key] = val;
326
+ } else {
327
+ safeContext[key] = String(val);
328
+ }
329
+ }
330
+
317
331
  const errorEntry = {
318
332
  error: {
319
333
  name: error.name,
320
334
  message: error.message,
321
- stack: error.stack
335
+ stack
322
336
  },
323
- context,
337
+ context: safeContext,
324
338
  timestamp: Date.now()
325
339
  };
326
340
 
327
341
  this.errorLog.push(errorEntry);
328
342
 
329
- // Mantener límite de log
343
+ // Maintain log limit
330
344
  if (this.errorLog.length > this.maxErrorLog) {
331
345
  this.errorLog.shift();
332
346
  }
@@ -10,6 +10,25 @@
10
10
  */
11
11
  import { logger } from './wu-logger.js';
12
12
 
13
+ /**
14
+ * @typedef {Object} WuEvent
15
+ * @property {string} name - Event name
16
+ * @property {*} data - Event payload
17
+ * @property {number} timestamp - Event timestamp
18
+ * @property {string} appName - Source app name
19
+ * @property {Object} meta - Additional metadata
20
+ * @property {boolean} verified - Whether origin was verified
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} WuEventBusConfig
25
+ * @property {number} [maxHistory=100] - Maximum events in history
26
+ * @property {boolean} [enableReplay=true] - Enable event replay
27
+ * @property {boolean} [enableWildcards=true] - Enable wildcard matching
28
+ * @property {boolean} [logEvents=false] - Log all events
29
+ * @property {boolean} [strictMode=false] - Reject unauthorized events
30
+ * @property {boolean} [validateOrigin=true] - Validate event origins
31
+ */
13
32
 
14
33
  export class WuEventBus {
15
34
  constructor() {
@@ -20,16 +39,21 @@ export class WuEventBus {
20
39
  this.authorizedApps = new Map(); // appName -> { token, permissions }
21
40
  this.trustedEvents = new Set(['wu:*', 'system:*']); // Eventos del sistema
22
41
 
42
+ // Auto-detect production environment for strictMode default
43
+ const isProduction = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';
44
+
23
45
  this.config = {
24
46
  maxHistory: 100,
25
47
  enableReplay: true,
26
48
  enableWildcards: true,
27
49
  logEvents: false,
28
50
  // 🔐 Opciones de seguridad
29
- strictMode: false, // Si true, rechaza eventos de apps no autorizadas
51
+ strictMode: isProduction, // Auto-enabled in production, permissive in development
30
52
  validateOrigin: true // Valida que appName sea una app registrada
31
53
  };
32
54
 
55
+ this._permissiveWarned = false;
56
+
33
57
  this.stats = {
34
58
  emitted: 0,
35
59
  subscriptions: 0,
@@ -127,6 +151,19 @@ export class WuEventBus {
127
151
  return `wu_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
128
152
  }
129
153
 
154
+ /**
155
+ * WARN PERMISSIVE MODE: Log a one-time warning when strictMode is off
156
+ * Alerts developers that events are flowing without authorization checks
157
+ */
158
+ _warnPermissiveMode() {
159
+ if (this._permissiveWarned) return;
160
+ this._permissiveWarned = true;
161
+ logger.warn(
162
+ '[WuEventBus] strictMode is disabled. Events are emitted without authorization checks. ' +
163
+ 'Enable strictMode for production by calling enableStrictMode() or setting NODE_ENV=production.'
164
+ );
165
+ }
166
+
130
167
  /**
131
168
  * 📢 EMIT: Emitir evento con validación de origen
132
169
  * @param {string} eventName - Nombre del evento
@@ -136,6 +173,11 @@ export class WuEventBus {
136
173
  emit(eventName, data, options = {}) {
137
174
  const appName = options.appName || 'unknown';
138
175
 
176
+ // Warn once if running in permissive mode (strictMode off)
177
+ if (!this.config.strictMode) {
178
+ this._warnPermissiveMode();
179
+ }
180
+
139
181
  // 🔐 Validar origen si está habilitado
140
182
  if (this.config.validateOrigin && this.config.strictMode) {
141
183
  if (!this._validateOrigin(eventName, appName, options.token)) {
@@ -62,19 +62,28 @@ export class WuHtmlParser {
62
62
  return this._cache.get(cacheKey);
63
63
  }
64
64
 
65
- const temp = document.createElement('div');
66
- temp.innerHTML = html;
65
+ const parser = new DOMParser();
66
+ const doc = parser.parseFromString(html, 'text/html');
67
67
 
68
68
  const inlineScripts = [];
69
69
  const externalScripts = [];
70
70
  const inlineStyles = [];
71
71
  const externalStyles = [];
72
72
 
73
- this._extractResources(temp, {
73
+ const ctx = {
74
74
  inlineScripts, externalScripts,
75
75
  inlineStyles, externalStyles,
76
76
  baseUrl
77
- });
77
+ };
78
+
79
+ // DOMParser moves <style>, <link>, and some <script> tags to <head>.
80
+ // Extract resources from both head and body to capture everything.
81
+ if (doc.head) {
82
+ this._extractResources(doc.head, ctx);
83
+ }
84
+
85
+ const temp = doc.body || doc.documentElement;
86
+ this._extractResources(temp, ctx);
78
87
 
79
88
  const result = {
80
89
  dom: temp.innerHTML,