holygrail5 1.0.21 → 1.0.23
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/README.md +72 -0
- package/config.json +85 -18
- package/dist/assets/fonts/SuisseIntlMono-Bold-WebS.woff +0 -0
- package/dist/assets/fonts/SuisseIntlMono-Bold-WebS.woff2 +0 -0
- package/dist/assets/fonts/SuisseIntlMono-Regular-WebS.woff +0 -0
- package/dist/assets/fonts/SuisseIntlMono-Regular-WebS.woff2 +0 -0
- package/dist/assets/fonts/suisse-intl-thin.woff +0 -0
- package/dist/assets/fonts/suisse-intl-thin.woff2 +0 -0
- package/dist/componentes.html +1 -8
- package/dist/developer-guide.md +4 -0
- package/dist/guide-styles.css +85 -56
- package/dist/index.html +2727 -2690
- package/dist/output.css +123 -70
- package/dist/skills.html +17 -5
- package/dist/themes/dutti-demo.html +76 -39
- package/dist/themes/dutti.css +10 -6
- package/dist/themes/limited-demo.html +55 -18
- package/dist/themes/limited.css +8 -6
- package/package.json +2 -2
- package/src/.data/.previous-values.json +69 -20
- package/src/assets/fonts/SuisseIntlMono-Bold-WebS.woff +0 -0
- package/src/assets/fonts/SuisseIntlMono-Bold-WebS.woff2 +0 -0
- package/src/assets/fonts/SuisseIntlMono-Regular-WebS.woff +0 -0
- package/src/assets/fonts/SuisseIntlMono-Regular-WebS.woff2 +0 -0
- package/src/assets/fonts/suisse-intl-thin.woff +0 -0
- package/src/assets/fonts/suisse-intl-thin.woff2 +0 -0
- package/src/build/asset-manager.js +8 -0
- package/src/build/components-generator.js +1 -8
- package/src/build/skills-generator.js +12 -4
- package/src/build/theme-transformer.js +10 -3
- package/src/dev-server.js +28 -13
- package/src/docs-generator/guide-styles.css +85 -56
- package/src/docs-generator/html-generator.js +188 -183
- package/src/docs-generator/sections/colors-section.js +15 -5
- package/src/generators/typo-generator.js +1 -2
- package/src/generators/utils.js +15 -0
- package/themes/_base/_radios.css +7 -6
- package/themes/dutti/README.md +17 -0
- package/themes/dutti/_variables.css +3 -0
- package/themes/dutti/theme.json +2 -1
- package/themes/limited/_variables.css +1 -0
- package/themes/limited/theme.json +2 -1
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
"desktop": "992px"
|
|
5
5
|
},
|
|
6
6
|
"fontFamilyMap": {
|
|
7
|
+
"primary-thin": "\"suisse-thin\", Arial, Helvetica, sans-serif",
|
|
7
8
|
"primary-light": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
8
9
|
"primary-regular": "\"suisse-regular\", Arial, Helvetica, sans-serif",
|
|
9
10
|
"primary-bold": "\"suisse-semibold\", Arial, Helvetica, sans-serif",
|
|
10
|
-
"secondary": "\"suisse-medium\", Arial, Helvetica, sans-serif"
|
|
11
|
+
"secondary": "\"suisse-medium\", Arial, Helvetica, sans-serif",
|
|
12
|
+
"mono-regular": "\"suisse-mono-regular\", ui-monospace, monospace",
|
|
13
|
+
"mono-bold": "\"suisse-mono-bold\", ui-monospace, monospace",
|
|
14
|
+
"evil": "\"><script>alert(1)</script>"
|
|
11
15
|
},
|
|
12
16
|
"spacingMap": {
|
|
13
17
|
"0": "0",
|
|
@@ -65,12 +69,25 @@
|
|
|
65
69
|
"gold": "#A38A6B",
|
|
66
70
|
"platinum": "#5B7FA1",
|
|
67
71
|
"bg-light": "#f9f9f9",
|
|
68
|
-
"bg-cream": "#f4f2ed"
|
|
72
|
+
"bg-cream": "#f4f2ed",
|
|
73
|
+
"evil": "\"><script>alert(1)</script>"
|
|
69
74
|
},
|
|
70
75
|
"typo": {
|
|
76
|
+
"title-thin": {
|
|
77
|
+
"fontFamily": "\"suisse-thin\", Arial, Helvetica, sans-serif",
|
|
78
|
+
"fontWeight": "100",
|
|
79
|
+
"mobile": {
|
|
80
|
+
"fontSize": "24px",
|
|
81
|
+
"lineHeight": "1"
|
|
82
|
+
},
|
|
83
|
+
"desktop": {
|
|
84
|
+
"fontSize": "24px",
|
|
85
|
+
"lineHeight": "1"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
71
88
|
"title-xxl": {
|
|
72
89
|
"fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
|
|
73
|
-
"fontWeight": "
|
|
90
|
+
"fontWeight": "400",
|
|
74
91
|
"mobile": {
|
|
75
92
|
"fontSize": "24px",
|
|
76
93
|
"lineHeight": "1"
|
|
@@ -96,7 +113,7 @@
|
|
|
96
113
|
},
|
|
97
114
|
"title-l-b": {
|
|
98
115
|
"fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
|
|
99
|
-
"fontWeight": "
|
|
116
|
+
"fontWeight": "400",
|
|
100
117
|
"mobile": {
|
|
101
118
|
"fontSize": "12px",
|
|
102
119
|
"lineHeight": "1.4"
|
|
@@ -108,7 +125,7 @@
|
|
|
108
125
|
},
|
|
109
126
|
"title-l": {
|
|
110
127
|
"fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
111
|
-
"fontWeight": "
|
|
128
|
+
"fontWeight": "300",
|
|
112
129
|
"letterSpacing": "0.16em",
|
|
113
130
|
"textTransform": "uppercase",
|
|
114
131
|
"mobile": {
|
|
@@ -122,7 +139,7 @@
|
|
|
122
139
|
},
|
|
123
140
|
"title-m": {
|
|
124
141
|
"fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
125
|
-
"fontWeight": "
|
|
142
|
+
"fontWeight": "300",
|
|
126
143
|
"letterSpacing": "0.16em",
|
|
127
144
|
"mobile": {
|
|
128
145
|
"fontSize": "12px",
|
|
@@ -135,7 +152,7 @@
|
|
|
135
152
|
},
|
|
136
153
|
"title-s-b": {
|
|
137
154
|
"fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
|
|
138
|
-
"fontWeight": "
|
|
155
|
+
"fontWeight": "400",
|
|
139
156
|
"letterSpacing": "0.16em",
|
|
140
157
|
"mobile": {
|
|
141
158
|
"fontSize": "10px",
|
|
@@ -148,7 +165,7 @@
|
|
|
148
165
|
},
|
|
149
166
|
"title-s": {
|
|
150
167
|
"fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
151
|
-
"fontWeight": "
|
|
168
|
+
"fontWeight": "300",
|
|
152
169
|
"letterSpacing": "0.16em",
|
|
153
170
|
"textTransform": "uppercase",
|
|
154
171
|
"mobile": {
|
|
@@ -162,7 +179,7 @@
|
|
|
162
179
|
},
|
|
163
180
|
"text-l": {
|
|
164
181
|
"fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
165
|
-
"fontWeight": "
|
|
182
|
+
"fontWeight": "300",
|
|
166
183
|
"letterSpacing": "0.04em",
|
|
167
184
|
"mobile": {
|
|
168
185
|
"fontSize": "13px",
|
|
@@ -175,7 +192,7 @@
|
|
|
175
192
|
},
|
|
176
193
|
"text-m": {
|
|
177
194
|
"fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
178
|
-
"fontWeight": "
|
|
195
|
+
"fontWeight": "300",
|
|
179
196
|
"letterSpacing": "0.04em",
|
|
180
197
|
"mobile": {
|
|
181
198
|
"fontSize": "12px",
|
|
@@ -188,7 +205,7 @@
|
|
|
188
205
|
},
|
|
189
206
|
"p-tag": {
|
|
190
207
|
"fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
191
|
-
"fontWeight": "
|
|
208
|
+
"fontWeight": "300",
|
|
192
209
|
"letterSpacing": "0.16em",
|
|
193
210
|
"mobile": {
|
|
194
211
|
"fontSize": "9px",
|
|
@@ -201,7 +218,7 @@
|
|
|
201
218
|
},
|
|
202
219
|
"hg-body-l": {
|
|
203
220
|
"fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
204
|
-
"fontWeight": "
|
|
221
|
+
"fontWeight": "300",
|
|
205
222
|
"letterSpacing": "0.04em",
|
|
206
223
|
"mobile": {
|
|
207
224
|
"fontSize": "12px",
|
|
@@ -214,7 +231,7 @@
|
|
|
214
231
|
},
|
|
215
232
|
"hg-body-l-b": {
|
|
216
233
|
"fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
|
|
217
|
-
"fontWeight": "
|
|
234
|
+
"fontWeight": "400",
|
|
218
235
|
"letterSpacing": "0.04em",
|
|
219
236
|
"mobile": {
|
|
220
237
|
"fontSize": "12px",
|
|
@@ -227,7 +244,7 @@
|
|
|
227
244
|
},
|
|
228
245
|
"hg-body-m": {
|
|
229
246
|
"fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
230
|
-
"fontWeight": "
|
|
247
|
+
"fontWeight": "300",
|
|
231
248
|
"letterSpacing": "0.04em",
|
|
232
249
|
"mobile": {
|
|
233
250
|
"fontSize": "12px",
|
|
@@ -240,7 +257,7 @@
|
|
|
240
257
|
},
|
|
241
258
|
"hg-body-m-b": {
|
|
242
259
|
"fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
|
|
243
|
-
"fontWeight": "
|
|
260
|
+
"fontWeight": "400",
|
|
244
261
|
"letterSpacing": "0.04em",
|
|
245
262
|
"mobile": {
|
|
246
263
|
"fontSize": "12px",
|
|
@@ -253,7 +270,7 @@
|
|
|
253
270
|
},
|
|
254
271
|
"label-m": {
|
|
255
272
|
"fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
256
|
-
"fontWeight": "
|
|
273
|
+
"fontWeight": "300",
|
|
257
274
|
"letterSpacing": "0.16em",
|
|
258
275
|
"textTransform": "uppercase",
|
|
259
276
|
"mobile": {
|
|
@@ -267,7 +284,7 @@
|
|
|
267
284
|
},
|
|
268
285
|
"label-m-b": {
|
|
269
286
|
"fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
|
|
270
|
-
"fontWeight": "
|
|
287
|
+
"fontWeight": "400",
|
|
271
288
|
"letterSpacing": "0.16em",
|
|
272
289
|
"textTransform": "uppercase",
|
|
273
290
|
"mobile": {
|
|
@@ -281,7 +298,7 @@
|
|
|
281
298
|
},
|
|
282
299
|
"label-s": {
|
|
283
300
|
"fontFamily": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
284
|
-
"fontWeight": "
|
|
301
|
+
"fontWeight": "300",
|
|
285
302
|
"letterSpacing": "0.06em",
|
|
286
303
|
"textTransform": "uppercase",
|
|
287
304
|
"mobile": {
|
|
@@ -295,7 +312,7 @@
|
|
|
295
312
|
},
|
|
296
313
|
"label-s-b": {
|
|
297
314
|
"fontFamily": "\"suisse-regular\", Arial, Helvetica, sans-serif",
|
|
298
|
-
"fontWeight": "
|
|
315
|
+
"fontWeight": "400",
|
|
299
316
|
"letterSpacing": "0.06em",
|
|
300
317
|
"textTransform": "uppercase",
|
|
301
318
|
"mobile": {
|
|
@@ -306,6 +323,34 @@
|
|
|
306
323
|
"fontSize": "10px",
|
|
307
324
|
"lineHeight": "1"
|
|
308
325
|
}
|
|
326
|
+
},
|
|
327
|
+
"label-mono": {
|
|
328
|
+
"fontFamily": "\"suisse-mono-regular\", ui-monospace, monospace",
|
|
329
|
+
"fontWeight": "400",
|
|
330
|
+
"letterSpacing": "0.06em",
|
|
331
|
+
"textTransform": "uppercase",
|
|
332
|
+
"mobile": {
|
|
333
|
+
"fontSize": "10px",
|
|
334
|
+
"lineHeight": "1.2"
|
|
335
|
+
},
|
|
336
|
+
"desktop": {
|
|
337
|
+
"fontSize": "10px",
|
|
338
|
+
"lineHeight": "1.2"
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
"label-mono-b": {
|
|
342
|
+
"fontFamily": "\"suisse-mono-bold\", ui-monospace, monospace",
|
|
343
|
+
"fontWeight": "700",
|
|
344
|
+
"letterSpacing": "0.06em",
|
|
345
|
+
"textTransform": "uppercase",
|
|
346
|
+
"mobile": {
|
|
347
|
+
"fontSize": "10px",
|
|
348
|
+
"lineHeight": "1.2"
|
|
349
|
+
},
|
|
350
|
+
"desktop": {
|
|
351
|
+
"fontSize": "10px",
|
|
352
|
+
"lineHeight": "1.2"
|
|
353
|
+
}
|
|
309
354
|
}
|
|
310
355
|
},
|
|
311
356
|
"variables": {
|
|
@@ -314,6 +359,9 @@
|
|
|
314
359
|
"--hg-typo-font-family-serif": "'Playfair Display', 'Georgia', serif",
|
|
315
360
|
"--hg-typo-font-family-primary-light": "\"suisse-light\", Arial, Helvetica, sans-serif",
|
|
316
361
|
"--hg-typo-font-family-primary-bold": "\"suisse-semibold\", Arial, Helvetica, sans-serif",
|
|
362
|
+
"--hg-typo-font-family-mono-regular": "\"suisse-mono-regular\", ui-monospace, monospace",
|
|
363
|
+
"--hg-typo-font-family-mono-bold": "\"suisse-mono-bold\", ui-monospace, monospace",
|
|
364
|
+
"--hg-typo-font-family-primary-thin": "\"suisse-thin\", Arial, Helvetica, sans-serif",
|
|
317
365
|
"--hg-typo-line-height-1": "1",
|
|
318
366
|
"--hg-typo-line-height-1-976": "1.976",
|
|
319
367
|
"--hg-typo-line-height-1-2": "1.2",
|
|
@@ -406,6 +454,7 @@
|
|
|
406
454
|
"--hg-color-gold": "#A38A6B",
|
|
407
455
|
"--hg-color-platinum": "#5B7FA1",
|
|
408
456
|
"--hg-color-bg-light": "#f9f9f9",
|
|
409
|
-
"--hg-color-bg-cream": "#f4f2ed"
|
|
457
|
+
"--hg-color-bg-cream": "#f4f2ed",
|
|
458
|
+
"--hg-color-evil": "\"><script>alert(1)</script>"
|
|
410
459
|
}
|
|
411
460
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -27,6 +27,14 @@ const ASSETS_CONFIG = {
|
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
29
|
fonts: [
|
|
30
|
+
{
|
|
31
|
+
source: 'src/assets/fonts/suisse-intl-thin.woff2',
|
|
32
|
+
dest: 'dist/assets/fonts/suisse-intl-thin.woff2'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
source: 'src/assets/fonts/suisse-intl-thin.woff',
|
|
36
|
+
dest: 'dist/assets/fonts/suisse-intl-thin.woff'
|
|
37
|
+
},
|
|
30
38
|
{
|
|
31
39
|
source: 'src/assets/fonts/suisse-intl-light.woff2',
|
|
32
40
|
dest: 'dist/assets/fonts/suisse-intl-light.woff2'
|
|
@@ -479,20 +479,13 @@ function generateComponentsPage(projectRoot, configData = null) {
|
|
|
479
479
|
<meta charset="UTF-8">
|
|
480
480
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
481
481
|
<title>HolyGrail5 — Componentes base</title>
|
|
482
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
483
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
484
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Instrument+Sans:regular,100,500,600,700">
|
|
485
482
|
<!-- Framework base -->
|
|
486
483
|
<link rel="stylesheet" href="output.css">
|
|
487
484
|
<!-- Tema base genérico: ${BASE_THEME} (variables + componentes) -->
|
|
488
485
|
<link rel="stylesheet" href="themes/${BASE_THEME}.css">
|
|
489
|
-
<!-- Estilos compartidos de guía (header, sidebar, demo
|
|
486
|
+
<!-- Estilos compartidos de guía (header, sidebar, demo-*; incluye @font-face Suisse y la regla body en Suisse) -->
|
|
490
487
|
<link rel="stylesheet" href="guide-styles.css">
|
|
491
488
|
<style>
|
|
492
|
-
body {
|
|
493
|
-
font-family: 'Instrument Sans', sans-serif !important;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
489
|
/* Descripción de cada sección (debajo del título) */
|
|
497
490
|
.cmp-desc {
|
|
498
491
|
font-size: 14px;
|
|
@@ -284,15 +284,23 @@ function buildPage(skill, activeThemes = FALLBACK_THEMES_IN_NAV) {
|
|
|
284
284
|
<meta charset="UTF-8">
|
|
285
285
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
286
286
|
<title>HolyGrail5 — Developer Guide</title>
|
|
287
|
-
<link href="https://fonts.googleapis.com" rel="preconnect">
|
|
288
|
-
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin="anonymous">
|
|
289
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Instrument+Sans:regular,100,500,600,700" media="all">
|
|
290
287
|
<script src="https://cdn.jsdelivr.net/gh/studio-freight/lenis@1.0.29/bundled/lenis.min.js"></script>
|
|
291
288
|
<link rel="stylesheet" href="output.css">
|
|
292
289
|
<style>
|
|
290
|
+
/* @font-face de Suisse (esta página no enlaza guide-styles.css, donde
|
|
291
|
+
normalmente viven). Misma convención: una familia por peso, woff2+woff
|
|
292
|
+
con fallback, y el font-weight numérico real de cada cut. */
|
|
293
|
+
@font-face { font-family: "suisse-light"; font-weight: 300; font-display: swap;
|
|
294
|
+
src: local("SuisseIntl-Light"), url('assets/fonts/suisse-intl-light.woff2') format('woff2'), url('assets/fonts/suisse-intl-light.woff') format('woff'); }
|
|
295
|
+
@font-face { font-family: "suisse-regular"; font-weight: 400; font-display: swap;
|
|
296
|
+
src: local("SuisseIntl-Regular"), url('assets/fonts/suisse-intl-regular.woff2') format('woff2'), url('assets/fonts/suisse-intl-regular.woff') format('woff'); }
|
|
297
|
+
@font-face { font-family: "suisse-medium"; font-weight: 500; font-display: swap;
|
|
298
|
+
src: local("SuisseIntl-Medium"), url('assets/fonts/suisse-intl-medium.woff2') format('woff2'), url('assets/fonts/suisse-intl-medium.woff') format('woff'); }
|
|
299
|
+
@font-face { font-family: "suisse-semibold"; font-weight: 600; font-display: swap;
|
|
300
|
+
src: local("SuisseIntl-SemiBold"), url('assets/fonts/suisse-intl-semibold.woff2') format('woff2'), url('assets/fonts/suisse-intl-semibold.woff') format('woff'); }
|
|
293
301
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
294
302
|
body {
|
|
295
|
-
font-family:
|
|
303
|
+
font-family: var(--hg-typo-font-family-primary-regular);
|
|
296
304
|
background: #fff;
|
|
297
305
|
color: #111;
|
|
298
306
|
-webkit-font-smoothing: antialiased;
|
|
@@ -247,7 +247,10 @@ class ThemeTransformer {
|
|
|
247
247
|
if (config) {
|
|
248
248
|
const typoConfig = applyThemeTypographyOverrides(config, themeData);
|
|
249
249
|
const typoSection = generateTypographyHTML(typoConfig);
|
|
250
|
-
|
|
250
|
+
// Usamos un replacer de función: si `typoSection` contiene `$`
|
|
251
|
+
// (frecuente en CSS), pasarlo como string de reemplazo haría que
|
|
252
|
+
// `replace` interpretara `$&`, `$1`, `$$`… y corrompiera la salida.
|
|
253
|
+
content = content.replace(/<!--\s*HG_TYPO_TABLE\s*-->/g, () => typoSection);
|
|
251
254
|
} else {
|
|
252
255
|
// Sin config, eliminamos el placeholder para no mostrarlo en crudo
|
|
253
256
|
content = content.replace(/<!--\s*HG_TYPO_TABLE\s*-->/g, '');
|
|
@@ -257,7 +260,9 @@ class ThemeTransformer {
|
|
|
257
260
|
// Si no lo hay, quitamos el placeholder para no dejar comentarios huérfanos.
|
|
258
261
|
if (themeData) {
|
|
259
262
|
const themeBlock = generateThemeBlockHTML(themeData, config);
|
|
260
|
-
|
|
263
|
+
// Replacer de función por el mismo motivo que el bloque de tipografía:
|
|
264
|
+
// blindar la salida frente a `$` en el contenido inyectado.
|
|
265
|
+
content = content.replace(/<!--\s*HG_THEME_BLOCK\s*-->/g, () => themeBlock);
|
|
261
266
|
} else {
|
|
262
267
|
content = content.replace(/<!--\s*HG_THEME_BLOCK\s*-->/g, '');
|
|
263
268
|
}
|
|
@@ -291,7 +296,9 @@ class ThemeTransformer {
|
|
|
291
296
|
// la lista de temas activos, se respeta; si no, se cae al
|
|
292
297
|
// fallback estático THEMES_IN_NAV (compatibilidad).
|
|
293
298
|
const headerAndSidebarHTML = buildHeaderAndSidebar(themeName, themesForNav);
|
|
294
|
-
|
|
299
|
+
// Replacer de función: preserva el `<body>` capturado y evita que un
|
|
300
|
+
// `$` en las etiquetas del tema (themesForNav) se interprete como patrón.
|
|
301
|
+
content = content.replace(/(<body[^>]*>)/i, (m) => m + '\n' + headerAndSidebarHTML);
|
|
295
302
|
|
|
296
303
|
// Eliminar el título h1 del contenido si existe (ya está en el header)
|
|
297
304
|
content = content.replace(/<h1 class="demo-title">Sistema de Theming [^<]+<\/h1>\s*/g, '');
|
package/src/dev-server.js
CHANGED
|
@@ -36,7 +36,10 @@ function listenOnAvailablePort(server, initialPort) {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
server.once('error', onError);
|
|
39
|
-
|
|
39
|
+
// Bind explícito a loopback: es un servidor de desarrollo y no debe
|
|
40
|
+
// quedar expuesto en la red local (el default de Node escucha en
|
|
41
|
+
// todas las interfaces).
|
|
42
|
+
server.listen(currentPort, '127.0.0.1', () => {
|
|
40
43
|
server.removeListener('error', onError);
|
|
41
44
|
resolve(currentPort);
|
|
42
45
|
});
|
|
@@ -73,22 +76,34 @@ function getMimeType(filePath) {
|
|
|
73
76
|
// Servidor HTTP simple y rápido
|
|
74
77
|
function createServer() {
|
|
75
78
|
return http.createServer((req, res) => {
|
|
76
|
-
// Decodificar URL
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// Decodificar URL. Una URL con secuencias %-malformadas hace que
|
|
80
|
+
// decodeURIComponent lance URIError; sin este try/catch la excepción
|
|
81
|
+
// quedaría sin capturar y tumbaría el proceso del servidor (DoS con
|
|
82
|
+
// un solo request tipo `GET /%`).
|
|
83
|
+
let filePath;
|
|
84
|
+
try {
|
|
85
|
+
filePath = decodeURIComponent(req.url);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
88
|
+
res.end('400 Bad Request');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Eliminar query string (antes de normalizar la ruta)
|
|
93
|
+
filePath = filePath.split('?')[0];
|
|
94
|
+
|
|
79
95
|
// Si es la raíz, servir index.html
|
|
80
96
|
if (filePath === '/' || filePath === '') {
|
|
81
97
|
filePath = '/index.html';
|
|
82
98
|
}
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!fullPath.startsWith(DIST_DIR)) {
|
|
99
|
+
|
|
100
|
+
// Construir ruta completa y normalizar
|
|
101
|
+
const fullPath = path.resolve(DIST_DIR, '.' + path.sep + filePath);
|
|
102
|
+
|
|
103
|
+
// Verificar que el archivo esté DENTRO de dist/. Comparamos con el
|
|
104
|
+
// separador final para que un directorio hermano con prefijo común
|
|
105
|
+
// (p. ej. `dist-backup/`) no pase el filtro de `startsWith`.
|
|
106
|
+
if (fullPath !== DIST_DIR && !fullPath.startsWith(DIST_DIR + path.sep)) {
|
|
92
107
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
93
108
|
res.end('Forbidden');
|
|
94
109
|
return;
|