slicejs-web-framework 3.5.0 → 3.5.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.
@@ -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
  }
@@ -1,4 +1,4 @@
1
- // api/middleware/securityMiddleware.js
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 = ['.js', '.css', '.html', '.json', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.woff', '.woff2', '.ttf'],
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
- directAccessProtection
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
+ }
package/api/index.js CHANGED
@@ -1,308 +1,7 @@
1
- // api/index.js - Seguridad automática sin configuración
2
- import express from 'slicejs-web-framework/api/framework/express.js';
3
- import path from 'path';
4
- import fs from 'fs';
5
- import { fileURLToPath } from 'url';
6
- import { dirname } from 'path';
7
- import {
8
- securityMiddleware,
9
- sliceFrameworkProtection,
10
- suspiciousRequestLogger
11
- } from './middleware/securityMiddleware.js';
12
- import { createPublicEnvProvider } from './utils/publicEnvResolver.js';
1
+ // api/index.js - Punto de entrada del servidor
2
+ import { createSliceServer } from 'slicejs-web-framework/api/framework/server.js';
13
3
 
14
- const __filename = fileURLToPath(import.meta.url);
15
- const __dirname = dirname(__filename);
16
- import sliceConfig from '../src/sliceConfig.json' with { type: 'json' };
4
+ const server = createSliceServer();
5
+ server.start();
17
6
 
