slicejs-web-framework 3.4.4 → 3.5.1
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/Slice/Components/Structural/Controller/Controller.js +135 -0
- package/Slice/Components/Structural/EventManager/EventManager.js +289 -4
- package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +368 -30
- package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -0
- package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +56 -0
- package/Slice/Slice.js +14 -1
- package/Slice/tests/event-emitters.test.js +79 -0
- package/Slice/tests/event-registry.test.js +141 -0
- package/Slice/tests/leak-orphans.test.js +131 -0
- package/api/{middleware → framework}/securityMiddleware.js +18 -74
- package/api/framework/server.js +333 -0
- package/api/index.js +5 -279
- package/api/tests/public-env-resolver.test.js +1 -1
- package/package.json +4 -2
- package/sliceConfig.schema.json +10 -1
- package/src/App/index.html +31 -1
- package/src/manifest.json +15 -0
- package/src/robots.txt +4 -0
- package/src/sitemap.xml +8 -0
- package/src/sliceConfig.json +5 -1
- package/types/index.d.ts +12 -0
- package/slicejs-cli/.github/workflows/ci.yml +0 -43
- package/slicejs-cli/LICENSE +0 -21
- package/slicejs-cli/assets/Slice.js-logo.png +0 -0
- package/slicejs-cli/client.js +0 -740
- package/slicejs-cli/commands/Print.js +0 -164
- package/slicejs-cli/commands/Validations.js +0 -92
- package/slicejs-cli/commands/build/build.js +0 -40
- package/slicejs-cli/commands/buildProduction/buildProduction.js +0 -577
- package/slicejs-cli/commands/bundle/bundle.js +0 -236
- package/slicejs-cli/commands/createComponent/VisualComponentTemplate.js +0 -55
- package/slicejs-cli/commands/createComponent/createComponent.js +0 -128
- package/slicejs-cli/commands/deleteComponent/deleteComponent.js +0 -81
- package/slicejs-cli/commands/doctor/doctor.js +0 -516
- package/slicejs-cli/commands/getComponent/getComponent.js +0 -695
- package/slicejs-cli/commands/init/init.js +0 -473
- package/slicejs-cli/commands/listComponents/listComponents.js +0 -172
- package/slicejs-cli/commands/startServer/startServer.js +0 -240
- package/slicejs-cli/commands/startServer/watchServer.js +0 -66
- package/slicejs-cli/commands/types/types.js +0 -583
- package/slicejs-cli/commands/utils/LocalCliDelegation.js +0 -53
- package/slicejs-cli/commands/utils/PackageManager.js +0 -148
- package/slicejs-cli/commands/utils/PathHelper.js +0 -75
- package/slicejs-cli/commands/utils/VersionChecker.js +0 -169
- package/slicejs-cli/commands/utils/bundling/BundleGenerator.js +0 -2525
- package/slicejs-cli/commands/utils/bundling/DependencyAnalyzer.js +0 -933
- package/slicejs-cli/commands/utils/loadConfig.js +0 -31
- package/slicejs-cli/commands/utils/sliceScripts.js +0 -48
- package/slicejs-cli/commands/utils/updateManager.js +0 -471
- package/slicejs-cli/package.json +0 -71
- package/slicejs-cli/playwright.config.js +0 -51
- package/slicejs-cli/pnpm-lock.yaml +0 -1629
- package/slicejs-cli/tests/build-command-integration.test.js +0 -87
- package/slicejs-cli/tests/build-mangling-e2e.test.js +0 -290
- package/slicejs-cli/tests/build-production-e2e.test.js +0 -140
- package/slicejs-cli/tests/builder-edge-cases.test.js +0 -322
- package/slicejs-cli/tests/bundle-generate-e2e.test.js +0 -115
- package/slicejs-cli/tests/bundle-generator.test.js +0 -691
- package/slicejs-cli/tests/bundle-v2-register-output.test.js +0 -470
- package/slicejs-cli/tests/bundling-dependency-edges.test.js +0 -127
- package/slicejs-cli/tests/bundling-imports-unit.test.js +0 -267
- package/slicejs-cli/tests/client-launcher-contract.test.js +0 -211
- package/slicejs-cli/tests/commands-component-crud.test.js +0 -128
- package/slicejs-cli/tests/commands-doctor.test.js +0 -152
- package/slicejs-cli/tests/commands-version-checker.test.js +0 -37
- package/slicejs-cli/tests/component-registry-parse.test.js +0 -34
- package/slicejs-cli/tests/dependency-analyzer.test.js +0 -24
- package/slicejs-cli/tests/e2e/bundles.spec.js +0 -91
- package/slicejs-cli/tests/e2e/dependency-scenarios.spec.js +0 -56
- package/slicejs-cli/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
- package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
- package/slicejs-cli/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
- package/slicejs-cli/tests/e2e/fixtures/components/registry.json +0 -12
- package/slicejs-cli/tests/e2e/fixtures/vendor-components.mjs +0 -65
- package/slicejs-cli/tests/e2e/navigation.spec.js +0 -44
- package/slicejs-cli/tests/e2e/render.spec.js +0 -34
- package/slicejs-cli/tests/e2e/serve.mjs +0 -264
- package/slicejs-cli/tests/e2e/shared-deps.spec.js +0 -61
- package/slicejs-cli/tests/e2e/unminified.spec.js +0 -33
- package/slicejs-cli/tests/e2e-serve.test.js +0 -148
- package/slicejs-cli/tests/fixtures/components.js +0 -8
- package/slicejs-cli/tests/fixtures/sliceConfig.json +0 -74
- package/slicejs-cli/tests/getcomponent.test.js +0 -435
- package/slicejs-cli/tests/helpers/setup.js +0 -102
- package/slicejs-cli/tests/init-command-contract.test.js +0 -101
- package/slicejs-cli/tests/init-e2e.test.js +0 -82
- package/slicejs-cli/tests/init-project-isolation.test.js +0 -98
- package/slicejs-cli/tests/local-cli-delegation.test.js +0 -81
- package/slicejs-cli/tests/package-manager.test.js +0 -127
- package/slicejs-cli/tests/path-helper.test.js +0 -206
- package/slicejs-cli/tests/perf-budget.test.js +0 -84
- package/slicejs-cli/tests/postinstall-command.test.js +0 -103
- package/slicejs-cli/tests/property-mangling-regression.test.js +0 -182
- package/slicejs-cli/tests/types-breakage.test.js +0 -491
- package/slicejs-cli/tests/types-generator-errors.test.js +0 -361
- package/slicejs-cli/tests/types-generator.test.js +0 -348
- /package/api/{utils → framework}/publicEnvResolver.js +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LeakInspector — dev-only memory-leak panel.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces components that stay registered in `slice.controller.activeComponents`
|
|
5
|
+
* while detached from the live DOM (built then cleared via innerHTML=''/remove()
|
|
6
|
+
* without destroyComponent). Intentionally-cached components (Route/MultiRoute/
|
|
7
|
+
* Router-managed) are excluded by the Controller's findOrphans() criteria.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors EventManagerDebugger: draggable fixed panel, conditional auto-refresh
|
|
10
|
+
* that only runs while open. Read-only except for the per-orphan Destroy action.
|
|
11
|
+
*/
|
|
12
|
+
export default class LeakInspector extends HTMLElement {
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.isOpen = false;
|
|
16
|
+
this._autoRefreshTimer = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async init() {
|
|
20
|
+
this.innerHTML = this.renderTemplate();
|
|
21
|
+
slice.stylesManager.registerComponentStyles('LeakInspector', this.renderStyles());
|
|
22
|
+
this.cacheElements();
|
|
23
|
+
this.bindEvents();
|
|
24
|
+
this.makeDraggable();
|
|
25
|
+
this.render();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
toggle() {
|
|
29
|
+
this.isOpen ? this.close() : this.open();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
open() {
|
|
33
|
+
this.isOpen = true;
|
|
34
|
+
this.container.classList.add('active');
|
|
35
|
+
this._startAutoRefresh();
|
|
36
|
+
this.render();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
close() {
|
|
40
|
+
this.isOpen = false;
|
|
41
|
+
this.container.classList.remove('active');
|
|
42
|
+
this._stopAutoRefresh();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_startAutoRefresh() {
|
|
46
|
+
this._stopAutoRefresh();
|
|
47
|
+
this._autoRefreshTimer = setInterval(() => {
|
|
48
|
+
if (!this.isOpen) return;
|
|
49
|
+
this.render();
|
|
50
|
+
}, 1500);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_stopAutoRefresh() {
|
|
54
|
+
if (this._autoRefreshTimer) {
|
|
55
|
+
clearInterval(this._autoRefreshTimer);
|
|
56
|
+
this._autoRefreshTimer = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
cacheElements() {
|
|
61
|
+
this.container = this.querySelector('#leak-inspector');
|
|
62
|
+
this.header = this.querySelector('.leak-header');
|
|
63
|
+
this.list = this.querySelector('#leak-list');
|
|
64
|
+
this.countLabel = this.querySelector('#leak-count');
|
|
65
|
+
this.sizeLabel = this.querySelector('#leak-size');
|
|
66
|
+
this.growthLabel = this.querySelector('#leak-growth');
|
|
67
|
+
this.refreshButton = this.querySelector('#leak-refresh');
|
|
68
|
+
this.closeButton = this.querySelector('#leak-close');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
bindEvents() {
|
|
72
|
+
this.refreshButton.addEventListener('click', () => this.render());
|
|
73
|
+
this.closeButton.addEventListener('click', () => this.close());
|
|
74
|
+
// Per-orphan Destroy (event delegation).
|
|
75
|
+
this.list.addEventListener('click', (event) => {
|
|
76
|
+
const button = event.target.closest('[data-destroy]');
|
|
77
|
+
if (!button) return;
|
|
78
|
+
const sliceId = button.getAttribute('data-destroy');
|
|
79
|
+
const component = slice.controller.activeComponents.get(sliceId);
|
|
80
|
+
if (component) {
|
|
81
|
+
slice.controller.destroyComponent(component);
|
|
82
|
+
this.render();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
render() {
|
|
88
|
+
const controller = slice?.controller;
|
|
89
|
+
if (!controller || !(controller.activeComponents instanceof Map)) {
|
|
90
|
+
this.list.textContent = 'Controller not available.';
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
controller.sampleComponentGrowth();
|
|
95
|
+
const orphans = controller.findOrphans();
|
|
96
|
+
|
|
97
|
+
this.sizeLabel.textContent = String(controller.activeComponents.size);
|
|
98
|
+
this.countLabel.textContent = String(orphans.length);
|
|
99
|
+
|
|
100
|
+
const growing = controller.isGrowthMonotonic();
|
|
101
|
+
this.growthLabel.textContent = growing ? 'growing ▲' : 'stable';
|
|
102
|
+
this.growthLabel.classList.toggle('warn', growing);
|
|
103
|
+
|
|
104
|
+
if (orphans.length === 0) {
|
|
105
|
+
this.list.innerHTML = '<div class="empty">No leaks detected ✓</div>';
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.list.innerHTML = orphans.map((orphan) => {
|
|
110
|
+
const path = orphan.retainPath.map((p) => this.escapeHtml(p)).join(' ◂ ');
|
|
111
|
+
return `
|
|
112
|
+
<details class="leak-row">
|
|
113
|
+
<summary>
|
|
114
|
+
<div class="leak-name">${this.escapeHtml(orphan.name)}</div>
|
|
115
|
+
<div class="leak-actions-row">
|
|
116
|
+
<span class="leak-id">${this.escapeHtml(orphan.sliceId)}</span>
|
|
117
|
+
<button class="leak-destroy" data-destroy="${this.escapeHtml(orphan.sliceId)}" title="Destroy now">destroy</button>
|
|
118
|
+
</div>
|
|
119
|
+
</summary>
|
|
120
|
+
<div class="leak-detail">
|
|
121
|
+
<div class="leak-reason">${this.escapeHtml(orphan.reason)}</div>
|
|
122
|
+
<div class="leak-path"><span class="path-key">retains</span> ${path}</div>
|
|
123
|
+
</div>
|
|
124
|
+
</details>
|
|
125
|
+
`;
|
|
126
|
+
}).join('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
makeDraggable() {
|
|
130
|
+
if (!this.header || !this.container) return;
|
|
131
|
+
let offset = { x: 0, y: 0 };
|
|
132
|
+
let isDragging = false;
|
|
133
|
+
this.header.style.cursor = 'grab';
|
|
134
|
+
|
|
135
|
+
this.header.addEventListener('mousedown', (event) => {
|
|
136
|
+
if (event.target.closest('.btn')) return;
|
|
137
|
+
isDragging = true;
|
|
138
|
+
offset.x = event.clientX - this.container.getBoundingClientRect().left;
|
|
139
|
+
offset.y = event.clientY - this.container.getBoundingClientRect().top;
|
|
140
|
+
this.header.style.cursor = 'grabbing';
|
|
141
|
+
});
|
|
142
|
+
document.addEventListener('mousemove', (event) => {
|
|
143
|
+
if (!isDragging) return;
|
|
144
|
+
const rect = this.container.getBoundingClientRect();
|
|
145
|
+
const x = Math.min(Math.max(event.clientX - offset.x, 0), window.innerWidth - rect.width);
|
|
146
|
+
const y = Math.min(Math.max(event.clientY - offset.y, 0), window.innerHeight - rect.height);
|
|
147
|
+
this.container.style.left = `${x}px`;
|
|
148
|
+
this.container.style.top = `${y}px`;
|
|
149
|
+
this.container.style.right = 'auto';
|
|
150
|
+
this.container.style.bottom = 'auto';
|
|
151
|
+
});
|
|
152
|
+
document.addEventListener('mouseup', () => {
|
|
153
|
+
if (!isDragging) return;
|
|
154
|
+
isDragging = false;
|
|
155
|
+
this.header.style.cursor = 'grab';
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
escapeHtml(value) {
|
|
160
|
+
return String(value)
|
|
161
|
+
.replace(/&/g, '&')
|
|
162
|
+
.replace(/</g, '<')
|
|
163
|
+
.replace(/>/g, '>')
|
|
164
|
+
.replace(/"/g, '"');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
renderTemplate() {
|
|
168
|
+
return `
|
|
169
|
+
<div id="leak-inspector">
|
|
170
|
+
<div class="leak-header">
|
|
171
|
+
<div class="brand">
|
|
172
|
+
<span class="status-dot"></span>
|
|
173
|
+
<span class="glyph">⚠</span>
|
|
174
|
+
<span class="title">LEAK INSPECTOR</span>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="actions">
|
|
177
|
+
<button id="leak-refresh" class="btn" title="Refresh" aria-label="Refresh">⟳</button>
|
|
178
|
+
<button id="leak-close" class="btn" title="Close" aria-label="Close">✕</button>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="leak-stats">
|
|
182
|
+
<div class="stat"><span class="stat-label">orphans</span><span id="leak-count" class="stat-value danger">0</span></div>
|
|
183
|
+
<div class="stat"><span class="stat-label">active</span><span id="leak-size" class="stat-value">0</span></div>
|
|
184
|
+
<div class="stat"><span class="stat-label">trend</span><span id="leak-growth" class="stat-value">stable</span></div>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="leak-list" id="leak-list"></div>
|
|
187
|
+
</div>
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
renderStyles() {
|
|
192
|
+
return `
|
|
193
|
+
slice-leak-inspector {
|
|
194
|
+
--li-accent: var(--danger-color, #ff6b6b);
|
|
195
|
+
--li-accent-rgb: 255, 107, 107;
|
|
196
|
+
--li-surface: var(--primary-background-color, rgba(17, 19, 28, 0.86));
|
|
197
|
+
--li-raised: var(--secondary-background-color, rgba(255, 255, 255, 0.035));
|
|
198
|
+
--li-raised-2: var(--tertiary-background-color, rgba(255, 255, 255, 0.06));
|
|
199
|
+
--li-border: var(--medium-color, rgba(255, 255, 255, 0.09));
|
|
200
|
+
--li-text: var(--font-primary-color, #e8eaf2);
|
|
201
|
+
--li-dim: var(--font-secondary-color, #888fa6);
|
|
202
|
+
--li-warn: var(--warning-color, #f5b14c);
|
|
203
|
+
--li-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', 'Cascadia Code', Menlo, Consolas, monospace;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
slice-leak-inspector #leak-inspector {
|
|
207
|
+
position: fixed;
|
|
208
|
+
bottom: 20px;
|
|
209
|
+
left: 20px;
|
|
210
|
+
width: min(380px, calc(100vw - 40px));
|
|
211
|
+
max-height: 64vh;
|
|
212
|
+
background: var(--li-surface);
|
|
213
|
+
border: 1px solid var(--li-border);
|
|
214
|
+
border-radius: 14px;
|
|
215
|
+
box-shadow: 0 24px 60px -12px rgba(0,0,0,0.55), 0 0 38px -18px rgba(var(--li-accent-rgb), 0.5);
|
|
216
|
+
-webkit-backdrop-filter: blur(22px) saturate(1.3);
|
|
217
|
+
backdrop-filter: blur(22px) saturate(1.3);
|
|
218
|
+
display: none;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
z-index: 10001;
|
|
221
|
+
overflow: hidden;
|
|
222
|
+
color: var(--li-text);
|
|
223
|
+
font-family: var(--li-mono);
|
|
224
|
+
}
|
|
225
|
+
slice-leak-inspector #leak-inspector.active {
|
|
226
|
+
display: flex;
|
|
227
|
+
animation: li-in 0.26s cubic-bezier(0.16, 1, 0.3, 1);
|
|
228
|
+
}
|
|
229
|
+
@keyframes li-in { from { opacity: 0; transform: translateY(10px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
|
230
|
+
slice-leak-inspector #leak-inspector * { box-sizing: border-box; }
|
|
231
|
+
slice-leak-inspector #leak-inspector::before {
|
|
232
|
+
content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
|
|
233
|
+
background: linear-gradient(180deg, var(--li-accent), transparent 70%); opacity: 0.85; pointer-events: none;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
slice-leak-inspector .leak-header {
|
|
237
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
238
|
+
padding: 12px 14px;
|
|
239
|
+
background: radial-gradient(120% 140% at 0% 0%, rgba(var(--li-accent-rgb), 0.1), transparent 60%), var(--li-raised);
|
|
240
|
+
border-bottom: 1px solid var(--li-border); user-select: none;
|
|
241
|
+
}
|
|
242
|
+
slice-leak-inspector .brand { display: flex; align-items: center; gap: 9px; }
|
|
243
|
+
slice-leak-inspector .status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--li-accent); animation: li-pulse 2.4s ease-out infinite; }
|
|
244
|
+
@keyframes li-pulse { 0% { box-shadow: 0 0 0 0 rgba(var(--li-accent-rgb), 0.55); } 70% { box-shadow: 0 0 0 7px rgba(var(--li-accent-rgb), 0); } 100% { box-shadow: 0 0 0 0 rgba(var(--li-accent-rgb), 0); } }
|
|
245
|
+
slice-leak-inspector .glyph { color: var(--li-accent); font-size: 12px; }
|
|
246
|
+
slice-leak-inspector .title { font-weight: 600; font-size: 11px; letter-spacing: 0.16em; color: var(--li-text); }
|
|
247
|
+
slice-leak-inspector .actions { display: flex; gap: 6px; }
|
|
248
|
+
slice-leak-inspector .btn {
|
|
249
|
+
width: 26px; height: 26px; display: flex; align-items: center; justify-content: center;
|
|
250
|
+
border-radius: 7px; border: 1px solid var(--li-border); background: var(--li-raised);
|
|
251
|
+
color: var(--li-dim); cursor: pointer; font-size: 13px; line-height: 1;
|
|
252
|
+
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
|
|
253
|
+
}
|
|
254
|
+
slice-leak-inspector .btn:hover { color: var(--li-text); background: var(--li-raised-2); border-color: rgba(var(--li-accent-rgb), 0.5); }
|
|
255
|
+
slice-leak-inspector .btn:active { transform: scale(0.92); }
|
|
256
|
+
|
|
257
|
+
slice-leak-inspector .leak-stats {
|
|
258
|
+
display: flex; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--li-border); background: var(--li-raised);
|
|
259
|
+
}
|
|
260
|
+
slice-leak-inspector .stat { flex: 1; display: flex; flex-direction: column; gap: 3px; align-items: center; padding: 6px 4px; border-radius: 8px; background: rgba(0,0,0,0.18); border: 1px solid var(--li-border); }
|
|
261
|
+
slice-leak-inspector .stat-label { font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--li-dim); }
|
|
262
|
+
slice-leak-inspector .stat-value { font-size: 16px; font-weight: 700; color: var(--li-text); }
|
|
263
|
+
slice-leak-inspector .stat-value.danger { color: var(--li-accent); }
|
|
264
|
+
slice-leak-inspector .stat-value.warn { color: var(--li-warn); }
|
|
265
|
+
|
|
266
|
+
slice-leak-inspector .leak-list { padding: 10px 12px 12px; overflow: auto; display: flex; flex-direction: column; gap: 7px; }
|
|
267
|
+
slice-leak-inspector .leak-list::-webkit-scrollbar { width: 8px; }
|
|
268
|
+
slice-leak-inspector .leak-list::-webkit-scrollbar-thumb { background: var(--li-raised-2); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
|
|
269
|
+
|
|
270
|
+
slice-leak-inspector .leak-row { display: block; padding: 9px 11px; background: var(--li-raised); border-radius: 9px; border: 1px solid var(--li-border); border-left: 2px solid var(--li-accent); }
|
|
271
|
+
slice-leak-inspector .leak-row summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; cursor: pointer; list-style: none; }
|
|
272
|
+
slice-leak-inspector .leak-row summary::-webkit-details-marker { display: none; }
|
|
273
|
+
slice-leak-inspector .leak-name { font-family: var(--li-mono); font-size: 12px; font-weight: 600; color: var(--li-text); }
|
|
274
|
+
slice-leak-inspector .leak-actions-row { display: flex; align-items: center; gap: 8px; }
|
|
275
|
+
slice-leak-inspector .leak-id { font-size: 10px; color: var(--li-dim); }
|
|
276
|
+
slice-leak-inspector .leak-destroy {
|
|
277
|
+
all: unset; cursor: pointer; font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase;
|
|
278
|
+
padding: 2px 8px; border-radius: 999px; color: var(--li-accent);
|
|
279
|
+
background: rgba(var(--li-accent-rgb), 0.12); border: 1px solid rgba(var(--li-accent-rgb), 0.3);
|
|
280
|
+
}
|
|
281
|
+
slice-leak-inspector .leak-destroy:hover { background: rgba(var(--li-accent-rgb), 0.22); }
|
|
282
|
+
slice-leak-inspector .leak-detail { margin-top: 9px; padding-top: 9px; border-top: 1px dashed var(--li-border); display: flex; flex-direction: column; gap: 6px; }
|
|
283
|
+
slice-leak-inspector .leak-reason { font-size: 11px; color: var(--li-dim); }
|
|
284
|
+
slice-leak-inspector .leak-path { font-family: var(--li-mono); font-size: 10.5px; color: var(--li-text); word-break: break-word; }
|
|
285
|
+
slice-leak-inspector .path-key { color: var(--li-dim); margin-right: 6px; }
|
|
286
|
+
slice-leak-inspector .empty { color: var(--li-dim); font-size: 11px; letter-spacing: 0.04em; text-align: center; padding: 22px 0; }
|
|
287
|
+
|
|
288
|
+
@media (prefers-reduced-motion: reduce) {
|
|
289
|
+
slice-leak-inspector #leak-inspector.active { animation: none; }
|
|
290
|
+
slice-leak-inspector .status-dot { animation: none; }
|
|
291
|
+
}
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
customElements.define('slice-leak-inspector', LeakInspector);
|
|
@@ -101,6 +101,62 @@ export default class ThemeManager {
|
|
|
101
101
|
if (typeof document !== 'undefined' && document.documentElement) {
|
|
102
102
|
document.documentElement.setAttribute('data-slice-theme', themeName);
|
|
103
103
|
}
|
|
104
|
+
this._syncThemeColor();
|
|
104
105
|
slice.logger.logInfo('ThemeManager', `Theme ${themeName} applied`);
|
|
105
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sync <meta name="theme-color"> to the theme's primary background color.
|
|
110
|
+
* Reads the CSS variable --primary-background-color from computed styles.
|
|
111
|
+
* @returns {void}
|
|
112
|
+
*/
|
|
113
|
+
/**
|
|
114
|
+
* Sync <meta name="theme-color"> and <meta name="color-scheme">
|
|
115
|
+
* from --primary-background-color (the active theme's body bg).
|
|
116
|
+
* @returns {void}
|
|
117
|
+
*/
|
|
118
|
+
_syncThemeColor() {
|
|
119
|
+
if (typeof document === 'undefined') return;
|
|
120
|
+
const bg = getComputedStyle(document.documentElement)
|
|
121
|
+
.getPropertyValue('--primary-background-color').trim();
|
|
122
|
+
if (!bg) return;
|
|
123
|
+
|
|
124
|
+
let meta = document.querySelector('meta[name="theme-color"]');
|
|
125
|
+
if (!meta) {
|
|
126
|
+
meta = document.createElement('meta');
|
|
127
|
+
meta.name = 'theme-color';
|
|
128
|
+
document.head.appendChild(meta);
|
|
129
|
+
}
|
|
130
|
+
meta.content = bg;
|
|
131
|
+
|
|
132
|
+
const scheme = this._isLightColor(bg) ? 'light' : 'dark';
|
|
133
|
+
let cs = document.querySelector('meta[name="color-scheme"]');
|
|
134
|
+
if (!cs) {
|
|
135
|
+
cs = document.createElement('meta');
|
|
136
|
+
cs.name = 'color-scheme';
|
|
137
|
+
document.head.appendChild(cs);
|
|
138
|
+
}
|
|
139
|
+
cs.content = scheme;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Rough luminance check for a hex color.
|
|
144
|
+
* Returns true when the color is perceived as light.
|
|
145
|
+
* @param {string} color hex (#fff / #282828)
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
_isLightColor(color) {
|
|
149
|
+
const hex = color.replace('#', '');
|
|
150
|
+
let r, g, b;
|
|
151
|
+
if (hex.length <= 3) {
|
|
152
|
+
r = parseInt(hex[0] + hex[0], 16);
|
|
153
|
+
g = parseInt(hex[1] + hex[1], 16);
|
|
154
|
+
b = parseInt(hex[2] + hex[2], 16);
|
|
155
|
+
} else {
|
|
156
|
+
r = parseInt(hex.slice(0, 2), 16);
|
|
157
|
+
g = parseInt(hex.slice(2, 4), 16);
|
|
158
|
+
b = parseInt(hex.slice(4, 6), 16);
|
|
159
|
+
}
|
|
160
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.5;
|
|
161
|
+
}
|
|
106
162
|
}
|
package/Slice/Slice.js
CHANGED
|
@@ -541,6 +541,14 @@ async function init() {
|
|
|
541
541
|
document.body.appendChild(window.slice.contextDebugger);
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
+
if (sliceConfig.debugger?.leakInspector?.enabled && !window.slice.isProduction()) {
|
|
545
|
+
const LeakInspectorModule = window.slice.frameworkClasses?.LeakInspector
|
|
546
|
+
|| await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/LeakInspector/LeakInspector.js`);
|
|
547
|
+
window.slice.leakInspector = new LeakInspectorModule();
|
|
548
|
+
await window.slice.leakInspector.init();
|
|
549
|
+
document.body.appendChild(window.slice.leakInspector);
|
|
550
|
+
}
|
|
551
|
+
|
|
544
552
|
if (sliceConfig.events?.enabled) {
|
|
545
553
|
const EventManagerModule = window.slice.frameworkClasses?.EventManager
|
|
546
554
|
|| await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/EventManager/EventManager.js`);
|
|
@@ -554,10 +562,14 @@ async function init() {
|
|
|
554
562
|
subscribeOnce: () => null,
|
|
555
563
|
unsubscribe: () => false,
|
|
556
564
|
emit: () => {},
|
|
565
|
+
register: () => window.slice.events,
|
|
566
|
+
loadGraph: () => window.slice.events,
|
|
567
|
+
isDeclared: () => false,
|
|
557
568
|
bind: () => ({
|
|
558
569
|
subscribe: () => null,
|
|
559
570
|
subscribeOnce: () => null,
|
|
560
571
|
emit: () => {},
|
|
572
|
+
register: () => window.slice.events,
|
|
561
573
|
}),
|
|
562
574
|
cleanupComponent: () => 0,
|
|
563
575
|
hasSubscribers: () => false,
|
|
@@ -608,7 +620,7 @@ async function init() {
|
|
|
608
620
|
const stylesInitPromise = window.slice.stylesManager.init();
|
|
609
621
|
const routesModulePromise = import(slice.paths.routesFile);
|
|
610
622
|
|
|
611
|
-
if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut || sliceConfig.logger?.ui?.shortcut) {
|
|
623
|
+
if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut || sliceConfig.logger?.ui?.shortcut || sliceConfig.debugger?.leakInspector?.shortcut) {
|
|
612
624
|
const normalize = (value) => (typeof value === 'string' ? value.toLowerCase() : '');
|
|
613
625
|
const toKey = (event) => {
|
|
614
626
|
const parts = [];
|
|
@@ -627,6 +639,7 @@ async function init() {
|
|
|
627
639
|
[normalize(sliceConfig.events?.ui?.shortcut)]: () => window.slice.eventsDebugger?.toggle?.(),
|
|
628
640
|
[normalize(sliceConfig.context?.ui?.shortcut)]: () => window.slice.contextDebugger?.toggle?.(),
|
|
629
641
|
[normalize(sliceConfig.logger?.ui?.shortcut)]: () => window.slice.logViewer?.toggle?.(),
|
|
642
|
+
[normalize(sliceConfig.debugger?.leakInspector?.shortcut)]: () => window.slice.leakInspector?.toggle?.(),
|
|
630
643
|
};
|
|
631
644
|
|
|
632
645
|
document.addEventListener('keydown', (event) => {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Tests for emitter tracing: who emits each event (bind attribution + dev
|
|
2
|
+
// stack-trace fallback), recorded only while a debugger is "open" (recording).
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
|
|
6
|
+
globalThis.slice = {
|
|
7
|
+
isProduction: () => false,
|
|
8
|
+
controller: { activeComponents: new Map() },
|
|
9
|
+
logger: { logError() {}, logInfo() {}, logWarning() {}, info() {}, error() {} },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const { default: EventManager } = await import('../Components/Structural/EventManager/EventManager.js');
|
|
13
|
+
|
|
14
|
+
test('bound emit is attributed to its component', () => {
|
|
15
|
+
const em = new EventManager();
|
|
16
|
+
em.startRecording();
|
|
17
|
+
const comp = { sliceId: 'card-1', constructor: { name: 'Card' } };
|
|
18
|
+
em.bind(comp).emit('user:login', { id: 1 });
|
|
19
|
+
|
|
20
|
+
const bucket = em.emitters.get('user:login');
|
|
21
|
+
assert.ok(bucket, 'event has emitters bucket');
|
|
22
|
+
const entry = bucket.get('card-1');
|
|
23
|
+
assert.equal(entry.name, 'Card');
|
|
24
|
+
assert.equal(entry.sliceId, 'card-1');
|
|
25
|
+
assert.equal(entry.count, 1);
|
|
26
|
+
|
|
27
|
+
const hist = em.emitHistory.find((e) => e.eventName === 'user:login');
|
|
28
|
+
assert.equal(hist.emitter.sliceId, 'card-1', 'history entry carries the emitter');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('global emit falls back to a dev stack-trace source', () => {
|
|
32
|
+
const em = new EventManager();
|
|
33
|
+
em.startRecording();
|
|
34
|
+
em.emit('router:change');
|
|
35
|
+
|
|
36
|
+
const hist = em.emitHistory.find((e) => e.eventName === 'router:change');
|
|
37
|
+
assert.ok(hist.emitter, 'emitter captured for global emit');
|
|
38
|
+
assert.ok(hist.emitter.source && hist.emitter.source.includes('.js'), 'source is file:line');
|
|
39
|
+
assert.ok(!hist.emitter.source.includes('EventManager.js'), 'attributes the caller, not the manager');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('emitter counts accumulate per distinct source', () => {
|
|
43
|
+
const em = new EventManager();
|
|
44
|
+
em.startRecording();
|
|
45
|
+
em.bind({ sliceId: 'a-1', constructor: { name: 'A' } }).emit('x:y');
|
|
46
|
+
em.bind({ sliceId: 'a-1', constructor: { name: 'A' } }).emit('x:y');
|
|
47
|
+
em.bind({ sliceId: 'b-2', constructor: { name: 'B' } }).emit('x:y');
|
|
48
|
+
|
|
49
|
+
const bucket = em.emitters.get('x:y');
|
|
50
|
+
assert.equal(bucket.get('a-1').count, 2);
|
|
51
|
+
assert.equal(bucket.get('b-2').count, 1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('no recording (debugger closed) => no emitter tracking, zero overhead', () => {
|
|
55
|
+
const em = new EventManager();
|
|
56
|
+
em.emit('a:b');
|
|
57
|
+
em.bind({ sliceId: 'z-1', constructor: { name: 'Z' } }).emit('a:b');
|
|
58
|
+
assert.equal(em.emitters.size, 0);
|
|
59
|
+
assert.equal(em.emitHistory.length, 0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('production: no stack-trace source captured', () => {
|
|
63
|
+
globalThis.slice.isProduction = () => true;
|
|
64
|
+
const em = new EventManager();
|
|
65
|
+
em.startRecording();
|
|
66
|
+
em.emit('p:q');
|
|
67
|
+
const hist = em.emitHistory.find((e) => e.eventName === 'p:q');
|
|
68
|
+
assert.equal(hist.emitter, null, 'no source attribution in production');
|
|
69
|
+
globalThis.slice.isProduction = () => false;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('startRecording resets the emitters map', () => {
|
|
73
|
+
const em = new EventManager();
|
|
74
|
+
em.startRecording();
|
|
75
|
+
em.bind({ sliceId: 'a-1', constructor: { name: 'A' } }).emit('x:y');
|
|
76
|
+
assert.equal(em.emitters.size, 1);
|
|
77
|
+
em.startRecording();
|
|
78
|
+
assert.equal(em.emitters.size, 0, 'fresh session clears prior emitters');
|
|
79
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Unit tests for the EventManager typed-event registry (register/validate/warn).
|
|
2
|
+
// EventManager only depends on the `slice` global for logging + isProduction +
|
|
3
|
+
// controller.activeComponents, so we stub a minimal one and drive the real class.
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
|
|
7
|
+
const warnings = [];
|
|
8
|
+
globalThis.slice = {
|
|
9
|
+
isProduction: () => false,
|
|
10
|
+
controller: { activeComponents: new Map() },
|
|
11
|
+
logger: {
|
|
12
|
+
logError() {},
|
|
13
|
+
logInfo() {},
|
|
14
|
+
logWarning(_c, message) { warnings.push(message); },
|
|
15
|
+
info() {}, error() {}, warn() {},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const { default: EventManager } = await import('../Components/Structural/EventManager/EventManager.js');
|
|
20
|
+
|
|
21
|
+
const undeclaredWarns = (name) => warnings.filter((w) => w.includes('Undeclared') && (!name || w.includes(name)));
|
|
22
|
+
|
|
23
|
+
test('loose mode: with no registry, nothing warns', () => {
|
|
24
|
+
warnings.length = 0;
|
|
25
|
+
const em = new EventManager();
|
|
26
|
+
em.emit('anything:happens');
|
|
27
|
+
em.subscribe('whatever:event', () => {});
|
|
28
|
+
assert.equal(undeclaredWarns().length, 0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('declared event emits and subscribes without warning', () => {
|
|
32
|
+
warnings.length = 0;
|
|
33
|
+
const em = new EventManager();
|
|
34
|
+
em.register({ 'user:login': { payload: { id: 'number' } } });
|
|
35
|
+
em.subscribe('user:login', () => {});
|
|
36
|
+
em.emit('user:login', { id: 1 });
|
|
37
|
+
assert.equal(undeclaredWarns().length, 0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('undeclared event warns exactly once but still delivers', () => {
|
|
41
|
+
warnings.length = 0;
|
|
42
|
+
const em = new EventManager();
|
|
43
|
+
em.register({ 'user:login': { payload: null } });
|
|
44
|
+
|
|
45
|
+
let calls = 0;
|
|
46
|
+
em.subscribe('ghost:event', () => { calls += 1; }); // subscribe (undeclared) -> warns
|
|
47
|
+
em.emit('ghost:event'); // emit (undeclared, already warned)
|
|
48
|
+
em.emit('ghost:event');
|
|
49
|
+
|
|
50
|
+
assert.equal(undeclaredWarns('ghost:event').length, 1, 'warned exactly once');
|
|
51
|
+
assert.equal(calls, 2, 'delivery still happened both emits');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('framework events (router:*/context:*) never warn', () => {
|
|
55
|
+
warnings.length = 0;
|
|
56
|
+
const em = new EventManager();
|
|
57
|
+
em.register({ 'user:login': { payload: null } });
|
|
58
|
+
em.emit('router:change');
|
|
59
|
+
em.emit('context:user', {});
|
|
60
|
+
em.subscribe('context:cart', () => {});
|
|
61
|
+
assert.equal(undeclaredWarns().length, 0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('production: undeclared events do not warn', () => {
|
|
65
|
+
warnings.length = 0;
|
|
66
|
+
globalThis.slice.isProduction = () => true;
|
|
67
|
+
const em = new EventManager();
|
|
68
|
+
em.register({ 'a:b': { payload: null } });
|
|
69
|
+
em.emit('zzz:undeclared');
|
|
70
|
+
assert.equal(undeclaredWarns().length, 0);
|
|
71
|
+
globalThis.slice.isProduction = () => false;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('register() merges catalogs and is chainable', () => {
|
|
75
|
+
const em = new EventManager();
|
|
76
|
+
const ret = em.register({ 'a:1': { payload: null } }).register({ 'b:2': { payload: { x: 'string' } } });
|
|
77
|
+
assert.equal(ret, em, 'returns the manager for chaining');
|
|
78
|
+
assert.ok(em.registry.has('a:1') && em.registry.has('b:2'), 'both catalogs merged');
|
|
79
|
+
assert.equal(em.isDeclared('a:1'), true);
|
|
80
|
+
assert.equal(em.isDeclared('nope:nope'), false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('register(namespace, catalog) prefixes keys', () => {
|
|
84
|
+
const em = new EventManager();
|
|
85
|
+
em.register('user', { login: { payload: { id: 'number' } }, logout: { payload: null } });
|
|
86
|
+
assert.ok(em.registry.has('user:login'), 'user:login declared');
|
|
87
|
+
assert.ok(em.registry.has('user:logout'), 'user:logout declared');
|
|
88
|
+
assert.equal(em.namespaceOf('user:login'), 'user');
|
|
89
|
+
assert.equal(em.namespaceOf('ready'), null);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('event without namespace warns once in dev', () => {
|
|
93
|
+
warnings.length = 0;
|
|
94
|
+
const em = new EventManager();
|
|
95
|
+
em.register({ 'ready': { payload: null } }); // declared but no namespace -> warns
|
|
96
|
+
em.emit('ready');
|
|
97
|
+
em.emit('ready');
|
|
98
|
+
const nsWarns = warnings.filter((w) => w.includes('no namespace'));
|
|
99
|
+
assert.equal(nsWarns.length, 1, 'namespace warning fired exactly once');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('no-namespace warning is silent in loose mode (no registry)', () => {
|
|
103
|
+
warnings.length = 0;
|
|
104
|
+
const em = new EventManager();
|
|
105
|
+
em.emit('ready'); // nobody registered -> loose mode, no warnings at all
|
|
106
|
+
assert.equal(warnings.length, 0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('loadGraph stores the static graph and exposes emitters/listeners', () => {
|
|
110
|
+
const em = new EventManager();
|
|
111
|
+
assert.deepEqual(em.staticEmittersOf('user:login'), [], 'empty before loadGraph');
|
|
112
|
+
em.loadGraph({
|
|
113
|
+
events: {
|
|
114
|
+
'user:login': {
|
|
115
|
+
payload: { id: 'number' },
|
|
116
|
+
emitters: [{ file: 'src/Login.js', line: 12, component: 'Login' }],
|
|
117
|
+
listeners: [{ file: 'src/Header.js', line: 5, component: 'Header' }],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
dynamic: { emitters: [], listeners: [] },
|
|
121
|
+
});
|
|
122
|
+
assert.equal(em.staticEmittersOf('user:login')[0].component, 'Login');
|
|
123
|
+
assert.equal(em.staticListenersOf('user:login')[0].component, 'Header');
|
|
124
|
+
assert.deepEqual(em.staticEmittersOf('unknown:event'), []);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('loadGraph ignores malformed input', () => {
|
|
128
|
+
const em = new EventManager();
|
|
129
|
+
em.loadGraph(null);
|
|
130
|
+
em.loadGraph({ nope: true });
|
|
131
|
+
assert.equal(em.graph, null);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('undeclared usage is tracked for the debugger drift view', () => {
|
|
135
|
+
warnings.length = 0;
|
|
136
|
+
const em = new EventManager();
|
|
137
|
+
em.register({ 'declared:ok': { payload: null } });
|
|
138
|
+
em.emit('drifted:event');
|
|
139
|
+
assert.ok(em.undeclared.has('drifted:event'), 'drift set records undeclared usage');
|
|
140
|
+
assert.ok(!em.undeclared.has('declared:ok'), 'declared events are not drift');
|
|
141
|
+
});
|