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,131 @@
|
|
|
1
|
+
// Real-Controller tests for the dev-mode leak detector (findOrphans + criteria).
|
|
2
|
+
// Loads the REAL Controller via the resolve hook (same pattern as
|
|
3
|
+
// destroy-cascade.test.js) and drives it with a minimal fake DOM. Fake nodes are
|
|
4
|
+
// made `instanceof HTMLElement` because findOrphans filters on that.
|
|
5
|
+
import { register } from 'node:module';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
|
|
9
|
+
globalThis.alert = () => {};
|
|
10
|
+
register(new URL('./fixtures/real-runtime-loader.mjs', import.meta.url));
|
|
11
|
+
|
|
12
|
+
globalThis.HTMLElement = class HTMLElement {};
|
|
13
|
+
globalThis.slice = {
|
|
14
|
+
logger: { logError() {}, logWarning() {}, logInfo() {} },
|
|
15
|
+
events: null,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const { default: Controller } = await import('../Components/Structural/Controller/Controller.js');
|
|
19
|
+
|
|
20
|
+
function mkEl(sliceId, children = []) {
|
|
21
|
+
return Object.assign(Object.create(HTMLElement.prototype), {
|
|
22
|
+
sliceId,
|
|
23
|
+
tagName: 'SLICE-X',
|
|
24
|
+
isConnected: true,
|
|
25
|
+
parentComponent: null,
|
|
26
|
+
_children: children,
|
|
27
|
+
querySelectorAll() {
|
|
28
|
+
const out = [];
|
|
29
|
+
const walk = (n) => { for (const c of n._children) { out.push(c); walk(c); } };
|
|
30
|
+
walk(this);
|
|
31
|
+
return out;
|
|
32
|
+
},
|
|
33
|
+
setAttribute() {},
|
|
34
|
+
remove() { this.isConnected = false; },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function flatten(root) {
|
|
39
|
+
const out = [root];
|
|
40
|
+
for (const c of root._children) out.push(...flatten(c));
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildTree(controller, root) {
|
|
45
|
+
for (const node of flatten(root)) controller.registerComponent(node);
|
|
46
|
+
controller.registerComponentsRecursively(root);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('a built-then-detached component (no destroy) IS flagged', () => {
|
|
50
|
+
const c = new Controller();
|
|
51
|
+
const comp = mkEl('leaky');
|
|
52
|
+
buildTree(c, comp);
|
|
53
|
+
comp.isConnected = false; // simulate innerHTML='' / remove() without destroyComponent
|
|
54
|
+
|
|
55
|
+
const orphans = c.findOrphans();
|
|
56
|
+
assert.equal(orphans.length, 1, 'one orphan');
|
|
57
|
+
assert.equal(orphans[0].sliceId, 'leaky');
|
|
58
|
+
assert.ok(orphans[0].retainPath.includes('Object(leaky)') || orphans[0].retainPath.length >= 1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a connected component is NOT flagged', () => {
|
|
62
|
+
const c = new Controller();
|
|
63
|
+
const comp = mkEl('alive');
|
|
64
|
+
buildTree(c, comp);
|
|
65
|
+
assert.equal(c.findOrphans().length, 0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('__sliceCached components are excluded (Route/MultiRoute cache)', () => {
|
|
69
|
+
const c = new Controller();
|
|
70
|
+
const comp = mkEl('cachedView');
|
|
71
|
+
buildTree(c, comp);
|
|
72
|
+
comp.isConnected = false;
|
|
73
|
+
comp.__sliceCached = true;
|
|
74
|
+
assert.equal(c.findOrphans().length, 0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('router-managed route-* sliceIds are excluded', () => {
|
|
78
|
+
const c = new Controller();
|
|
79
|
+
const comp = mkEl('route-Home');
|
|
80
|
+
buildTree(c, comp);
|
|
81
|
+
comp.isConnected = false;
|
|
82
|
+
assert.equal(c.findOrphans().length, 0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('ancestor de-dup: only the detached root is reported', () => {
|
|
86
|
+
const c = new Controller();
|
|
87
|
+
const child = mkEl('child');
|
|
88
|
+
const parent = mkEl('parent', [child]);
|
|
89
|
+
buildTree(c, parent);
|
|
90
|
+
parent.isConnected = false;
|
|
91
|
+
child.isConnected = false;
|
|
92
|
+
|
|
93
|
+
const orphans = c.findOrphans();
|
|
94
|
+
assert.equal(orphans.length, 1, 'only the root, not the descendant');
|
|
95
|
+
assert.equal(orphans[0].sliceId, 'parent');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('descendants of a cached subtree are NOT flagged', () => {
|
|
99
|
+
const c = new Controller();
|
|
100
|
+
const child = mkEl('child');
|
|
101
|
+
const parent = mkEl('parent', [child]);
|
|
102
|
+
buildTree(c, parent);
|
|
103
|
+
parent.__sliceCached = true; // cached route root
|
|
104
|
+
parent.isConnected = false;
|
|
105
|
+
child.isConnected = false;
|
|
106
|
+
assert.equal(c.findOrphans().length, 0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('app-registered exclusion predicate is honored', () => {
|
|
110
|
+
const c = new Controller();
|
|
111
|
+
const comp = mkEl('special');
|
|
112
|
+
buildTree(c, comp);
|
|
113
|
+
comp.isConnected = false;
|
|
114
|
+
c.registerLeakExclusion((node) => node.sliceId === 'special');
|
|
115
|
+
assert.equal(c.findOrphans().length, 0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('growth heuristic trips on monotonic increase and resets on shrink', () => {
|
|
119
|
+
const c = new Controller();
|
|
120
|
+
assert.equal(c.isGrowthMonotonic(), false, 'needs >= 5 samples');
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < 5; i++) {
|
|
123
|
+
c.registerComponent(mkEl(`g${i}`));
|
|
124
|
+
c.sampleComponentGrowth();
|
|
125
|
+
}
|
|
126
|
+
assert.equal(c.isGrowthMonotonic(), true, 'sizes 1..5 are monotonic growth');
|
|
127
|
+
|
|
128
|
+
c.destroyComponent('g4');
|
|
129
|
+
c.sampleComponentGrowth(); // size dips
|
|
130
|
+
assert.equal(c.isGrowthMonotonic(), false, 'a shrink breaks the monotonic signal');
|
|
131
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// api/
|
|
1
|
+
// api/framework/securityMiddleware.js
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -7,7 +7,17 @@ import path from 'path';
|
|
|
7
7
|
*/
|
|
8
8
|
export function securityMiddleware(options = {}) {
|
|
9
9
|
const {
|
|
10
|
-
allowedExtensions = [
|
|
10
|
+
allowedExtensions = [
|
|
11
|
+
'.js', '.mjs', '.cjs',
|
|
12
|
+
'.css',
|
|
13
|
+
'.html', '.htm',
|
|
14
|
+
'.json',
|
|
15
|
+
'.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.ico',
|
|
16
|
+
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
17
|
+
'.txt', '.xml', '.pdf',
|
|
18
|
+
'.webm', '.mp4', '.mp3', '.wav', '.ogg',
|
|
19
|
+
'.wasm'
|
|
20
|
+
],
|
|
11
21
|
blockedPaths = [
|
|
12
22
|
'/node_modules',
|
|
13
23
|
'/package.json',
|
|
@@ -15,7 +25,8 @@ export function securityMiddleware(options = {}) {
|
|
|
15
25
|
'/.env',
|
|
16
26
|
'/.git'
|
|
17
27
|
],
|
|
18
|
-
allowPublicAssets = true
|
|
28
|
+
allowPublicAssets = true,
|
|
29
|
+
publicPaths = ['/assets', '/public', '/images', '/styles']
|
|
19
30
|
} = options;
|
|
20
31
|
|
|
21
32
|
return (req, res, next) => {
|
|
@@ -35,9 +46,8 @@ export function securityMiddleware(options = {}) {
|
|
|
35
46
|
});
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
// 2. Permitir acceso a assets públicos
|
|
49
|
+
// 2. Permitir acceso a assets públicos (según sliceConfig.publicFolders)
|
|
39
50
|
if (allowPublicAssets) {
|
|
40
|
-
const publicPaths = ['/assets', '/public', '/images', '/styles'];
|
|
41
51
|
const isPublicAsset = publicPaths.some(publicPath =>
|
|
42
52
|
requestPath.startsWith(publicPath)
|
|
43
53
|
);
|
|
@@ -92,9 +102,7 @@ export function sliceFrameworkProtection(options = {}) {
|
|
|
92
102
|
|
|
93
103
|
// Rutas del framework que requieren verificación
|
|
94
104
|
const frameworkPaths = [
|
|
95
|
-
'/Slice/Components/Structural'
|
|
96
|
-
'/Slice/Core',
|
|
97
|
-
'/Slice/Services'
|
|
105
|
+
'/Slice/Components/Structural'
|
|
98
106
|
];
|
|
99
107
|
|
|
100
108
|
const isFrameworkFile = frameworkPaths.some(fwPath =>
|
|
@@ -182,72 +190,8 @@ export function suspiciousRequestLogger() {
|
|
|
182
190
|
};
|
|
183
191
|
}
|
|
184
192
|
|
|
185
|
-
/**
|
|
186
|
-
* Middleware para bloquear acceso directo vía navegador (typing en la URL)
|
|
187
|
-
* pero permitir peticiones desde scripts (fetch, import, etc.)
|
|
188
|
-
*/
|
|
189
|
-
export function directAccessProtection(options = {}) {
|
|
190
|
-
const { protectedPaths = [] } = options;
|
|
191
|
-
|
|
192
|
-
return (req, res, next) => {
|
|
193
|
-
const requestPath = req.path;
|
|
194
|
-
|
|
195
|
-
const isProtectedPath = protectedPaths.some(protectedPath =>
|
|
196
|
-
requestPath.startsWith(protectedPath)
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
if (!isProtectedPath) {
|
|
200
|
-
return next();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Detectar acceso directo:
|
|
204
|
-
// - No tiene Referer (usuario escribió la URL directamente)
|
|
205
|
-
// - Accept header indica navegación HTML
|
|
206
|
-
const referer = req.get('Referer');
|
|
207
|
-
const accept = req.get('Accept') || '';
|
|
208
|
-
|
|
209
|
-
const isDirectBrowserAccess = !referer && accept.includes('text/html');
|
|
210
|
-
|
|
211
|
-
if (isDirectBrowserAccess) {
|
|
212
|
-
console.warn(`🚫 Blocked direct browser access: ${requestPath}`);
|
|
213
|
-
return res.status(403).send(`
|
|
214
|
-
<!DOCTYPE html>
|
|
215
|
-
<html>
|
|
216
|
-
<head>
|
|
217
|
-
<title>Access Denied</title>
|
|
218
|
-
<style>
|
|
219
|
-
body {
|
|
220
|
-
font-family: system-ui;
|
|
221
|
-
max-width: 600px;
|
|
222
|
-
margin: 100px auto;
|
|
223
|
-
padding: 20px;
|
|
224
|
-
}
|
|
225
|
-
h1 { color: #d32f2f; }
|
|
226
|
-
code {
|
|
227
|
-
background: #f5f5f5;
|
|
228
|
-
padding: 2px 6px;
|
|
229
|
-
border-radius: 3px;
|
|
230
|
-
}
|
|
231
|
-
</style>
|
|
232
|
-
</head>
|
|
233
|
-
<body>
|
|
234
|
-
<h1>🚫 Direct Access Denied</h1>
|
|
235
|
-
<p>This file cannot be accessed directly.</p>
|
|
236
|
-
<p>Path: <code>${requestPath}</code></p>
|
|
237
|
-
<p>Framework files are automatically loaded by the application.</p>
|
|
238
|
-
<p><a href="/">← Return to application</a></p>
|
|
239
|
-
</body>
|
|
240
|
-
</html>
|
|
241
|
-
`);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
next();
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
193
|
export default {
|
|
249
194
|
securityMiddleware,
|
|
250
195
|
sliceFrameworkProtection,
|
|
251
|
-
suspiciousRequestLogger
|
|
252
|
-
|
|
253
|
-
};
|
|
196
|
+
suspiciousRequestLogger
|
|
197
|
+
};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// api/framework/server.js - Servidor Express encapsulado del framework
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import compression from 'compression';
|
|
6
|
+
import {
|
|
7
|
+
securityMiddleware,
|
|
8
|
+
sliceFrameworkProtection,
|
|
9
|
+
suspiciousRequestLogger
|
|
10
|
+
} from './securityMiddleware.js';
|
|
11
|
+
import { createPublicEnvProvider } from './publicEnvResolver.js';
|
|
12
|
+
|
|
13
|
+
export function createSliceServer(options = {}) {
|
|
14
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
15
|
+
const runMode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
|
16
|
+
const folderDeployed = runMode === 'production' ? 'dist' : 'src';
|
|
17
|
+
|
|
18
|
+
let sliceConfig = {};
|
|
19
|
+
try {
|
|
20
|
+
const configPath = path.join(projectRoot, 'src', 'sliceConfig.json');
|
|
21
|
+
if (fs.existsSync(configPath)) {
|
|
22
|
+
sliceConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// usa defaults si no se puede leer la config
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let frameworkVersion = '';
|
|
29
|
+
try {
|
|
30
|
+
const pkgPath = new URL('../../package.json', import.meta.url);
|
|
31
|
+
frameworkVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
|
32
|
+
} catch {
|
|
33
|
+
frameworkVersion = 'unknown';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PORT = process.env.PORT || sliceConfig.server?.port || 3001;
|
|
37
|
+
|
|
38
|
+
const normalizedPublicFolders = (Array.isArray(sliceConfig.publicFolders) ? sliceConfig.publicFolders : [])
|
|
39
|
+
.filter((entry) => typeof entry === 'string')
|
|
40
|
+
.map((entry) => entry.trim())
|
|
41
|
+
.filter((entry) => entry.length > 0)
|
|
42
|
+
.map((entry) => (entry.startsWith('/') ? entry : `/${entry}`));
|
|
43
|
+
|
|
44
|
+
const publicEnvProvider = createPublicEnvProvider({
|
|
45
|
+
mode: runMode,
|
|
46
|
+
envFilePath: path.join(projectRoot, '.env')
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const app = express();
|
|
50
|
+
|
|
51
|
+
// ==============================================
|
|
52
|
+
// MIDDLEWARES DE SEGURIDAD (APLICAR PRIMERO)
|
|
53
|
+
// ==============================================
|
|
54
|
+
|
|
55
|
+
// 1. Logger de peticiones sospechosas (solo observación, no bloquea)
|
|
56
|
+
app.use(suspiciousRequestLogger());
|
|
57
|
+
|
|
58
|
+
// 2. Protección del framework - TOTALMENTE AUTOMÁTICA
|
|
59
|
+
app.use(sliceFrameworkProtection({ port: PORT }));
|
|
60
|
+
|
|
61
|
+
app.use(securityMiddleware({
|
|
62
|
+
publicPaths: normalizedPublicFolders
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// ==============================================
|
|
66
|
+
// MIDDLEWARES DE APLICACIÓN
|
|
67
|
+
// ==============================================
|
|
68
|
+
|
|
69
|
+
// Compresión gzip/brotli para respuestas (threshold 0 = comprimir todo)
|
|
70
|
+
app.use(compression({ threshold: 0 }));
|
|
71
|
+
|
|
72
|
+
// Security headers básicos
|
|
73
|
+
app.use((req, res, next) => {
|
|
74
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
75
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
76
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
77
|
+
next();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Middleware global para archivos JavaScript con MIME types correctos
|
|
81
|
+
app.use((req, res, next) => {
|
|
82
|
+
if (req.path.endsWith('.js') || req.path.endsWith('.mjs') || req.path.endsWith('.cjs')) {
|
|
83
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
84
|
+
}
|
|
85
|
+
next();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Middleware para parsear JSON y formularios
|
|
89
|
+
app.use(express.json());
|
|
90
|
+
app.use(express.urlencoded({ extended: true }));
|
|
91
|
+
|
|
92
|
+
// Configurar headers de CORS
|
|
93
|
+
app.use((req, res, next) => {
|
|
94
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
95
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
96
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
97
|
+
|
|
98
|
+
if (req.method === 'OPTIONS') {
|
|
99
|
+
res.sendStatus(200);
|
|
100
|
+
} else {
|
|
101
|
+
next();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ==============================================
|
|
106
|
+
// RUNTIME MODE ENDPOINT
|
|
107
|
+
// ==============================================
|
|
108
|
+
|
|
109
|
+
app.get('/slice-env.json', (req, res) => {
|
|
110
|
+
const payload = publicEnvProvider.getPayload();
|
|
111
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
112
|
+
res.setHeader('Pragma', 'no-cache');
|
|
113
|
+
res.setHeader('Expires', '0');
|
|
114
|
+
res.json(payload);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ==============================================
|
|
118
|
+
// PWA — manifest + service worker (dev y prod)
|
|
119
|
+
// ==============================================
|
|
120
|
+
|
|
121
|
+
const servePwaFile = (res, fileName, contentType, extraHeaders = {}) => {
|
|
122
|
+
const filePath = path.join(projectRoot, folderDeployed, fileName);
|
|
123
|
+
try {
|
|
124
|
+
if (fs.existsSync(filePath)) {
|
|
125
|
+
res.setHeader('Content-Type', contentType);
|
|
126
|
+
for (const [key, value] of Object.entries(extraHeaders)) res.setHeader(key, value);
|
|
127
|
+
return res.send(fs.readFileSync(filePath, 'utf8'));
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`Error reading ${fileName}:`, error);
|
|
131
|
+
return res.status(500).send(`Error reading ${fileName}`);
|
|
132
|
+
}
|
|
133
|
+
return res.status(404).send(`${fileName} not found`);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
app.get('/service-worker.js', (req, res) => {
|
|
137
|
+
servePwaFile(res, 'service-worker.js', 'application/javascript; charset=utf-8', {
|
|
138
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
139
|
+
'Service-Worker-Allowed': '/',
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
app.get('/manifest.json', (req, res) => {
|
|
144
|
+
servePwaFile(res, 'manifest.json', 'application/manifest+json; charset=utf-8');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ==============================================
|
|
148
|
+
// ARCHIVOS ESTÁTICOS (DESPUÉS DE SEGURIDAD)
|
|
149
|
+
// ==============================================
|
|
150
|
+
|
|
151
|
+
if (runMode === 'production') {
|
|
152
|
+
app.get('/Slice/Slice.js', (req, res) => {
|
|
153
|
+
const slicePath = path.join(projectRoot, 'node_modules', 'slicejs-web-framework', 'Slice', 'Slice.js');
|
|
154
|
+
try {
|
|
155
|
+
if (fs.existsSync(slicePath)) {
|
|
156
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
157
|
+
return res.send(fs.readFileSync(slicePath, 'utf8'));
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(`Error reading Slice.js:`, error);
|
|
161
|
+
return res.status(500).send('Error loading framework');
|
|
162
|
+
}
|
|
163
|
+
return res.status(404).send('Slice.js not found');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
app.use('/Slice', (req, res) => res.status(404).send('Not found'));
|
|
167
|
+
app.use('/Components', (req, res) => res.status(404).send('Not found'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Middleware personalizado para archivos de bundles con MIME types correctos
|
|
171
|
+
app.use('/bundles/', (req, res, next) => {
|
|
172
|
+
if (req.path.endsWith('.js')) {
|
|
173
|
+
const filePath = path.join(projectRoot, folderDeployed, 'bundles', req.path);
|
|
174
|
+
console.log(`📂 Processing bundle: ${req.path} -> ${filePath}`);
|
|
175
|
+
|
|
176
|
+
if (fs.existsSync(filePath)) {
|
|
177
|
+
try {
|
|
178
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
179
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
180
|
+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
181
|
+
res.setHeader('Pragma', 'no-cache');
|
|
182
|
+
res.setHeader('Expires', '0');
|
|
183
|
+
console.log(`✅ Serving bundle: ${req.path} (${fileContent.length} bytes, ${Buffer.byteLength(fileContent, 'utf8')} bytes UTF-8)`);
|
|
184
|
+
return res.send(fileContent);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error(`Error reading bundle file:`, error);
|
|
187
|
+
return res.status(500).send('Error reading bundle file');
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
console.log(`❌ Bundle file not found: ${filePath}`);
|
|
191
|
+
return res.status(404).send('Bundle file not found');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
next();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
app.use('/bundles/', express.static(path.join(projectRoot, folderDeployed, 'bundles')));
|
|
199
|
+
console.log(`📦 Serving bundles from /${folderDeployed}/bundles`);
|
|
200
|
+
|
|
201
|
+
// Servir framework Slice.js (solo development)
|
|
202
|
+
if (runMode === 'development') {
|
|
203
|
+
app.use('/Slice/', express.static(path.join(projectRoot, 'node_modules', 'slicejs-web-framework', 'Slice')));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (runMode === 'development') {
|
|
207
|
+
app.use(express.static(path.join(projectRoot, folderDeployed)));
|
|
208
|
+
} else {
|
|
209
|
+
app.use('/App', express.static(path.join(projectRoot, folderDeployed, 'App')));
|
|
210
|
+
const serveStaticFile = (req, res, filePath, contentType, fileName) => {
|
|
211
|
+
try {
|
|
212
|
+
if (fs.existsSync(filePath)) {
|
|
213
|
+
res.setHeader('Content-Type', contentType);
|
|
214
|
+
return res.send(fs.readFileSync(filePath, 'utf8'));
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error(`Error reading ${fileName}:`, error);
|
|
218
|
+
return res.status(500).send(`Error reading ${fileName}`);
|
|
219
|
+
}
|
|
220
|
+
return res.status(404).send(`${fileName} not found`);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
app.get('/routes.js', (req, res) => {
|
|
224
|
+
const routesPath = path.join(projectRoot, folderDeployed, 'routes.js');
|
|
225
|
+
serveStaticFile(req, res, routesPath, 'application/javascript; charset=utf-8', 'routes.js');
|
|
226
|
+
});
|
|
227
|
+
app.get('/sliceConfig.json', (req, res) => {
|
|
228
|
+
const configPath = path.join(projectRoot, folderDeployed, 'sliceConfig.json');
|
|
229
|
+
serveStaticFile(req, res, configPath, 'application/json; charset=utf-8', 'sliceConfig.json');
|
|
230
|
+
});
|
|
231
|
+
for (const folder of normalizedPublicFolders) {
|
|
232
|
+
app.use(folder, express.static(path.join(projectRoot, folderDeployed, folder)));
|
|
233
|
+
}
|
|
234
|
+
app.use('/bundles/', express.static(path.join(projectRoot, folderDeployed, 'bundles')));
|
|
235
|
+
app.use('/dist/', express.static(path.join(projectRoot, 'dist')));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ==============================================
|
|
239
|
+
// RUTAS DE API
|
|
240
|
+
// ==============================================
|
|
241
|
+
|
|
242
|
+
// Ruta de ejemplo para API
|
|
243
|
+
app.get('/api/status', (req, res) => {
|
|
244
|
+
res.json({
|
|
245
|
+
status: 'ok',
|
|
246
|
+
mode: runMode,
|
|
247
|
+
folder: folderDeployed,
|
|
248
|
+
timestamp: new Date().toISOString(),
|
|
249
|
+
framework: 'Slice.js',
|
|
250
|
+
version: frameworkVersion,
|
|
251
|
+
security: {
|
|
252
|
+
enabled: true,
|
|
253
|
+
mode: 'automatic',
|
|
254
|
+
description: 'Zero-config security - works with any domain'
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ==============================================
|
|
260
|
+
// SEO: robots.txt & sitemap.xml
|
|
261
|
+
// ==============================================
|
|
262
|
+
|
|
263
|
+
app.get('/robots.txt', (req, res) => {
|
|
264
|
+
res.sendFile(path.join(projectRoot, folderDeployed, 'robots.txt'));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
app.get('/sitemap.xml', (req, res) => {
|
|
268
|
+
res.sendFile(path.join(projectRoot, folderDeployed, 'sitemap.xml'));
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ==============================================
|
|
272
|
+
// API 404 — las rutas /api/* irreconocibles devuelven JSON, no HTML
|
|
273
|
+
// ==============================================
|
|
274
|
+
|
|
275
|
+
app.use('/api', (req, res) => {
|
|
276
|
+
res.status(404).json({ error: 'API route not found', path: req.originalUrl });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ==============================================
|
|
280
|
+
// SPA FALLBACK
|
|
281
|
+
// ==============================================
|
|
282
|
+
|
|
283
|
+
app.get('*', (req, res) => {
|
|
284
|
+
const indexPath = path.join(projectRoot, folderDeployed, 'App', 'index.html');
|
|
285
|
+
res.sendFile(indexPath, (err) => {
|
|
286
|
+
if (err) {
|
|
287
|
+
console.error(`[SPA Fallback] Error sending ${indexPath}:`, err);
|
|
288
|
+
res.status(500).send(`<h1>500 - Internal Server Error</h1><p>Could not serve application file</p>`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// ==============================================
|
|
294
|
+
// ERROR HANDLER CENTRALIZADO
|
|
295
|
+
// ==============================================
|
|
296
|
+
|
|
297
|
+
app.use((err, req, res, next) => {
|
|
298
|
+
console.error('[Server Error]', err.stack || err.message || err);
|
|
299
|
+
if (res.headersSent) return next(err);
|
|
300
|
+
res.status(err.status || 500).json({
|
|
301
|
+
error: 'Internal Server Error',
|
|
302
|
+
message: process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : err.message
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ==============================================
|
|
307
|
+
// INICIO DEL SERVIDOR
|
|
308
|
+
// ==============================================
|
|
309
|
+
|
|
310
|
+
function startServer() {
|
|
311
|
+
const server = app.listen(PORT, () => {
|
|
312
|
+
console.log(`🔒 Security middleware: active (zero-config, automatic)`);
|
|
313
|
+
console.log(`🚀 Slice.js server running on port ${PORT}`);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
process.on('SIGINT', () => {
|
|
317
|
+
console.log('\n🛑 Slice server stopped');
|
|
318
|
+
process.exit(0);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
process.on('SIGTERM', () => {
|
|
322
|
+
console.log('\n🛑 Server terminated');
|
|
323
|
+
process.exit(0);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return server;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
app,
|
|
331
|
+
start: startServer
|
|
332
|
+
};
|
|
333
|
+
}
|