wu-framework 1.0.0
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/LICENSE +21 -0
- package/README.md +559 -0
- package/package.json +84 -0
- package/src/api/wu-simple.js +316 -0
- package/src/core/wu-app.js +192 -0
- package/src/core/wu-cache.js +374 -0
- package/src/core/wu-core.js +1296 -0
- package/src/core/wu-error-boundary.js +380 -0
- package/src/core/wu-event-bus.js +257 -0
- package/src/core/wu-hooks.js +348 -0
- package/src/core/wu-html-parser.js +280 -0
- package/src/core/wu-loader.js +271 -0
- package/src/core/wu-logger.js +119 -0
- package/src/core/wu-manifest.js +366 -0
- package/src/core/wu-performance.js +226 -0
- package/src/core/wu-plugin.js +213 -0
- package/src/core/wu-proxy-sandbox.js +153 -0
- package/src/core/wu-registry.js +130 -0
- package/src/core/wu-sandbox-pool.js +390 -0
- package/src/core/wu-sandbox.js +720 -0
- package/src/core/wu-script-executor.js +216 -0
- package/src/core/wu-snapshot-sandbox.js +184 -0
- package/src/core/wu-store.js +297 -0
- package/src/core/wu-strategies.js +241 -0
- package/src/core/wu-style-bridge.js +357 -0
- package/src/index.js +690 -0
- package/src/utils/dependency-resolver.js +326 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🚀 WU-FRAMEWORK SIMPLIFIED API
|
|
3
|
+
* Developer-friendly interface for universal microfrontends
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { wu as wuCore } from '../index.js'
|
|
7
|
+
import { logger } from '../core/wu-logger.js'
|
|
8
|
+
|
|
9
|
+
class WuSimpleAPI {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.core = wuCore
|
|
12
|
+
this.autoInitialized = false
|
|
13
|
+
this.defaultConfig = {
|
|
14
|
+
container: '#app',
|
|
15
|
+
mode: 'auto', // auto, spa, microfrontend
|
|
16
|
+
debug: false,
|
|
17
|
+
timeout: 30000,
|
|
18
|
+
retries: 3
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 🌟 SMART MOUNT: Intelligent mounting with auto-discovery
|
|
24
|
+
* wu.mount('dashboard').into('#container')
|
|
25
|
+
* wu.mount('http://localhost:3001/dashboard')
|
|
26
|
+
*/
|
|
27
|
+
mount(appNameOrUrl) {
|
|
28
|
+
const mountAPI = {
|
|
29
|
+
into: async (containerSelector = this.defaultConfig.container) => {
|
|
30
|
+
await this._ensureInitialized()
|
|
31
|
+
|
|
32
|
+
// 🔍 Detect if it's a URL or app name
|
|
33
|
+
if (this._isUrl(appNameOrUrl)) {
|
|
34
|
+
return await this._mountFromUrl(appNameOrUrl, containerSelector)
|
|
35
|
+
} else {
|
|
36
|
+
return await this._mountFromName(appNameOrUrl, containerSelector)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Support direct call: wu.mount('dashboard', '#container')
|
|
42
|
+
if (typeof arguments[1] === 'string') {
|
|
43
|
+
return mountAPI.into(arguments[1])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return mountAPI
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 🚀 QUICK LOAD: One-liner for loading remote microfrontends
|
|
51
|
+
* wu.load('http://localhost:3001/dashboard')
|
|
52
|
+
* wu.load('http://localhost:3001', { name: 'dashboard' })
|
|
53
|
+
*/
|
|
54
|
+
async load(url, options = {}) {
|
|
55
|
+
await this._ensureInitialized()
|
|
56
|
+
|
|
57
|
+
const appName = options.name || this._extractAppName(url)
|
|
58
|
+
const container = options.container || this.defaultConfig.container
|
|
59
|
+
|
|
60
|
+
// Auto-register and mount
|
|
61
|
+
await this._registerFromUrl(url, appName)
|
|
62
|
+
return await this.mount(appName).into(container)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 🎯 BATCH OPERATIONS: Load multiple apps at once
|
|
67
|
+
* wu.loadMany([
|
|
68
|
+
* { name: 'dashboard', url: 'http://localhost:3001' },
|
|
69
|
+
* { name: 'sidebar', url: 'http://localhost:3002' }
|
|
70
|
+
* ])
|
|
71
|
+
*/
|
|
72
|
+
async loadMany(apps) {
|
|
73
|
+
await this._ensureInitialized()
|
|
74
|
+
|
|
75
|
+
const results = []
|
|
76
|
+
for (const app of apps) {
|
|
77
|
+
try {
|
|
78
|
+
await this._registerFromUrl(app.url, app.name)
|
|
79
|
+
results.push({ name: app.name, status: 'registered' })
|
|
80
|
+
} catch (error) {
|
|
81
|
+
results.push({ name: app.name, status: 'failed', error: error.message })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return results
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 🔧 CONFIGURATION: Flexible configuration methods
|
|
90
|
+
* wu.config({ debug: true, timeout: 60000 })
|
|
91
|
+
* wu.debug(true)
|
|
92
|
+
* wu.timeout(30000)
|
|
93
|
+
*/
|
|
94
|
+
config(options = {}) {
|
|
95
|
+
Object.assign(this.defaultConfig, options)
|
|
96
|
+
|
|
97
|
+
if (options.debug) {
|
|
98
|
+
logger.wuDebug('🔧 Configuration updated:', this.defaultConfig)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return this
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 🐛 DEBUG MODE: Quick debug toggle
|
|
106
|
+
* wu.debug(true)
|
|
107
|
+
* wu.debug() // toggle
|
|
108
|
+
*/
|
|
109
|
+
debug(enabled) {
|
|
110
|
+
if (enabled === undefined) {
|
|
111
|
+
this.defaultConfig.debug = !this.defaultConfig.debug
|
|
112
|
+
} else {
|
|
113
|
+
this.defaultConfig.debug = !!enabled
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
logger.wuInfo(`🐛 Debug mode: ${this.defaultConfig.debug ? 'ON' : 'OFF'}`)
|
|
117
|
+
return this
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* ⏱️ TIMEOUT: Set operation timeout
|
|
122
|
+
* wu.timeout(60000) // 60 seconds
|
|
123
|
+
*/
|
|
124
|
+
timeout(ms) {
|
|
125
|
+
this.defaultConfig.timeout = ms
|
|
126
|
+
logger.wuDebug(`⏱️ Timeout set to ${ms}ms`)
|
|
127
|
+
return this
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 🔄 RETRIES: Set retry attempts
|
|
132
|
+
* wu.retries(5)
|
|
133
|
+
*/
|
|
134
|
+
retries(count) {
|
|
135
|
+
this.defaultConfig.retries = count
|
|
136
|
+
logger.wuDebug(`🔄 Retries set to ${count}`)
|
|
137
|
+
return this
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 🎯 DEFAULT CONTAINER: Set default mounting container
|
|
142
|
+
* wu.container('#main-app')
|
|
143
|
+
*/
|
|
144
|
+
container(selector) {
|
|
145
|
+
this.defaultConfig.container = selector
|
|
146
|
+
logger.wuDebug(`🎯 Default container set to ${selector}`)
|
|
147
|
+
return this
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 🌐 GLOBAL SETTINGS: Environment-based configuration
|
|
152
|
+
* wu.development() // Sets debug: true, retries: 1
|
|
153
|
+
* wu.production() // Sets debug: false, retries: 3
|
|
154
|
+
*/
|
|
155
|
+
development() {
|
|
156
|
+
return this.config({
|
|
157
|
+
debug: true,
|
|
158
|
+
retries: 1,
|
|
159
|
+
timeout: 60000
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
production() {
|
|
164
|
+
return this.config({
|
|
165
|
+
debug: false,
|
|
166
|
+
retries: 3,
|
|
167
|
+
timeout: 30000
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 🚀 QUICK SETUP: Common configurations
|
|
173
|
+
* wu.spa() // Single Page App mode
|
|
174
|
+
* wu.micro() // Microfrontend mode
|
|
175
|
+
*/
|
|
176
|
+
spa() {
|
|
177
|
+
return this.config({
|
|
178
|
+
mode: 'spa',
|
|
179
|
+
container: '#app',
|
|
180
|
+
debug: false
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
micro() {
|
|
185
|
+
return this.config({
|
|
186
|
+
mode: 'microfrontend',
|
|
187
|
+
debug: true,
|
|
188
|
+
retries: 5
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 📊 STATUS: Get framework and apps status
|
|
194
|
+
* wu.status()
|
|
195
|
+
*/
|
|
196
|
+
status() {
|
|
197
|
+
const stats = this.core.getStats?.() || {}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
initialized: this.autoInitialized,
|
|
201
|
+
apps: {
|
|
202
|
+
registered: stats.registered || 0,
|
|
203
|
+
mounted: stats.mounted || 0,
|
|
204
|
+
available: stats.apps || []
|
|
205
|
+
},
|
|
206
|
+
health: this.core.quantumState?.dimensionalStability || 'unknown',
|
|
207
|
+
config: this.defaultConfig
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 🧹 CLEANUP: Clean shutdown
|
|
213
|
+
* wu.destroy()
|
|
214
|
+
*/
|
|
215
|
+
async destroy() {
|
|
216
|
+
if (this.core.destroy) {
|
|
217
|
+
await this.core.destroy()
|
|
218
|
+
}
|
|
219
|
+
this.autoInitialized = false
|
|
220
|
+
logger.wuInfo('🧹 Framework destroyed')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
// 🔒 PRIVATE METHODS
|
|
225
|
+
|
|
226
|
+
async _ensureInitialized() {
|
|
227
|
+
if (!this.autoInitialized) {
|
|
228
|
+
await this._autoInitialize()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async _autoInitialize() {
|
|
233
|
+
if (this.defaultConfig.debug) {
|
|
234
|
+
logger.wuDebug('🚀 Auto-initializing Wu Framework...')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Smart initialization with minimal config
|
|
238
|
+
await this.core.init({ apps: [] })
|
|
239
|
+
this.autoInitialized = true
|
|
240
|
+
|
|
241
|
+
if (this.defaultConfig.debug) {
|
|
242
|
+
logger.wuDebug('✅ Wu Framework auto-initialized')
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
_isUrl(str) {
|
|
247
|
+
try {
|
|
248
|
+
new URL(str)
|
|
249
|
+
return true
|
|
250
|
+
} catch {
|
|
251
|
+
return str.includes('://') || str.startsWith('http')
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_extractAppName(url) {
|
|
256
|
+
try {
|
|
257
|
+
const urlObj = new URL(url)
|
|
258
|
+
const pathSegments = urlObj.pathname.split('/').filter(Boolean)
|
|
259
|
+
return pathSegments[pathSegments.length - 1] || urlObj.hostname.split('.')[0]
|
|
260
|
+
} catch {
|
|
261
|
+
return 'app-' + Date.now()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async _registerFromUrl(url, appName) {
|
|
266
|
+
const baseUrl = this._getBaseUrl(url)
|
|
267
|
+
|
|
268
|
+
await this.core.registerApp({
|
|
269
|
+
name: appName,
|
|
270
|
+
url: baseUrl
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_getBaseUrl(url) {
|
|
275
|
+
try {
|
|
276
|
+
const urlObj = new URL(url)
|
|
277
|
+
return `${urlObj.protocol}//${urlObj.host}`
|
|
278
|
+
} catch {
|
|
279
|
+
return url
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async _mountFromUrl(url, containerSelector) {
|
|
284
|
+
const appName = this._extractAppName(url)
|
|
285
|
+
await this._registerFromUrl(url, appName)
|
|
286
|
+
return await this._mountFromName(appName, containerSelector)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async _mountFromName(appName, containerSelector) {
|
|
290
|
+
try {
|
|
291
|
+
await this.core.mount(appName, containerSelector)
|
|
292
|
+
|
|
293
|
+
if (this.defaultConfig.debug) {
|
|
294
|
+
logger.wuDebug(`✅ ${appName} mounted successfully in ${containerSelector}`)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
app: appName,
|
|
299
|
+
container: containerSelector,
|
|
300
|
+
status: 'mounted',
|
|
301
|
+
unmount: () => this.core.unmount(appName)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error(`[Wu] ❌ Failed to mount ${appName}:`, error.message)
|
|
306
|
+
throw new Error(`Failed to mount ${appName}: ${error.message}`)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 🌟 Create singleton instance
|
|
312
|
+
const wuSimple = new WuSimpleAPI()
|
|
313
|
+
|
|
314
|
+
// 🎯 Export simple API
|
|
315
|
+
export { wuSimple as wu }
|
|
316
|
+
export default wuSimple
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🎯 WU-APP: SIMPLIFIED API WRAPPER
|
|
3
|
+
*
|
|
4
|
+
* Wrapper simple para uso declarativo de microfrontends
|
|
5
|
+
* Mantiene todo el core de wu-framework pero simplifica el uso
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class WuApp {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} name - Nombre de la app
|
|
11
|
+
* @param {Object} config - Configuración de la app
|
|
12
|
+
* @param {string} config.url - URL donde está corriendo la app
|
|
13
|
+
* @param {string} [config.container] - Selector del contenedor (opcional)
|
|
14
|
+
* @param {Object} wu - Instancia de WuCore
|
|
15
|
+
*/
|
|
16
|
+
constructor(name, config, wu) {
|
|
17
|
+
this.name = name
|
|
18
|
+
this.url = config.url
|
|
19
|
+
this.container = config.container
|
|
20
|
+
this._wu = wu
|
|
21
|
+
this._mounted = false
|
|
22
|
+
this._autoInit = config.autoInit !== false // Default true
|
|
23
|
+
|
|
24
|
+
// Auto-register app in wu-framework
|
|
25
|
+
if (this._autoInit) {
|
|
26
|
+
this._registerApp()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Registrar app en wu-framework
|
|
32
|
+
* @private
|
|
33
|
+
*/
|
|
34
|
+
_registerApp() {
|
|
35
|
+
if (!this._wu.apps.has(this.name)) {
|
|
36
|
+
// Usar el método interno de wu-framework para registrar
|
|
37
|
+
this._wu.apps.set(this.name, {
|
|
38
|
+
name: this.name,
|
|
39
|
+
url: this.url,
|
|
40
|
+
status: 'registered'
|
|
41
|
+
})
|
|
42
|
+
console.log(`📦 App registered: ${this.name} at ${this.url}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Montar la app en el contenedor
|
|
48
|
+
* @param {string} [container] - Selector del contenedor (opcional, usa config.container si no se pasa)
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async mount(container) {
|
|
52
|
+
const targetContainer = container || this.container
|
|
53
|
+
|
|
54
|
+
if (!targetContainer) {
|
|
55
|
+
throw new Error(`Container not specified for app: ${this.name}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Asegurar que wu-framework está inicializado
|
|
59
|
+
if (!this._wu.initialized) {
|
|
60
|
+
await this._wu.init({
|
|
61
|
+
apps: [{ name: this.name, url: this.url }]
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Montar usando wu-framework core
|
|
66
|
+
await this._wu.mount(this.name, targetContainer)
|
|
67
|
+
this._mounted = true
|
|
68
|
+
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Desmontar la app
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
async unmount() {
|
|
77
|
+
if (!this._mounted) {
|
|
78
|
+
console.warn(`⚠️ App ${this.name} is not mounted`)
|
|
79
|
+
return this
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await this._wu.unmount(this.name)
|
|
83
|
+
this._mounted = false
|
|
84
|
+
|
|
85
|
+
return this
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Remontar la app (útil para recargas)
|
|
90
|
+
* @param {string} [container] - Selector del contenedor
|
|
91
|
+
* @returns {Promise<void>}
|
|
92
|
+
*/
|
|
93
|
+
async remount(container) {
|
|
94
|
+
await this.unmount()
|
|
95
|
+
await this.mount(container)
|
|
96
|
+
return this
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Verificar si la app está montada
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
get isMounted() {
|
|
104
|
+
return this._mounted && this._wu.mounted?.has(this.name)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Obtener información de la app
|
|
109
|
+
* @returns {Object}
|
|
110
|
+
*/
|
|
111
|
+
get info() {
|
|
112
|
+
return {
|
|
113
|
+
name: this.name,
|
|
114
|
+
url: this.url,
|
|
115
|
+
container: this.container,
|
|
116
|
+
mounted: this.isMounted,
|
|
117
|
+
status: this._wu.apps.get(this.name)?.status || 'unknown'
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Recargar la app (limpiar cache y remontar)
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
async reload() {
|
|
126
|
+
console.log(`🔄 Reloading app: ${this.name}`)
|
|
127
|
+
|
|
128
|
+
await this.unmount()
|
|
129
|
+
|
|
130
|
+
// Limpiar caches
|
|
131
|
+
if (this._wu.loader?.clearCache) {
|
|
132
|
+
this._wu.loader.clearCache(this.name)
|
|
133
|
+
}
|
|
134
|
+
if (this._wu.manifest?.clearCache) {
|
|
135
|
+
this._wu.manifest.clearCache(this.name)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await this.mount()
|
|
139
|
+
console.log(`✅ App reloaded: ${this.name}`)
|
|
140
|
+
|
|
141
|
+
return this
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Verificar el estado de la app
|
|
146
|
+
* @returns {Object}
|
|
147
|
+
*/
|
|
148
|
+
async verify() {
|
|
149
|
+
const container = document.querySelector(this.container)
|
|
150
|
+
const hasShadowDOM = container?.shadowRoot !== null
|
|
151
|
+
const hasContent = (container?.shadowRoot?.children?.length || 0) > 0
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
name: this.name,
|
|
155
|
+
mounted: this.isMounted,
|
|
156
|
+
container: {
|
|
157
|
+
found: !!container,
|
|
158
|
+
selector: this.container,
|
|
159
|
+
hasShadowDOM,
|
|
160
|
+
hasContent
|
|
161
|
+
},
|
|
162
|
+
wu: {
|
|
163
|
+
registered: this._wu.apps.has(this.name),
|
|
164
|
+
mountedInWu: this._wu.mounted?.has(this.name)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Shorthand para mount
|
|
171
|
+
*/
|
|
172
|
+
async start(container) {
|
|
173
|
+
return await this.mount(container)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Shorthand para unmount
|
|
178
|
+
*/
|
|
179
|
+
async stop() {
|
|
180
|
+
return await this.unmount()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Destruir la app completamente
|
|
185
|
+
*/
|
|
186
|
+
async destroy() {
|
|
187
|
+
await this.unmount()
|
|
188
|
+
this._wu.apps.delete(this.name)
|
|
189
|
+
this._mounted = false
|
|
190
|
+
console.log(`🗑️ App destroyed: ${this.name}`)
|
|
191
|
+
}
|
|
192
|
+
}
|