slicejs-web-framework 3.5.0 → 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/StylesManager/ThemeManager/ThemeManager.js +56 -0
- package/api/{middleware → framework}/securityMiddleware.js +18 -74
- package/api/framework/server.js +333 -0
- package/api/index.js +5 -306
- package/api/tests/public-env-resolver.test.js +1 -1
- package/package.json +4 -2
- 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/api/{utils → framework}/publicEnvResolver.js +0 -0
|
@@ -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/
|
|
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
|
+
}
|
package/api/index.js
CHANGED
|
@@ -1,308 +1,7 @@
|
|
|
1
|
-
// api/index.js -
|
|
2
|
-
import
|
|
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
|
|
15
|
-
|
|
16
|
-
import sliceConfig from '../src/sliceConfig.json' with { type: 'json' };
|
|
4
|
+
const server = createSliceServer();
|
|
5
|
+
server.start();
|
|
17
6
|
|
|
18
|
-
|
|
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('../
|
|
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.
|
|
3
|
+
"version": "3.5.1",
|
|
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": {
|
package/src/App/index.html
CHANGED
|
@@ -3,9 +3,39 @@
|
|
|
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
|
|
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="manifest" href="/manifest.json" />
|
|
13
|
+
<meta name="theme-color" content="#F8F4FF" />
|
|
14
|
+
<meta name="color-scheme" content="light dark" />
|
|
15
|
+
<meta property="og:type" content="website" />
|
|
16
|
+
<meta property="og:title" content="Slice.js Web Framework" />
|
|
17
|
+
<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." />
|
|
18
|
+
<meta property="og:url" content="https://slicejs.com" />
|
|
19
|
+
<meta property="og:image" content="https://slicejs.com/images/og-image.png" />
|
|
20
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
21
|
+
<meta name="twitter:title" content="Slice.js Web Framework" />
|
|
22
|
+
<meta name="twitter:description" content="Slice.js — a lightweight web framework built on native Web Components." />
|
|
23
|
+
<meta name="twitter:image" content="https://slicejs.com/images/og-image.png" />
|
|
8
24
|
<link rel="stylesheet" type="text/css" href="/App/style.css" />
|
|
25
|
+
<script type="application/ld+json">
|
|
26
|
+
{
|
|
27
|
+
"@context": "https://schema.org",
|
|
28
|
+
"@type": "WebSite",
|
|
29
|
+
"name": "Slice.js Web Framework",
|
|
30
|
+
"url": "https://slicejs.com",
|
|
31
|
+
"description": "Slice.js — a lightweight web framework built on native Web Components. Build web apps one slice at a time.",
|
|
32
|
+
"author": {
|
|
33
|
+
"@type": "Organization",
|
|
34
|
+
"name": "VKneider",
|
|
35
|
+
"url": "https://github.com/VKneider"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
9
39
|
</head>
|
|
10
40
|
|
|
11
41
|
<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
package/src/sitemap.xml
ADDED
|
File without changes
|