18
- let server;
19
- const app = express();
20
-
21
- // Parsear argumentos de línea de comandos
22
- const args = process.argv.slice(2);
23
-
24
- const runMode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
25
- const folderDeployed = runMode === 'production' ? 'dist' : 'src';
26
- const publicEnvProvider = createPublicEnvProvider({
27
- mode: runMode,
28
- envFilePath: path.join(__dirname, '..', '.env')
29
- });
30
-
31
- // Obtener puerto desde process.env.PORT con fallback a sliceConfig.json
32
- const PORT = process.env.PORT || sliceConfig.server?.port || 3001;
33
-
34
- // ==============================================
35
- // MIDDLEWARES DE SEGURIDAD (APLICAR PRIMERO)
36
- // ==============================================
37
-
38
- // 1. Logger de peticiones sospechosas (solo observación, no bloquea)
39
- app.use(suspiciousRequestLogger());
40
-
41
- // 2. Protección del framework - TOTALMENTE AUTOMÁTICA
42
- // Detecta automáticamente el dominio desde los headers
43
- // Funciona en localhost, IP, y cualquier dominio
44
- app.use(sliceFrameworkProtection());
45
-
46
- // 3. Middleware de seguridad general
47
- app.use(securityMiddleware({
48
- allowedExtensions: [
49
- '.js', '.css', '.html', '.json',
50
- '.svg', '.png', '.jpg', '.jpeg', '.gif',
51
- '.woff', '.woff2', '.ttf', '.ico'
52
- ],
53
- blockedPaths: [
54
- '/node_modules',
55
- '/package.json',
56
- '/package-lock.json',
57
- '/.env',
58
- '/.git',
59
- '/api/middleware'
60
- ],
61
- allowPublicAssets: true
62
- }));
63
-
64
- // ==============================================
65
- // MIDDLEWARES DE APLICACIÓN
66
- // ==============================================
67
-
68
- // Middleware global para archivos JavaScript con MIME types correctos
69
- app.use((req, res, next) => {
70
- if (req.path.endsWith('.js')) {
71
- // Forzar headers correctos para TODOS los archivos .js
72
- res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
73
- }
74
- next();
75
- });
76
-
77
- // Middleware para parsear JSON y formularios
78
- app.use(express.json());
79
- app.use(express.urlencoded({ extended: true }));
80
-
81
- // Configurar headers de CORS
82
- app.use((req, res, next) => {
83
- res.header('Access-Control-Allow-Origin', '*');
84
- res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
85
- res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
86
-
87
- if (req.method === 'OPTIONS') {
88
- res.sendStatus(200);
89
- } else {
90
- next();
91
- }
92
- });
93
-
94
- // ==============================================
95
- // RUNTIME MODE ENDPOINT
96
- // ==============================================
97
-
98
- app.get('/slice-env.json', (req, res) => {
99
- const payload = publicEnvProvider.getPayload();
100
- res.setHeader('Cache-Control', 'no-store');
101
- res.setHeader('Pragma', 'no-cache');
102
- res.setHeader('Expires', '0');
103
- res.json(payload);
104
- });
105
-
106
- // ==============================================
107
- // PWA — manifest + service worker (dev y prod)
108
- // ==============================================
109
- // Despacha los archivos PWA desde la carpeta desplegada (src en dev, dist en
110
- // prod) si existen; 404 si no, así no estorba cuando la app no es PWA.
111
- // El service worker se sirve SIN caché (para que sus actualizaciones lleguen) y
112
- // con Service-Worker-Allowed:/ para permitir scope raíz.
113
- const servePwaFile = (res, fileName, contentType, extraHeaders = {}) => {
114
- const filePath = path.join(__dirname, `../${folderDeployed}`, fileName);
115
- try {
116
- if (fs.existsSync(filePath)) {
117
- res.setHeader('Content-Type', contentType);
118
- for (const [key, value] of Object.entries(extraHeaders)) res.setHeader(key, value);
119
- return res.send(fs.readFileSync(filePath, 'utf8'));
120
- }
121
- } catch (error) {
122
- console.error(`Error reading ${fileName}:`, error);
123
- return res.status(500).send(`Error reading ${fileName}`);
124
- }
125
- return res.status(404).send(`${fileName} not found`);
126
- };
127
-
128
- app.get('/service-worker.js', (req, res) => {
129
- servePwaFile(res, 'service-worker.js', 'application/javascript; charset=utf-8', {
130
- 'Cache-Control': 'no-cache, no-store, must-revalidate',
131
- 'Service-Worker-Allowed': '/',
132
- });
133
- });
134
-
135
- app.get('/manifest.json', (req, res) => {
136
- servePwaFile(res, 'manifest.json', 'application/manifest+json; charset=utf-8');
137
- });
138
-
139
- // ==============================================
140
- // ARCHIVOS ESTÁTICOS (DESPUÉS DE SEGURIDAD)
141
- // ==============================================
142
-
143
- if (runMode === 'production') {
144
- app.get('/Slice/Slice.js', (req, res) => {
145
- const slicePath = path.join(__dirname, '..', 'node_modules', 'slicejs-web-framework', 'Slice', 'Slice.js');
146
- try {
147
- if (fs.existsSync(slicePath)) {
148
- res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
149
- return res.send(fs.readFileSync(slicePath, 'utf8'));
150
- }
151
- } catch (error) {
152
- console.error(`Error reading Slice.js:`, error);
153
- return res.status(500).send('Error loading framework');
154
- }
155
- return res.status(404).send('Slice.js not found');
156
- });
157
-
158
- app.use('/Slice', (req, res) => res.status(404).send('Not found'));
159
- app.use('/Components', (req, res) => res.status(404).send('Not found'));
160
- }
161
-
162
- // Middleware personalizado para archivos de bundles con MIME types correctos
163
- // ⚠️ DEBE IR ANTES del middleware general para tener prioridad
164
- app.use('/bundles/', (req, res, next) => {
165
- // Solo procesar archivos .js
166
- if (req.path.endsWith('.js')) {
167
- const filePath = path.join(__dirname, `../${folderDeployed}`, 'bundles', req.path);
168
- console.log(`📂 Processing bundle: ${req.path} -> ${filePath}`);
169
-
170
- // Verificar que el archivo existe
171
- if (fs.existsSync(filePath)) {
172
- try {
173
- // Leer y servir el archivo con headers correctos
174
- const fileContent = fs.readFileSync(filePath, 'utf8');
175
- res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
176
- res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // No cachear para permitir actualizaciones en tiempo real
177
- res.setHeader('Pragma', 'no-cache');
178
- res.setHeader('Expires', '0');
179
- console.log(`✅ Serving bundle: ${req.path} (${fileContent.length} bytes, ${Buffer.byteLength(fileContent, 'utf8')} bytes UTF-8)`);
180
- return res.send(fileContent);
181
- } catch (error) {
182
- console.error(`Error reading bundle file:`, error);
183
- return res.status(500).send('Error reading bundle file');
184
- }
185
- } else {
186
- console.log(`❌ Bundle file not found: ${filePath}`);
187
- return res.status(404).send('Bundle file not found');
188
- }
189
- }
190
-
191
- // Para archivos no .js, continuar con el middleware estático normal
192
- next();
193
- });
194
-
195
- // Servir otros archivos de bundles (JSON, CSS, etc.) con el middleware estático normal
196
- app.use('/bundles/', express.static(path.join(__dirname, `../${folderDeployed}`, 'bundles')));
197
- console.log(`📦 Serving bundles from /${folderDeployed}/bundles`);
198
-
199
- // Servir framework Slice.js (solo development)
200
- if (runMode === 'development') {
201
- app.use('/Slice/', express.static(path.join(__dirname, '..', 'node_modules', 'slicejs-web-framework', 'Slice')));
202
- }
203
-
204
- // Servir archivos estáticos del proyecto con allowlist
205
- const publicFolders = Array.isArray(sliceConfig.publicFolders) ? sliceConfig.publicFolders : [];
206
- const normalizedPublicFolders = publicFolders
207
- .filter((entry) => typeof entry === 'string')
208
- .map((entry) => entry.trim())
209
- .filter((entry) => entry.length > 0)
210
- .map((entry) => (entry.startsWith('/') ? entry : `/${entry}`));
211
-
212
- if (runMode === 'development') {
213
- app.use(express.static(path.join(__dirname, `../${folderDeployed}`)));
214
- } else {
215
- app.use('/App', express.static(path.join(__dirname, `../${folderDeployed}`, 'App')));
216
- const serveStaticFile = (req, res, filePath, contentType, fileName) => {
217
- try {
218
- if (fs.existsSync(filePath)) {
219
- res.setHeader('Content-Type', contentType);
220
- return res.send(fs.readFileSync(filePath, 'utf8'));
221
- }
222
- } catch (error) {
223
- console.error(`Error reading ${fileName}:`, error);
224
- return res.status(500).send(`Error reading ${fileName}`);
225
- }
226
- return res.status(404).send(`${fileName} not found`);
227
- };
228
-
229
- // (manifest.json y service-worker.js los sirve el bloque PWA compartido de arriba,
230
- // con headers correctos en dev y prod.)
231
- app.get('/routes.js', (req, res) => {
232
- const routesPath = path.join(__dirname, `../${folderDeployed}`, 'routes.js');
233
- serveStaticFile(req, res, routesPath, 'application/javascript; charset=utf-8', 'routes.js');
234
- });
235
- app.get('/sliceConfig.json', (req, res) => {
236
- const configPath = path.join(__dirname, `../${folderDeployed}`, 'sliceConfig.json');
237
- serveStaticFile(req, res, configPath, 'application/json; charset=utf-8', 'sliceConfig.json');
238
- });
239
- for (const folder of normalizedPublicFolders) {
240
- app.use(folder, express.static(path.join(__dirname, `../${folderDeployed}`, folder)));
241
- }
242
- app.use('/bundles/', express.static(path.join(__dirname, `../${folderDeployed}`, 'bundles')));
243
- app.use('/dist/', express.static(path.join(__dirname, '..', 'dist')));
244
- }
245
-
246
- // ==============================================
247
- // RUTAS DE API
248
- // ==============================================
249
-
250
- // Ruta de ejemplo para API
251
- app.get('/api/status', (req, res) => {
252
- res.json({
253
- status: 'ok',
254
- mode: runMode,
255
- folder: folderDeployed,
256
- timestamp: new Date().toISOString(),
257
- framework: 'Slice.js',
258
- version: '2.0.0',
259
- security: {
260
- enabled: true,
261
- mode: 'automatic',
262
- description: 'Zero-config security - works with any domain'
263
- }
264
- });
265
- });
266
-
267
-
268
- // ==============================================
269
- // SPA FALLBACK
270
- // ==============================================
271
-
272
- // SPA fallback - servir index.html para rutas no encontradas
273
- app.get('*', (req, res) => {
274
- const indexPath = path.join(__dirname, `../${folderDeployed}`, "App", 'index.html');
275
- res.sendFile(indexPath, (err) => {
276
- if (err) {
277
- console.error(`[SPA Fallback] Error sending ${indexPath}:`, err);
278
- res.status(500).send(`<h1>500 - Internal Server Error</h1><p>Could not serve application file</p>`);
279
- }
280
- });
281
- });
282
-
283
- // ==============================================
284
- // INICIO DEL SERVIDOR
285
- // ==============================================
286
-
287
- function startServer() {
288
- server = app.listen(PORT, () => {
289
- console.log(`🔒 Security middleware: active (zero-config, automatic)`);
290
- console.log(`🚀 Slice.js server running on port ${PORT}`);
291
- });
292
- }
293
-
294
- // Manejar cierre del proceso
295
- process.on('SIGINT', () => {
296
- console.log('\n🛑 Slice server stopped');
297
- process.exit(0);
298
- });
299
-
300
- process.on('SIGTERM', () => {
301
- console.log('\n🛑 Server terminated');
302
- process.exit(0);
303
- });
304
-
305
- // Iniciar servidor
306
- startServer();
307
-
308
- export default app;
7
+ export default server.app;
@@ -4,7 +4,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
6
 
