slicejs-web-framework 4.0.1 → 4.0.2
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/.opencode/opencode.json +14 -14
- package/LICENSE +21 -21
- package/README.md +97 -97
- package/Slice/Components/Structural/ContextManager/ContextManager.js +422 -422
- package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +470 -470
- package/Slice/Components/Structural/Controller/Controller.js +1406 -1406
- package/Slice/Components/Structural/Controller/allowedValuesValidation.js +52 -52
- package/Slice/Components/Structural/Debugger/Debugger.html +72 -72
- package/Slice/Components/Structural/Debugger/Debugger.js +1505 -1505
- package/Slice/Components/Structural/EventManager/EventManager.js +651 -651
- package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +985 -985
- package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -296
- package/Slice/Components/Structural/Logger/Log.js +10 -10
- package/Slice/Components/Structural/Logger/LogViewer/LogViewer.js +662 -662
- package/Slice/Components/Structural/Logger/Logger.js +225 -225
- package/Slice/Components/Structural/Router/Router.js +810 -793
- package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
- package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +175 -162
- package/Slice/Slice.js +708 -685
- package/Slice/tests/build-bundled-component-without-category.test.js +103 -103
- package/Slice/tests/build-js-only-visual-components.test.js +144 -144
- package/Slice/tests/build-singleton.test.js +244 -244
- package/Slice/tests/bundle-v2-runtime-contract.test.js +747 -738
- package/Slice/tests/context-patch-use.test.js +95 -95
- package/Slice/tests/destroy-cascade.test.js +215 -215
- package/Slice/tests/event-emitters.test.js +79 -79
- package/Slice/tests/event-registry.test.js +141 -141
- package/Slice/tests/fixtures/real-runtime-loader.mjs +21 -21
- package/Slice/tests/leak-orphans.test.js +131 -131
- package/Slice/tests/props-allowed-values-validation.test.js +119 -119
- package/Slice/tests/public-env-runtime-accessors.test.js +44 -44
- package/Slice/tests/public-env-typed-accessors.test.js +66 -66
- package/Slice/tests/router-loading-finally.test.js +68 -68
- package/api/framework/devDepsOptimizer.js +335 -335
- package/api/framework/express.js +3 -3
- package/api/framework/precompressedStatic.js +93 -93
- package/api/framework/publicEnvResolver.js +123 -123
- package/api/framework/securityMiddleware.js +197 -197
- package/api/framework/server.js +411 -411
- package/api/index.js +7 -7
- package/api/tests/dev-deps-optimizer.test.js +137 -137
- package/api/tests/dev-server-external.test.js +56 -56
- package/api/tests/precompressed-static.test.js +119 -119
- package/api/tests/public-env-resolver.test.js +193 -193
- package/package.json +49 -49
- package/sliceConfig.schema.json +122 -122
- package/src/App/index.html +69 -69
- package/src/App/index.js +20 -20
- package/src/App/style.css +11 -11
- package/src/Components/AppComponents/AboutSection/AboutSection.css +9 -9
- package/src/Components/AppComponents/AboutSection/AboutSection.html +8 -8
- package/src/Components/AppComponents/AboutSection/AboutSection.js +12 -12
- package/src/Components/AppComponents/AppShell/AppShell.css +10 -10
- package/src/Components/AppComponents/AppShell/AppShell.html +4 -4
- package/src/Components/AppComponents/AppShell/AppShell.js +36 -36
- package/src/Components/AppComponents/HomeSection/HomeSection.css +20 -20
- package/src/Components/AppComponents/HomeSection/HomeSection.html +10 -10
- package/src/Components/AppComponents/HomeSection/HomeSection.js +19 -19
- package/src/Components/components.js +8 -8
- package/src/public/Styles/sliceStyles.css +34 -34
- package/src/public/Themes/Dark.css +42 -42
- package/src/public/Themes/Light.css +31 -31
- package/src/public/Themes/Slice.css +47 -47
- package/src/public/manifest.json +15 -15
- package/src/public/robots.txt +4 -4
- package/src/public/sitemap.xml +8 -8
- package/src/routes.js +9 -9
- package/src/sliceConfig.json +72 -72
- package/tests/router-build-failure.test.js +125 -0
- package/tests/router-emit-ordering.test.js +63 -63
- package/types/index.d.ts +219 -219
package/api/framework/server.js
CHANGED
|
@@ -1,411 +1,411 @@
|
|
|
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
|
-
import { createDevDepsOptimizer } from './devDepsOptimizer.js';
|
|
13
|
-
import { createPrecompressedStatic } from './precompressedStatic.js';
|
|
14
|
-
|
|
15
|
-
export function createSliceServer(options = {}) {
|
|
16
|
-
const projectRoot = options.projectRoot || process.cwd();
|
|
17
|
-
const runMode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
|
18
|
-
const folderDeployed = runMode === 'production' ? 'dist' : 'src';
|
|
19
|
-
|
|
20
|
-
let sliceConfig = {};
|
|
21
|
-
try {
|
|
22
|
-
const configPath = path.join(projectRoot, 'src', 'sliceConfig.json');
|
|
23
|
-
if (fs.existsSync(configPath)) {
|
|
24
|
-
sliceConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
25
|
-
}
|
|
26
|
-
} catch {
|
|
27
|
-
// usa defaults si no se puede leer la config
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let frameworkVersion = '';
|
|
31
|
-
try {
|
|
32
|
-
const pkgPath = new URL('../../package.json', import.meta.url);
|
|
33
|
-
frameworkVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
|
34
|
-
} catch {
|
|
35
|
-
frameworkVersion = 'unknown';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const PORT = process.env.PORT || sliceConfig.server?.port || 3001;
|
|
39
|
-
|
|
40
|
-
const publicEnvProvider = createPublicEnvProvider({
|
|
41
|
-
mode: runMode,
|
|
42
|
-
envFilePath: path.join(projectRoot, '.env')
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const app = express();
|
|
46
|
-
|
|
47
|
-
// ==============================================
|
|
48
|
-
// MIDDLEWARES DE SEGURIDAD (APLICAR PRIMERO)
|
|
49
|
-
// ==============================================
|
|
50
|
-
|
|
51
|
-
// 1. Logger de peticiones sospechosas (solo observación, no bloquea)
|
|
52
|
-
app.use(suspiciousRequestLogger());
|
|
53
|
-
|
|
54
|
-
// 2. Protección del framework - TOTALMENTE AUTOMÁTICA
|
|
55
|
-
app.use(sliceFrameworkProtection({ port: PORT }));
|
|
56
|
-
|
|
57
|
-
app.use(securityMiddleware());
|
|
58
|
-
|
|
59
|
-
// ==============================================
|
|
60
|
-
// MIDDLEWARES DE APLICACIÓN
|
|
61
|
-
// ==============================================
|
|
62
|
-
|
|
63
|
-
// Compresión gzip/brotli para respuestas (threshold 0 = comprimir todo)
|
|
64
|
-
app.use(compression({ threshold: 0 }));
|
|
65
|
-
|
|
66
|
-
// Security headers básicos
|
|
67
|
-
app.use((req, res, next) => {
|
|
68
|
-
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
69
|
-
res.setHeader('X-Frame-Options', 'DENY');
|
|
70
|
-
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
71
|
-
next();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Middleware global para archivos JavaScript con MIME types correctos
|
|
75
|
-
app.use((req, res, next) => {
|
|
76
|
-
if (req.path.endsWith('.js') || req.path.endsWith('.mjs') || req.path.endsWith('.cjs')) {
|
|
77
|
-
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
78
|
-
}
|
|
79
|
-
next();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Middleware para parsear JSON y formularios
|
|
83
|
-
app.use(express.json());
|
|
84
|
-
app.use(express.urlencoded({ extended: true }));
|
|
85
|
-
|
|
86
|
-
// Configurar headers de CORS
|
|
87
|
-
app.use((req, res, next) => {
|
|
88
|
-
res.header('Access-Control-Allow-Origin', '*');
|
|
89
|
-
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
90
|
-
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
91
|
-
|
|
92
|
-
if (req.method === 'OPTIONS') {
|
|
93
|
-
res.sendStatus(200);
|
|
94
|
-
} else {
|
|
95
|
-
next();
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// ==============================================
|
|
100
|
-
// RUNTIME MODE ENDPOINT
|
|
101
|
-
// ==============================================
|
|
102
|
-
|
|
103
|
-
app.get('/slice-env.json', (req, res) => {
|
|
104
|
-
const payload = publicEnvProvider.getPayload();
|
|
105
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
106
|
-
res.setHeader('Pragma', 'no-cache');
|
|
107
|
-
res.setHeader('Expires', '0');
|
|
108
|
-
res.json(payload);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ==============================================
|
|
112
|
-
// PWA — manifest + service worker (dev y prod)
|
|
113
|
-
// ==============================================
|
|
114
|
-
|
|
115
|
-
// Resolve a deployed asset preferring the public/ folder (the convention),
|
|
116
|
-
// falling back to the deploy root for pre-`public/` projects.
|
|
117
|
-
const resolveDeployedFile = (fileName) => {
|
|
118
|
-
const inPublic = path.join(projectRoot, folderDeployed, 'public', fileName);
|
|
119
|
-
return fs.existsSync(inPublic) ? inPublic : path.join(projectRoot, folderDeployed, fileName);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const servePwaFile = (res, fileName, contentType, extraHeaders = {}) => {
|
|
123
|
-
const filePath = resolveDeployedFile(fileName);
|
|
124
|
-
try {
|
|
125
|
-
if (fs.existsSync(filePath)) {
|
|
126
|
-
res.setHeader('Content-Type', contentType);
|
|
127
|
-
for (const [key, value] of Object.entries(extraHeaders)) res.setHeader(key, value);
|
|
128
|
-
return res.send(fs.readFileSync(filePath, 'utf8'));
|
|
129
|
-
}
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error(`Error reading ${fileName}:`, error);
|
|
132
|
-
return res.status(500).send(`Error reading ${fileName}`);
|
|
133
|
-
}
|
|
134
|
-
return res.status(404).send(`${fileName} not found`);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
app.get('/service-worker.js', (req, res) => {
|
|
138
|
-
servePwaFile(res, 'service-worker.js', 'application/javascript; charset=utf-8', {
|
|
139
|
-
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
140
|
-
'Service-Worker-Allowed': '/',
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
app.get('/manifest.json', (req, res) => {
|
|
145
|
-
servePwaFile(res, 'manifest.json', 'application/manifest+json; charset=utf-8');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// ==============================================
|
|
149
|
-
// ARCHIVOS ESTÁTICOS (DESPUÉS DE SEGURIDAD)
|
|
150
|
-
// ==============================================
|
|
151
|
-
|
|
152
|
-
if (runMode === 'production') {
|
|
153
|
-
app.get('/Slice/Slice.js', (req, res) => {
|
|
154
|
-
// Prefer the self-contained copy emitted into dist by `slice build`: it is
|
|
155
|
-
// the only location guaranteed to ship inside a serverless function (Vercel
|
|
156
|
-
// bundles dist/** via includeFiles, but prunes node_modules that aren't
|
|
157
|
-
// statically imported — and under pnpm the package sits behind a symlink).
|
|
158
|
-
// Fall back to node_modules for non-serverless production (`slice start`).
|
|
159
|
-
const distSlice = path.join(projectRoot, folderDeployed, 'Slice', 'Slice.js');
|
|
160
|
-
const nodeModulesSlice = path.join(projectRoot, 'node_modules', 'slicejs-web-framework', 'Slice', 'Slice.js');
|
|
161
|
-
const slicePath = fs.existsSync(distSlice) ? distSlice : nodeModulesSlice;
|
|
162
|
-
try {
|
|
163
|
-
if (fs.existsSync(slicePath)) {
|
|
164
|
-
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
165
|
-
return res.send(fs.readFileSync(slicePath, 'utf8'));
|
|
166
|
-
}
|
|
167
|
-
} catch (error) {
|
|
168
|
-
console.error(`Error reading Slice.js:`, error);
|
|
169
|
-
return res.status(500).send('Error loading framework');
|
|
170
|
-
}
|
|
171
|
-
return res.status(404).send('Slice.js not found');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
app.use('/Slice', (req, res) => res.status(404).send('Not found'));
|
|
175
|
-
app.use('/Components', (req, res) => res.status(404).send('Not found'));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Serve precompressed .br/.gz siblings (from `slice build --compress`) when the
|
|
179
|
-
// client accepts them. Mounted before the bundle handler and static serves so
|
|
180
|
-
// it wins, and its Content-Encoding makes the runtime `compression` middleware
|
|
181
|
-
// skip re-compressing. Production only (dist holds the precompressed files).
|
|
182
|
-
if (runMode === 'production') {
|
|
183
|
-
const distRoot = path.join(projectRoot, folderDeployed);
|
|
184
|
-
app.use(createPrecompressedStatic([path.join(distRoot, 'public'), distRoot]));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Middleware personalizado para archivos de bundles con MIME types correctos
|
|
188
|
-
app.use('/bundles/', (req, res, next) => {
|
|
189
|
-
if (req.path.endsWith('.js')) {
|
|
190
|
-
const filePath = path.join(projectRoot, folderDeployed, 'bundles', req.path);
|
|
191
|
-
console.log(`📂 Processing bundle: ${req.path} -> ${filePath}`);
|
|
192
|
-
|
|
193
|
-
if (fs.existsSync(filePath)) {
|
|
194
|
-
try {
|
|
195
|
-
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
196
|
-
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
197
|
-
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
198
|
-
res.setHeader('Pragma', 'no-cache');
|
|
199
|
-
res.setHeader('Expires', '0');
|
|
200
|
-
console.log(`✅ Serving bundle: ${req.path} (${fileContent.length} bytes, ${Buffer.byteLength(fileContent, 'utf8')} bytes UTF-8)`);
|
|
201
|
-
return res.send(fileContent);
|
|
202
|
-
} catch (error) {
|
|
203
|
-
console.error(`Error reading bundle file:`, error);
|
|
204
|
-
return res.status(500).send('Error reading bundle file');
|
|
205
|
-
}
|
|
206
|
-
} else {
|
|
207
|
-
console.log(`❌ Bundle file not found: ${filePath}`);
|
|
208
|
-
return res.status(404).send('Bundle file not found');
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
next();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
app.use('/bundles/', express.static(path.join(projectRoot, folderDeployed, 'bundles')));
|
|
216
|
-
console.log(`📦 Serving bundles from /${folderDeployed}/bundles`);
|
|
217
|
-
|
|
218
|
-
// Servir framework Slice.js (solo development)
|
|
219
|
-
if (runMode === 'development') {
|
|
220
|
-
app.use('/Slice/', express.static(path.join(projectRoot, 'node_modules', 'slicejs-web-framework', 'Slice')));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (runMode === 'development') {
|
|
224
|
-
// External (node_modules) dependency support in dev — always on, no app
|
|
225
|
-
// bundling. Rewrites bare imports in served src modules to /@slice-modules/…
|
|
226
|
-
// and serves each package pre-bundled on demand with esbuild (same resolver
|
|
227
|
-
// as the production build → dev/prod parity for CommonJS and ESM packages).
|
|
228
|
-
const devDeps = createDevDepsOptimizer({ projectRoot });
|
|
229
|
-
|
|
230
|
-
if (devDeps.enabled) {
|
|
231
|
-
const srcRoot = path.join(projectRoot, folderDeployed);
|
|
232
|
-
|
|
233
|
-
// Serve a node_modules package pre-bundled as a single ESM module.
|
|
234
|
-
app.get(/^\/@slice-modules\/(.+)$/, async (req, res) => {
|
|
235
|
-
const spec = req.params[0];
|
|
236
|
-
try {
|
|
237
|
-
const { code } = await devDeps.bundlePackage(spec);
|
|
238
|
-
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
239
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
240
|
-
return res.send(code);
|
|
241
|
-
} catch (error) {
|
|
242
|
-
const message = `Failed to load external dependency "${spec}": ${error.message}`;
|
|
243
|
-
console.error(`[slice:dev] ${message}`);
|
|
244
|
-
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
245
|
-
return res
|
|
246
|
-
.status(502)
|
|
247
|
-
.send(`throw new Error(${JSON.stringify(message)});`);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
// Rewrite bare imports in served src JavaScript to the virtual endpoint.
|
|
252
|
-
app.use(async (req, res, next) => {
|
|
253
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
|
|
254
|
-
const reqPath = req.path;
|
|
255
|
-
if (!(reqPath.endsWith('.js') || reqPath.endsWith('.mjs'))) return next();
|
|
256
|
-
// Framework internals, generated bundles and the virtual endpoint are
|
|
257
|
-
// served elsewhere and must not be rewritten.
|
|
258
|
-
if (reqPath.startsWith('/@slice-modules/') || reqPath.startsWith('/bundles/') || reqPath.startsWith('/Slice/')) {
|
|
259
|
-
return next();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const filePath = path.join(srcRoot, decodeURIComponent(reqPath));
|
|
263
|
-
const normalized = path.normalize(filePath);
|
|
264
|
-
if (normalized !== srcRoot && !normalized.startsWith(srcRoot + path.sep)) {
|
|
265
|
-
return next(); // path traversal guard
|
|
266
|
-
}
|
|
267
|
-
if (!fs.existsSync(normalized) || !fs.statSync(normalized).isFile()) {
|
|
268
|
-
return next();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
const original = fs.readFileSync(normalized, 'utf8');
|
|
273
|
-
const rewritten = await devDeps.rewriteBareImports(original);
|
|
274
|
-
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
275
|
-
return res.send(rewritten);
|
|
276
|
-
} catch (error) {
|
|
277
|
-
console.error(`[slice:dev] Failed to rewrite ${reqPath}: ${error.message}`);
|
|
278
|
-
return next();
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Centralized public/ folder served at the root URL (Themes, Styles,
|
|
284
|
-
// images…). Mounted before the general src static so its files win.
|
|
285
|
-
app.use(express.static(path.join(projectRoot, folderDeployed, 'public')));
|
|
286
|
-
app.use(express.static(path.join(projectRoot, folderDeployed)));
|
|
287
|
-
} else {
|
|
288
|
-
// Serve the built public/ assets at the root URL.
|
|
289
|
-
app.use(express.static(path.join(projectRoot, folderDeployed, 'public')));
|
|
290
|
-
app.use('/App', express.static(path.join(projectRoot, folderDeployed, 'App')));
|
|
291
|
-
const serveStaticFile = (req, res, filePath, contentType, fileName) => {
|
|
292
|
-
try {
|
|
293
|
-
if (fs.existsSync(filePath)) {
|
|
294
|
-
res.setHeader('Content-Type', contentType);
|
|
295
|
-
return res.send(fs.readFileSync(filePath, 'utf8'));
|
|
296
|
-
}
|
|
297
|
-
} catch (error) {
|
|
298
|
-
console.error(`Error reading ${fileName}:`, error);
|
|
299
|
-
return res.status(500).send(`Error reading ${fileName}`);
|
|
300
|
-
}
|
|
301
|
-
return res.status(404).send(`${fileName} not found`);
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
app.get('/routes.js', (req, res) => {
|
|
305
|
-
const routesPath = path.join(projectRoot, folderDeployed, 'routes.js');
|
|
306
|
-
serveStaticFile(req, res, routesPath, 'application/javascript; charset=utf-8', 'routes.js');
|
|
307
|
-
});
|
|
308
|
-
app.get('/sliceConfig.json', (req, res) => {
|
|
309
|
-
const configPath = path.join(projectRoot, folderDeployed, 'sliceConfig.json');
|
|
310
|
-
serveStaticFile(req, res, configPath, 'application/json; charset=utf-8', 'sliceConfig.json');
|
|
311
|
-
});
|
|
312
|
-
app.use('/bundles/', express.static(path.join(projectRoot, folderDeployed, 'bundles')));
|
|
313
|
-
app.use('/dist/', express.static(path.join(projectRoot, 'dist')));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// ==============================================
|
|
317
|
-
// RUTAS DE API
|
|
318
|
-
// ==============================================
|
|
319
|
-
|
|
320
|
-
// Ruta de ejemplo para API
|
|
321
|
-
app.get('/api/status', (req, res) => {
|
|
322
|
-
res.json({
|
|
323
|
-
status: 'ok',
|
|
324
|
-
mode: runMode,
|
|
325
|
-
folder: folderDeployed,
|
|
326
|
-
timestamp: new Date().toISOString(),
|
|
327
|
-
framework: 'Slice.js',
|
|
328
|
-
version: frameworkVersion,
|
|
329
|
-
security: {
|
|
330
|
-
enabled: true,
|
|
331
|
-
mode: 'automatic',
|
|
332
|
-
description: 'Zero-config security - works with any domain'
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// ==============================================
|
|
338
|
-
// SEO: robots.txt & sitemap.xml
|
|
339
|
-
// ==============================================
|
|
340
|
-
|
|
341
|
-
app.get('/robots.txt', (req, res) => {
|
|
342
|
-
res.sendFile(resolveDeployedFile('robots.txt'));
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
app.get('/sitemap.xml', (req, res) => {
|
|
346
|
-
res.sendFile(resolveDeployedFile('sitemap.xml'));
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
// ==============================================
|
|
350
|
-
// API 404 — las rutas /api/* irreconocibles devuelven JSON, no HTML
|
|
351
|
-
// ==============================================
|
|
352
|
-
|
|
353
|
-
app.use('/api', (req, res) => {
|
|
354
|
-
res.status(404).json({ error: 'API route not found', path: req.originalUrl });
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// ==============================================
|
|
358
|
-
// SPA FALLBACK
|
|
359
|
-
// ==============================================
|
|
360
|
-
|
|
361
|
-
app.get('*', (req, res) => {
|
|
362
|
-
const indexPath = path.join(projectRoot, folderDeployed, 'App', 'index.html');
|
|
363
|
-
res.sendFile(indexPath, (err) => {
|
|
364
|
-
if (err) {
|
|
365
|
-
console.error(`[SPA Fallback] Error sending ${indexPath}:`, err);
|
|
366
|
-
res.status(500).send(`<h1>500 - Internal Server Error</h1><p>Could not serve application file</p>`);
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// ==============================================
|
|
372
|
-
// ERROR HANDLER CENTRALIZADO
|
|
373
|
-
// ==============================================
|
|
374
|
-
|
|
375
|
-
app.use((err, req, res, next) => {
|
|
376
|
-
console.error('[Server Error]', err.stack || err.message || err);
|
|
377
|
-
if (res.headersSent) return next(err);
|
|
378
|
-
res.status(err.status || 500).json({
|
|
379
|
-
error: 'Internal Server Error',
|
|
380
|
-
message: process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : err.message
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// ==============================================
|
|
385
|
-
// INICIO DEL SERVIDOR
|
|
386
|
-
// ==============================================
|
|
387
|
-
|
|
388
|
-
function startServer() {
|
|
389
|
-
const server = app.listen(PORT, () => {
|
|
390
|
-
console.log(`🔒 Security middleware: active (zero-config, automatic)`);
|
|
391
|
-
console.log(`🚀 Slice.js server running on port ${PORT}`);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
process.on('SIGINT', () => {
|
|
395
|
-
console.log('\n🛑 Slice server stopped');
|
|
396
|
-
process.exit(0);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
process.on('SIGTERM', () => {
|
|
400
|
-
console.log('\n🛑 Server terminated');
|
|
401
|
-
process.exit(0);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
return server;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return {
|
|
408
|
-
app,
|
|
409
|
-
start: startServer
|
|
410
|
-
};
|
|
411
|
-
}
|
|
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
|
+
import { createDevDepsOptimizer } from './devDepsOptimizer.js';
|
|
13
|
+
import { createPrecompressedStatic } from './precompressedStatic.js';
|
|
14
|
+
|
|
15
|
+
export function createSliceServer(options = {}) {
|
|
16
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
17
|
+
const runMode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
|
18
|
+
const folderDeployed = runMode === 'production' ? 'dist' : 'src';
|
|
19
|
+
|
|
20
|
+
let sliceConfig = {};
|
|
21
|
+
try {
|
|
22
|
+
const configPath = path.join(projectRoot, 'src', 'sliceConfig.json');
|
|
23
|
+
if (fs.existsSync(configPath)) {
|
|
24
|
+
sliceConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// usa defaults si no se puede leer la config
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let frameworkVersion = '';
|
|
31
|
+
try {
|
|
32
|
+
const pkgPath = new URL('../../package.json', import.meta.url);
|
|
33
|
+
frameworkVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
|
34
|
+
} catch {
|
|
35
|
+
frameworkVersion = 'unknown';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PORT = process.env.PORT || sliceConfig.server?.port || 3001;
|
|
39
|
+
|
|
40
|
+
const publicEnvProvider = createPublicEnvProvider({
|
|
41
|
+
mode: runMode,
|
|
42
|
+
envFilePath: path.join(projectRoot, '.env')
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const app = express();
|
|
46
|
+
|
|
47
|
+
// ==============================================
|
|
48
|
+
// MIDDLEWARES DE SEGURIDAD (APLICAR PRIMERO)
|
|
49
|
+
// ==============================================
|
|
50
|
+
|
|
51
|
+
// 1. Logger de peticiones sospechosas (solo observación, no bloquea)
|
|
52
|
+
app.use(suspiciousRequestLogger());
|
|
53
|
+
|
|
54
|
+
// 2. Protección del framework - TOTALMENTE AUTOMÁTICA
|
|
55
|
+
app.use(sliceFrameworkProtection({ port: PORT }));
|
|
56
|
+
|
|
57
|
+
app.use(securityMiddleware());
|
|
58
|
+
|
|
59
|
+
// ==============================================
|
|
60
|
+
// MIDDLEWARES DE APLICACIÓN
|
|
61
|
+
// ==============================================
|
|
62
|
+
|
|
63
|
+
// Compresión gzip/brotli para respuestas (threshold 0 = comprimir todo)
|
|
64
|
+
app.use(compression({ threshold: 0 }));
|
|
65
|
+
|
|
66
|
+
// Security headers básicos
|
|
67
|
+
app.use((req, res, next) => {
|
|
68
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
69
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
70
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
71
|
+
next();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Middleware global para archivos JavaScript con MIME types correctos
|
|
75
|
+
app.use((req, res, next) => {
|
|
76
|
+
if (req.path.endsWith('.js') || req.path.endsWith('.mjs') || req.path.endsWith('.cjs')) {
|
|
77
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
78
|
+
}
|
|
79
|
+
next();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Middleware para parsear JSON y formularios
|
|
83
|
+
app.use(express.json());
|
|
84
|
+
app.use(express.urlencoded({ extended: true }));
|
|
85
|
+
|
|
86
|
+
// Configurar headers de CORS
|
|
87
|
+
app.use((req, res, next) => {
|
|
88
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
89
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
90
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
91
|
+
|
|
92
|
+
if (req.method === 'OPTIONS') {
|
|
93
|
+
res.sendStatus(200);
|
|
94
|
+
} else {
|
|
95
|
+
next();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ==============================================
|
|
100
|
+
// RUNTIME MODE ENDPOINT
|
|
101
|
+
// ==============================================
|
|
102
|
+
|
|
103
|
+
app.get('/slice-env.json', (req, res) => {
|
|
104
|
+
const payload = publicEnvProvider.getPayload();
|
|
105
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
106
|
+
res.setHeader('Pragma', 'no-cache');
|
|
107
|
+
res.setHeader('Expires', '0');
|
|
108
|
+
res.json(payload);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ==============================================
|
|
112
|
+
// PWA — manifest + service worker (dev y prod)
|
|
113
|
+
// ==============================================
|
|
114
|
+
|
|
115
|
+
// Resolve a deployed asset preferring the public/ folder (the convention),
|
|
116
|
+
// falling back to the deploy root for pre-`public/` projects.
|
|
117
|
+
const resolveDeployedFile = (fileName) => {
|
|
118
|
+
const inPublic = path.join(projectRoot, folderDeployed, 'public', fileName);
|
|
119
|
+
return fs.existsSync(inPublic) ? inPublic : path.join(projectRoot, folderDeployed, fileName);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const servePwaFile = (res, fileName, contentType, extraHeaders = {}) => {
|
|
123
|
+
const filePath = resolveDeployedFile(fileName);
|
|
124
|
+
try {
|
|
125
|
+
if (fs.existsSync(filePath)) {
|
|
126
|
+
res.setHeader('Content-Type', contentType);
|
|
127
|
+
for (const [key, value] of Object.entries(extraHeaders)) res.setHeader(key, value);
|
|
128
|
+
return res.send(fs.readFileSync(filePath, 'utf8'));
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error(`Error reading ${fileName}:`, error);
|
|
132
|
+
return res.status(500).send(`Error reading ${fileName}`);
|
|
133
|
+
}
|
|
134
|
+
return res.status(404).send(`${fileName} not found`);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
app.get('/service-worker.js', (req, res) => {
|
|
138
|
+
servePwaFile(res, 'service-worker.js', 'application/javascript; charset=utf-8', {
|
|
139
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
140
|
+
'Service-Worker-Allowed': '/',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
app.get('/manifest.json', (req, res) => {
|
|
145
|
+
servePwaFile(res, 'manifest.json', 'application/manifest+json; charset=utf-8');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ==============================================
|
|
149
|
+
// ARCHIVOS ESTÁTICOS (DESPUÉS DE SEGURIDAD)
|
|
150
|
+
// ==============================================
|
|
151
|
+
|
|
152
|
+
if (runMode === 'production') {
|
|
153
|
+
app.get('/Slice/Slice.js', (req, res) => {
|
|
154
|
+
// Prefer the self-contained copy emitted into dist by `slice build`: it is
|
|
155
|
+
// the only location guaranteed to ship inside a serverless function (Vercel
|
|
156
|
+
// bundles dist/** via includeFiles, but prunes node_modules that aren't
|
|
157
|
+
// statically imported — and under pnpm the package sits behind a symlink).
|
|
158
|
+
// Fall back to node_modules for non-serverless production (`slice start`).
|
|
159
|
+
const distSlice = path.join(projectRoot, folderDeployed, 'Slice', 'Slice.js');
|
|
160
|
+
const nodeModulesSlice = path.join(projectRoot, 'node_modules', 'slicejs-web-framework', 'Slice', 'Slice.js');
|
|
161
|
+
const slicePath = fs.existsSync(distSlice) ? distSlice : nodeModulesSlice;
|
|
162
|
+
try {
|
|
163
|
+
if (fs.existsSync(slicePath)) {
|
|
164
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
165
|
+
return res.send(fs.readFileSync(slicePath, 'utf8'));
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(`Error reading Slice.js:`, error);
|
|
169
|
+
return res.status(500).send('Error loading framework');
|
|
170
|
+
}
|
|
171
|
+
return res.status(404).send('Slice.js not found');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
app.use('/Slice', (req, res) => res.status(404).send('Not found'));
|
|
175
|
+
app.use('/Components', (req, res) => res.status(404).send('Not found'));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Serve precompressed .br/.gz siblings (from `slice build --compress`) when the
|
|
179
|
+
// client accepts them. Mounted before the bundle handler and static serves so
|
|
180
|
+
// it wins, and its Content-Encoding makes the runtime `compression` middleware
|
|
181
|
+
// skip re-compressing. Production only (dist holds the precompressed files).
|
|
182
|
+
if (runMode === 'production') {
|
|
183
|
+
const distRoot = path.join(projectRoot, folderDeployed);
|
|
184
|
+
app.use(createPrecompressedStatic([path.join(distRoot, 'public'), distRoot]));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Middleware personalizado para archivos de bundles con MIME types correctos
|
|
188
|
+
app.use('/bundles/', (req, res, next) => {
|
|
189
|
+
if (req.path.endsWith('.js')) {
|
|
190
|
+
const filePath = path.join(projectRoot, folderDeployed, 'bundles', req.path);
|
|
191
|
+
console.log(`📂 Processing bundle: ${req.path} -> ${filePath}`);
|
|
192
|
+
|
|
193
|
+
if (fs.existsSync(filePath)) {
|
|
194
|
+
try {
|
|
195
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
196
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
197
|
+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
198
|
+
res.setHeader('Pragma', 'no-cache');
|
|
199
|
+
res.setHeader('Expires', '0');
|
|
200
|
+
console.log(`✅ Serving bundle: ${req.path} (${fileContent.length} bytes, ${Buffer.byteLength(fileContent, 'utf8')} bytes UTF-8)`);
|
|
201
|
+
return res.send(fileContent);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error(`Error reading bundle file:`, error);
|
|
204
|
+
return res.status(500).send('Error reading bundle file');
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
console.log(`❌ Bundle file not found: ${filePath}`);
|
|
208
|
+
return res.status(404).send('Bundle file not found');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
next();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
app.use('/bundles/', express.static(path.join(projectRoot, folderDeployed, 'bundles')));
|
|
216
|
+
console.log(`📦 Serving bundles from /${folderDeployed}/bundles`);
|
|
217
|
+
|
|
218
|
+
// Servir framework Slice.js (solo development)
|
|
219
|
+
if (runMode === 'development') {
|
|
220
|
+
app.use('/Slice/', express.static(path.join(projectRoot, 'node_modules', 'slicejs-web-framework', 'Slice')));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (runMode === 'development') {
|
|
224
|
+
// External (node_modules) dependency support in dev — always on, no app
|
|
225
|
+
// bundling. Rewrites bare imports in served src modules to /@slice-modules/…
|
|
226
|
+
// and serves each package pre-bundled on demand with esbuild (same resolver
|
|
227
|
+
// as the production build → dev/prod parity for CommonJS and ESM packages).
|
|
228
|
+
const devDeps = createDevDepsOptimizer({ projectRoot });
|
|
229
|
+
|
|
230
|
+
if (devDeps.enabled) {
|
|
231
|
+
const srcRoot = path.join(projectRoot, folderDeployed);
|
|
232
|
+
|
|
233
|
+
// Serve a node_modules package pre-bundled as a single ESM module.
|
|
234
|
+
app.get(/^\/@slice-modules\/(.+)$/, async (req, res) => {
|
|
235
|
+
const spec = req.params[0];
|
|
236
|
+
try {
|
|
237
|
+
const { code } = await devDeps.bundlePackage(spec);
|
|
238
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
239
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
240
|
+
return res.send(code);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const message = `Failed to load external dependency "${spec}": ${error.message}`;
|
|
243
|
+
console.error(`[slice:dev] ${message}`);
|
|
244
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
245
|
+
return res
|
|
246
|
+
.status(502)
|
|
247
|
+
.send(`throw new Error(${JSON.stringify(message)});`);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Rewrite bare imports in served src JavaScript to the virtual endpoint.
|
|
252
|
+
app.use(async (req, res, next) => {
|
|
253
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
|
|
254
|
+
const reqPath = req.path;
|
|
255
|
+
if (!(reqPath.endsWith('.js') || reqPath.endsWith('.mjs'))) return next();
|
|
256
|
+
// Framework internals, generated bundles and the virtual endpoint are
|
|
257
|
+
// served elsewhere and must not be rewritten.
|
|
258
|
+
if (reqPath.startsWith('/@slice-modules/') || reqPath.startsWith('/bundles/') || reqPath.startsWith('/Slice/')) {
|
|
259
|
+
return next();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const filePath = path.join(srcRoot, decodeURIComponent(reqPath));
|
|
263
|
+
const normalized = path.normalize(filePath);
|
|
264
|
+
if (normalized !== srcRoot && !normalized.startsWith(srcRoot + path.sep)) {
|
|
265
|
+
return next(); // path traversal guard
|
|
266
|
+
}
|
|
267
|
+
if (!fs.existsSync(normalized) || !fs.statSync(normalized).isFile()) {
|
|
268
|
+
return next();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const original = fs.readFileSync(normalized, 'utf8');
|
|
273
|
+
const rewritten = await devDeps.rewriteBareImports(original);
|
|
274
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
275
|
+
return res.send(rewritten);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error(`[slice:dev] Failed to rewrite ${reqPath}: ${error.message}`);
|
|
278
|
+
return next();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Centralized public/ folder served at the root URL (Themes, Styles,
|
|
284
|
+
// images…). Mounted before the general src static so its files win.
|
|
285
|
+
app.use(express.static(path.join(projectRoot, folderDeployed, 'public')));
|
|
286
|
+
app.use(express.static(path.join(projectRoot, folderDeployed)));
|
|
287
|
+
} else {
|
|
288
|
+
// Serve the built public/ assets at the root URL.
|
|
289
|
+
app.use(express.static(path.join(projectRoot, folderDeployed, 'public')));
|
|
290
|
+
app.use('/App', express.static(path.join(projectRoot, folderDeployed, 'App')));
|
|
291
|
+
const serveStaticFile = (req, res, filePath, contentType, fileName) => {
|
|
292
|
+
try {
|
|
293
|
+
if (fs.existsSync(filePath)) {
|
|
294
|
+
res.setHeader('Content-Type', contentType);
|
|
295
|
+
return res.send(fs.readFileSync(filePath, 'utf8'));
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error(`Error reading ${fileName}:`, error);
|
|
299
|
+
return res.status(500).send(`Error reading ${fileName}`);
|
|
300
|
+
}
|
|
301
|
+
return res.status(404).send(`${fileName} not found`);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
app.get('/routes.js', (req, res) => {
|
|
305
|
+
const routesPath = path.join(projectRoot, folderDeployed, 'routes.js');
|
|
306
|
+
serveStaticFile(req, res, routesPath, 'application/javascript; charset=utf-8', 'routes.js');
|
|
307
|
+
});
|
|
308
|
+
app.get('/sliceConfig.json', (req, res) => {
|
|
309
|
+
const configPath = path.join(projectRoot, folderDeployed, 'sliceConfig.json');
|
|
310
|
+
serveStaticFile(req, res, configPath, 'application/json; charset=utf-8', 'sliceConfig.json');
|
|
311
|
+
});
|
|
312
|
+
app.use('/bundles/', express.static(path.join(projectRoot, folderDeployed, 'bundles')));
|
|
313
|
+
app.use('/dist/', express.static(path.join(projectRoot, 'dist')));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ==============================================
|
|
317
|
+
// RUTAS DE API
|
|
318
|
+
// ==============================================
|
|
319
|
+
|
|
320
|
+
// Ruta de ejemplo para API
|
|
321
|
+
app.get('/api/status', (req, res) => {
|
|
322
|
+
res.json({
|
|
323
|
+
status: 'ok',
|
|
324
|
+
mode: runMode,
|
|
325
|
+
folder: folderDeployed,
|
|
326
|
+
timestamp: new Date().toISOString(),
|
|
327
|
+
framework: 'Slice.js',
|
|
328
|
+
version: frameworkVersion,
|
|
329
|
+
security: {
|
|
330
|
+
enabled: true,
|
|
331
|
+
mode: 'automatic',
|
|
332
|
+
description: 'Zero-config security - works with any domain'
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ==============================================
|
|
338
|
+
// SEO: robots.txt & sitemap.xml
|
|
339
|
+
// ==============================================
|
|
340
|
+
|
|
341
|
+
app.get('/robots.txt', (req, res) => {
|
|
342
|
+
res.sendFile(resolveDeployedFile('robots.txt'));
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
app.get('/sitemap.xml', (req, res) => {
|
|
346
|
+
res.sendFile(resolveDeployedFile('sitemap.xml'));
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// ==============================================
|
|
350
|
+
// API 404 — las rutas /api/* irreconocibles devuelven JSON, no HTML
|
|
351
|
+
// ==============================================
|
|
352
|
+
|
|
353
|
+
app.use('/api', (req, res) => {
|
|
354
|
+
res.status(404).json({ error: 'API route not found', path: req.originalUrl });
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ==============================================
|
|
358
|
+
// SPA FALLBACK
|
|
359
|
+
// ==============================================
|
|
360
|
+
|
|
361
|
+
app.get('*', (req, res) => {
|
|
362
|
+
const indexPath = path.join(projectRoot, folderDeployed, 'App', 'index.html');
|
|
363
|
+
res.sendFile(indexPath, (err) => {
|
|
364
|
+
if (err) {
|
|
365
|
+
console.error(`[SPA Fallback] Error sending ${indexPath}:`, err);
|
|
366
|
+
res.status(500).send(`<h1>500 - Internal Server Error</h1><p>Could not serve application file</p>`);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ==============================================
|
|
372
|
+
// ERROR HANDLER CENTRALIZADO
|
|
373
|
+
// ==============================================
|
|
374
|
+
|
|
375
|
+
app.use((err, req, res, next) => {
|
|
376
|
+
console.error('[Server Error]', err.stack || err.message || err);
|
|
377
|
+
if (res.headersSent) return next(err);
|
|
378
|
+
res.status(err.status || 500).json({
|
|
379
|
+
error: 'Internal Server Error',
|
|
380
|
+
message: process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : err.message
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ==============================================
|
|
385
|
+
// INICIO DEL SERVIDOR
|
|
386
|
+
// ==============================================
|
|
387
|
+
|
|
388
|
+
function startServer() {
|
|
389
|
+
const server = app.listen(PORT, () => {
|
|
390
|
+
console.log(`🔒 Security middleware: active (zero-config, automatic)`);
|
|
391
|
+
console.log(`🚀 Slice.js server running on port ${PORT}`);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
process.on('SIGINT', () => {
|
|
395
|
+
console.log('\n🛑 Slice server stopped');
|
|
396
|
+
process.exit(0);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
process.on('SIGTERM', () => {
|
|
400
|
+
console.log('\n🛑 Server terminated');
|
|
401
|
+
process.exit(0);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
return server;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
app,
|
|
409
|
+
start: startServer
|
|
410
|
+
};
|
|
411
|
+
}
|