7
- const resolverModulePath = new URL('../utils/publicEnvResolver.js', import.meta.url);
7
+ const resolverModulePath = new URL('../framework/publicEnvResolver.js', import.meta.url);
8
8
 
9
9
  async function withTempEnvFile(contents, callback) {
10
10
  const dir = await mkdtemp(path.join(tmpdir(), 'slice-public-env-'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-web-framework",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -25,10 +25,12 @@
25
25
  "type": "module",
26
26
  "exports": {
27
27
  ".": "./Slice/Slice.js",
28
- "./api/framework/express.js": "./api/framework/express.js"
28
+ "./api/framework/express.js": "./api/framework/express.js",
29
+ "./api/framework/server.js": "./api/framework/server.js"
29
30
  },
30
31
  "types": "./types/index.d.ts",
31
32
  "dependencies": {
33
+ "compression": "^1.8.1",
32
34
  "express": "^4.19.2"
33
35
  },
34
36
  "devDependencies": {
@@ -3,9 +3,62 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Slice App</title>
6
+ <title>Slice.js Web Framework</title>
7
+ <meta name="description" content="Slice.js — a lightweight web framework built on native Web Components. Build web apps one slice at a time. No build step required." />
8
+ <meta name="robots" content="index, follow" />
9
+ <meta name="author" content="vkneider.dev" />
10
+ <link rel="canonical" href="https://slicejs.com" />
7
11
  <link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
12
+ <link rel="apple-touch-icon" href="/images/og-image.png" />
13
+ <link rel="manifest" href="/manifest.json" />
14
+ <meta name="theme-color" content="#F8F4FF" />
15
+ <meta name="color-scheme" content="light dark" />
16
+ <meta property="og:type" content="website" />
17
+ <meta property="og:title" content="Slice.js Web Framework" />
18
+ <meta property="og:description" content="Slice.js — a lightweight web framework built on native Web Components. Build web apps one slice at a time. No build step required." />
19
+ <meta property="og:url" content="https://slicejs.com" />
20
+ <meta property="og:image" content="https://slicejs.com/images/og-image.png" />
21
+ <meta name="twitter:card" content="summary_large_image" />
22
+ <meta name="twitter:title" content="Slice.js Web Framework" />
23
+ <meta name="twitter:description" content="Slice.js — a lightweight web framework built on native Web Components." />
24
+ <meta name="twitter:image" content="https://slicejs.com/images/og-image.png" />
8
25
  <link rel="stylesheet" type="text/css" href="/App/style.css" />
26
+ <script type="application/ld+json">
27
+ {
28
+ "@context": "https://schema.org",
29
+ "@type": "WebSite",
30
+ "name": "Slice.js Web Framework",
31
+ "url": "https://slicejs.com",
32
+ "description": "Slice.js — a lightweight web framework built on native Web Components. Build web apps one slice at a time.",
33
+ "author": {
34
+ "@type": "Organization",
35
+ "name": "VKneider",
36
+ "url": "https://github.com/VKneider"
37
+ },
38
+ "potentialAction": {
39
+ "@type": "SearchAction",
40
+ "target": {
41
+ "@type": "EntryPoint",
42
+ "urlTemplate": "https://slicejs.com/?q={search_term_string}"
43
+ },
44
+ "query-input": "required name=search_term_string"
45
+ }
46
+ }
47
+ </script>
48
+ <script type="application/ld+json">
49
+ {
50
+ "@context": "https://schema.org",
51
+ "@type": "BreadcrumbList",
52
+ "itemListElement": [
53
+ {
54
+ "@type": "ListItem",
55
+ "position": 1,
56
+ "name": "Home",
57
+ "item": "https://slicejs.com/"
58
+ }
59
+ ]
60
+ }
61
+ </script>
9
62
  </head>
10
63
 
11
64
  <body>
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "Slice.js Web Framework",
3
+ "short_name": "Slice.js",
4
+ "start_url": "/",
5
+ "display": "standalone",
6
+ "background_color": "#F8F4FF",
7
+ "theme_color": "#F8F4FF",
8
+ "icons": [
9
+ {
10
+ "src": "/images/favicon.ico",
11
+ "sizes": "192x192 512x512",
12
+ "type": "image/x-icon"
13
+ }
14
+ ]
15
+ }
package/src/robots.txt ADDED
@@ -0,0 +1,4 @@
1
+ User-agent: *
2
+ Allow: /
3
+
4
+ Sitemap: https://slicejs.com/sitemap.xml
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
+ <url>
4
+ <loc>https://slicejs.com/</loc>
5
+ <priority>1.0</priority>
6
+ <changefreq>weekly</changefreq>
7
+ </url>
8
+ </urlset>
File without changes