slicejs-web-framework 4.1.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Slice/Components/Structural/Controller/Controller.js +1427 -1406
- package/Slice/Components/Structural/EventManager/EventManager.js +652 -651
- package/Slice/Components/Structural/Router/Router.js +982 -820
- package/Slice/Components/Structural/Router/applyRouteMetadata.js +151 -8
- package/Slice/Components/Structural/Translator/Translator.js +1111 -0
- package/Slice/Components/Structural/Translator/TranslatorDebugger.js +511 -0
- package/Slice/Components/Structural/Translator/content.js +188 -0
- package/Slice/Components/Structural/Translator/dom.js +253 -0
- package/Slice/Components/Structural/Translator/localePath.js +90 -0
- package/Slice/Components/Structural/Translator/localized.js +80 -0
- package/Slice/Components/Structural/Translator/markers.js +148 -0
- package/Slice/Components/Structural/Translator/messages.js +211 -0
- package/Slice/Slice.js +826 -711
- package/api/framework/content.js +73 -0
- package/api/framework/seo.js +394 -43
- package/api/framework/server.js +205 -28
- package/package.json +2 -1
- package/sliceConfig.schema.json +150 -122
- package/types/index.d.ts +347 -219
package/api/framework/server.js
CHANGED
|
@@ -15,8 +15,10 @@ import { pathToFileURL } from 'node:url';
|
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
16
|
import zlib from 'node:zlib';
|
|
17
17
|
import {
|
|
18
|
+
buildTranslatorContext,
|
|
18
19
|
injectHead,
|
|
19
20
|
loadSeoManifest,
|
|
21
|
+
loadTranslatorDictionaries,
|
|
20
22
|
matchPathPattern,
|
|
21
23
|
normalizeOrigin,
|
|
22
24
|
pickRouteEntry,
|
|
@@ -53,6 +55,54 @@ function readComponentPrefixes(projectRoot) {
|
|
|
53
55
|
return [];
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
/** The `translator` block from the deployed config (`dist` wins in production). */
|
|
59
|
+
function readTranslatorConfig(projectRoot, folderDeployed) {
|
|
60
|
+
for (const folder of [folderDeployed, 'src']) {
|
|
61
|
+
const file = path.join(projectRoot, folder, 'sliceConfig.json');
|
|
62
|
+
if (!fs.existsSync(file)) continue;
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(fs.readFileSync(file, 'utf8')).translator || null;
|
|
65
|
+
} catch {
|
|
66
|
+
// unreadable config: SEO stays single-language, as before
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The `router` block from the deployed config (`dist` wins in production). */
|
|
73
|
+
function readRouterConfig(projectRoot, folderDeployed) {
|
|
74
|
+
for (const folder of [folderDeployed, 'src']) {
|
|
75
|
+
const file = path.join(projectRoot, folder, 'sliceConfig.json');
|
|
76
|
+
if (!fs.existsSync(file)) continue;
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(fs.readFileSync(file, 'utf8')).router || {};
|
|
79
|
+
} catch {
|
|
80
|
+
// unreadable config: no title affixes, as before
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Folder holding the translator dictionaries, from the deployed config (`translator.path`,
|
|
88
|
+
* `/locales` by default). Null when the translator is off or the path is unusable, so no
|
|
89
|
+
* extra mount is added. `slice build` copies `src/<path>` into `dist/<path>`; without this
|
|
90
|
+
* mount production answers the SPA shell for every dictionary and the runtime shows keys.
|
|
91
|
+
*/
|
|
92
|
+
function readTranslatorFolder(projectRoot, folderDeployed) {
|
|
93
|
+
const file = path.join(projectRoot, folderDeployed, 'sliceConfig.json');
|
|
94
|
+
if (!fs.existsSync(file)) return null;
|
|
95
|
+
try {
|
|
96
|
+
const translator = JSON.parse(fs.readFileSync(file, 'utf8')).translator;
|
|
97
|
+
if (!translator?.enabled) return null;
|
|
98
|
+
const value = typeof translator.path === 'string' && translator.path.trim() ? translator.path : '/locales';
|
|
99
|
+
const folder = value.replace(/^\/+|\/+$/g, '');
|
|
100
|
+
return folder && !folder.includes('..') ? folder : null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
56
106
|
/**
|
|
57
107
|
* True for `/Components/<Category>/<Name>/<Name>.css` when the component's `.js`
|
|
58
108
|
* exists but it has no stylesheet. JS-only Visual components (MultiRoute,
|
|
@@ -145,8 +195,16 @@ export function sliceFallback(options = {}) {
|
|
|
145
195
|
// With SEO configured the route set is known, so this is also where the
|
|
146
196
|
// 200-vs-404 decision happens.
|
|
147
197
|
if (seo) {
|
|
148
|
-
const
|
|
149
|
-
|
|
198
|
+
const result = seo.resolve(pathname);
|
|
199
|
+
if (result.location) {
|
|
200
|
+
const query = req.originalUrl.includes('?') ? req.originalUrl.slice(req.originalUrl.indexOf('?')) : '';
|
|
201
|
+
const location = result.location.includes('?') ? result.location : `${result.location}${query}`;
|
|
202
|
+
// A locale redirect depends on the current default locale and prefix mode; do not
|
|
203
|
+
// let a browser keep it for an hour after a config change.
|
|
204
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
205
|
+
return res.redirect(result.status, location);
|
|
206
|
+
}
|
|
207
|
+
return sendRendered(req, res, result.page, result.status);
|
|
150
208
|
}
|
|
151
209
|
|
|
152
210
|
return sendShell(req, res);
|
|
@@ -187,14 +245,24 @@ async function loadRouteDefinitions(projectRoot, folderDeployed) {
|
|
|
187
245
|
path.join(projectRoot, folderDeployed, 'routes.js'),
|
|
188
246
|
path.join(projectRoot, 'src', 'routes.js')
|
|
189
247
|
];
|
|
248
|
+
const normalize = (base, value) => {
|
|
249
|
+
const merged = `${base || ''}${value || ''}`.replace(/\/+/g, '/');
|
|
250
|
+
return merged.length > 1 ? merged.replace(/\/+$/, '') || '/' : '/';
|
|
251
|
+
};
|
|
252
|
+
const flatten = (list, base = '') => {
|
|
253
|
+
const entries = [];
|
|
254
|
+
for (const route of Array.isArray(list) ? list : []) {
|
|
255
|
+
const full = normalize(base, route?.path);
|
|
256
|
+
if (full) entries.push({ path: full, metadata: route?.metadata || {} });
|
|
257
|
+
if (Array.isArray(route?.children)) entries.push(...flatten(route.children, full));
|
|
258
|
+
}
|
|
259
|
+
return entries;
|
|
260
|
+
};
|
|
190
261
|
for (const candidate of candidates) {
|
|
191
262
|
if (!fs.existsSync(candidate)) continue;
|
|
192
263
|
const module = await import(pathToFileURL(candidate).href);
|
|
193
264
|
const routes = module.default ?? module.routes ?? [];
|
|
194
|
-
return (
|
|
195
|
-
path: String(route?.path ?? '').replace(/\/+$/, '') || '/',
|
|
196
|
-
metadata: route?.metadata || {}
|
|
197
|
-
})).filter((route) => route.path);
|
|
265
|
+
return flatten(routes).filter((route) => route.path);
|
|
198
266
|
}
|
|
199
267
|
return [];
|
|
200
268
|
}
|
|
@@ -244,15 +312,35 @@ export async function prepareSeo(options = {}) {
|
|
|
244
312
|
const siteName = config.siteName || manifest.defaults?.siteName || null;
|
|
245
313
|
const locale = config.locale || manifest.defaults?.locale || null;
|
|
246
314
|
const routes = await loadRouteDefinitions(projectRoot, folderDeployed);
|
|
315
|
+
if (!routes.length) {
|
|
316
|
+
throw new Error('seo: routes.js is missing or empty; every page would answer 404');
|
|
317
|
+
}
|
|
247
318
|
|
|
248
|
-
|
|
249
|
-
|
|
319
|
+
// Multilingual SEO (translator feature 0003, phase 5). Without a translator this is
|
|
320
|
+
// one `null` locale and every rule below collapses to the old single-language path.
|
|
321
|
+
const translatorConfig = config.translator || readTranslatorConfig(projectRoot, folderDeployed);
|
|
322
|
+
const routerConfig = readRouterConfig(projectRoot, folderDeployed);
|
|
323
|
+
const translator = buildTranslatorContext(
|
|
324
|
+
translatorConfig,
|
|
325
|
+
loadTranslatorDictionaries(projectRoot, folderDeployed, translatorConfig)
|
|
326
|
+
);
|
|
327
|
+
const locales = translator.enabled
|
|
328
|
+
? (translator.localePrefix ? translator.locales : [translator.defaultLocale])
|
|
329
|
+
: [null];
|
|
330
|
+
const ogLocaleConfig = manifest.locales && typeof manifest.locales === 'object' ? manifest.locales : {};
|
|
331
|
+
|
|
332
|
+
const render = (activeLocale, pathname, routeMetadata) => {
|
|
333
|
+
const translate = activeLocale
|
|
334
|
+
? (key, params) => translator.t(activeLocale, key, params)
|
|
335
|
+
: null;
|
|
336
|
+
const url = translator.enabled ? translator.localize(pathname, activeLocale) : pathname;
|
|
337
|
+
const meta = resolveMetadata({ pathname, routeMetadata, manifest, origin, translate, url, routerConfig });
|
|
250
338
|
|
|
251
339
|
// Precedence: an image declared on the route entry wins, then the generated
|
|
252
340
|
// card, then `defaults.image` (the generic banner). Comparing against the raw
|
|
253
341
|
// entry keeps the framework from guessing where the image came from.
|
|
254
342
|
const explicitImage = pickRouteEntry(pathname, manifest)?.entry?.image;
|
|
255
|
-
const card = cards[pathname];
|
|
343
|
+
const card = cards[url] || cards[pathname];
|
|
256
344
|
if (card && !explicitImage && !routeMetadata?.image) {
|
|
257
345
|
meta.image = /^https?:\/\//i.test(card.path) ? card.path : `${origin}${card.path}`;
|
|
258
346
|
meta.imageWidth = card.width ?? null;
|
|
@@ -261,7 +349,26 @@ export async function prepareSeo(options = {}) {
|
|
|
261
349
|
meta.imageAlt = meta.imageAlt || meta.title;
|
|
262
350
|
}
|
|
263
351
|
|
|
264
|
-
|
|
352
|
+
const alternates = translator.enabled && translator.localePrefix && activeLocale && translator.locales.length > 1
|
|
353
|
+
? translator.locales
|
|
354
|
+
.map((code) => ({
|
|
355
|
+
hreflang: code,
|
|
356
|
+
href: `${origin}${translator.localize(pathname, code)}`
|
|
357
|
+
}))
|
|
358
|
+
.concat([{
|
|
359
|
+
hreflang: 'x-default',
|
|
360
|
+
href: `${origin}${translator.localize(pathname, translator.defaultLocale)}`
|
|
361
|
+
}])
|
|
362
|
+
: null;
|
|
363
|
+
|
|
364
|
+
const ogLocale = translator.enabled && activeLocale
|
|
365
|
+
? (ogLocaleConfig[activeLocale]?.ogLocale || translator.ogLocale(activeLocale))
|
|
366
|
+
: locale;
|
|
367
|
+
|
|
368
|
+
const head = renderHead(meta, { siteName, locale: ogLocale, alternates });
|
|
369
|
+
return encodeSeoPage(injectHead(shell, head, translator.enabled && activeLocale
|
|
370
|
+
? { lang: activeLocale, dir: translator.direction(activeLocale) }
|
|
371
|
+
: {}));
|
|
265
372
|
};
|
|
266
373
|
|
|
267
374
|
const pages = new Map();
|
|
@@ -271,44 +378,100 @@ export async function prepareSeo(options = {}) {
|
|
|
271
378
|
patterns.push(route);
|
|
272
379
|
continue;
|
|
273
380
|
}
|
|
274
|
-
|
|
381
|
+
for (const activeLocale of locales) {
|
|
382
|
+
const url = translator.enabled ? translator.localize(route.path, activeLocale) : route.path;
|
|
383
|
+
pages.set(url, render(activeLocale, route.path, route.metadata));
|
|
384
|
+
}
|
|
275
385
|
}
|
|
276
386
|
// Longest pattern first so `/p/${id}/detail` wins over `/p/${id}`.
|
|
277
387
|
patterns.sort((a, b) => b.path.length - a.path.length);
|
|
278
388
|
|
|
279
|
-
const notFound =
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
)
|
|
389
|
+
const notFound = new Map();
|
|
390
|
+
const notFoundPrefix = typeof routerConfig.titlePrefix === 'string' ? routerConfig.titlePrefix : '';
|
|
391
|
+
const notFoundSuffix = typeof routerConfig.titleSuffix === 'string' ? routerConfig.titleSuffix : '';
|
|
392
|
+
for (const activeLocale of locales) {
|
|
393
|
+
const key = 'seo.notFound.title';
|
|
394
|
+
const translated = translator.enabled && activeLocale ? translator.t(activeLocale, key) : key;
|
|
395
|
+
const title = `${notFoundPrefix}${translated === key ? 'Not found' : translated}${notFoundSuffix}`;
|
|
396
|
+
const descriptionKey = 'seo.notFound.description';
|
|
397
|
+
const description = translator.enabled && activeLocale ? translator.t(activeLocale, descriptionKey) : descriptionKey;
|
|
398
|
+
const head = renderHead(
|
|
399
|
+
{
|
|
400
|
+
title,
|
|
401
|
+
description: description === descriptionKey ? null : description,
|
|
402
|
+
robots: 'noindex, nofollow'
|
|
403
|
+
},
|
|
404
|
+
{ locale: translator.enabled && activeLocale ? translator.ogLocale(activeLocale) : null }
|
|
405
|
+
);
|
|
406
|
+
notFound.set(activeLocale, encodeSeoPage(injectHead(
|
|
407
|
+
shell,
|
|
408
|
+
head,
|
|
409
|
+
translator.enabled && activeLocale
|
|
410
|
+
? { lang: activeLocale, dir: translator.direction(activeLocale) }
|
|
411
|
+
: {}
|
|
412
|
+
)));
|
|
413
|
+
}
|
|
283
414
|
|
|
284
415
|
// A `${param}` route cannot be enumerated, so it is rendered per path. The
|
|
285
416
|
// cache keeps repeated visits of the same product from recompressing.
|
|
286
417
|
const dynamicPages = new Map();
|
|
287
418
|
const DYNAMIC_LIMIT = 200;
|
|
288
419
|
|
|
420
|
+
/** The page for an app path in one locale: prerendered, cached dynamic, or 404. */
|
|
421
|
+
const settle = (appPath, activeLocale) => {
|
|
422
|
+
const url = translator.enabled ? translator.localize(appPath, activeLocale) : appPath;
|
|
423
|
+
const exact = pages.get(url);
|
|
424
|
+
if (exact) return { status: 200, page: exact };
|
|
425
|
+
|
|
426
|
+
const dynamic = dynamicPages.get(url);
|
|
427
|
+
if (dynamic) return { status: 200, page: dynamic };
|
|
428
|
+
|
|
429
|
+
for (const route of patterns) {
|
|
430
|
+
if (!matchPathPattern(route.path, appPath)) continue;
|
|
431
|
+
const page = render(activeLocale, appPath, route.metadata);
|
|
432
|
+
if (dynamicPages.size >= DYNAMIC_LIMIT) {
|
|
433
|
+
dynamicPages.delete(dynamicPages.keys().next().value);
|
|
434
|
+
}
|
|
435
|
+
dynamicPages.set(url, page);
|
|
436
|
+
return { status: 200, page };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return { status: 404, page: notFound.get(activeLocale) || notFound.get(null) };
|
|
440
|
+
};
|
|
441
|
+
|
|
289
442
|
return {
|
|
290
443
|
origin,
|
|
291
444
|
manifest,
|
|
292
445
|
routes,
|
|
446
|
+
translator,
|
|
293
447
|
prerendered: pages.size,
|
|
294
448
|
resolve(pathname) {
|
|
295
|
-
|
|
296
|
-
|
|
449
|
+
if (!translator.enabled || !translator.localePrefix) {
|
|
450
|
+
return settle(pathname, translator.defaultLocale);
|
|
451
|
+
}
|
|
297
452
|
|
|
298
|
-
const
|
|
299
|
-
if (dynamic) return { status: 200, page: dynamic };
|
|
453
|
+
const { locale: urlLocale, path: appPath } = translator.split(pathname);
|
|
300
454
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
dynamicPages.delete(dynamicPages.keys().next().value);
|
|
455
|
+
if (urlLocale) {
|
|
456
|
+
// `/es/pricing` under "prefix-except-default": the canonical URL is `/pricing`.
|
|
457
|
+
if (translator.localePrefix === 'except-default' && urlLocale === translator.defaultLocale) {
|
|
458
|
+
return { status: 301, location: appPath };
|
|
306
459
|
}
|
|
307
|
-
|
|
308
|
-
return { status: 200, page };
|
|
460
|
+
return settle(appPath, urlLocale);
|
|
309
461
|
}
|
|
310
462
|
|
|
311
|
-
|
|
463
|
+
// With "always" every page has a locale in the URL. `/` picks the default (the
|
|
464
|
+
// client may then apply the saved/browser choice). A known unprefixed path is a
|
|
465
|
+
// 302 to its prefixed URL, not a 404: the client can render it and search engines
|
|
466
|
+
// must see the canonical URL.
|
|
467
|
+
if (translator.localePrefix === 'always') {
|
|
468
|
+
if (pathname === '/' || settle(pathname, translator.defaultLocale).status === 200) {
|
|
469
|
+
return { status: 302, location: translator.localize(pathname, translator.defaultLocale) };
|
|
470
|
+
}
|
|
471
|
+
return { status: 404, page: notFound.get(translator.defaultLocale) };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return settle(pathname, translator.defaultLocale);
|
|
312
475
|
}
|
|
313
476
|
};
|
|
314
477
|
}
|
|
@@ -654,6 +817,19 @@ export function createSliceServer(options = {}) {
|
|
|
654
817
|
app.use('/dist/', express.static(path.join(projectRoot, 'dist')));
|
|
655
818
|
}
|
|
656
819
|
|
|
820
|
+
// Translator dictionaries (`translator.path`, `/locales` by default): app data copied
|
|
821
|
+
// into dist by `slice build`. Mounted in both modes so the path is explicit; without it
|
|
822
|
+
// production answered the SPA shell for every dictionary and the runtime showed keys.
|
|
823
|
+
const translatorFolder = readTranslatorFolder(projectRoot, folderDeployed);
|
|
824
|
+
if (translatorFolder) {
|
|
825
|
+
app.use(
|
|
826
|
+
`/${translatorFolder}`,
|
|
827
|
+
express.static(path.join(projectRoot, folderDeployed, translatorFolder), {
|
|
828
|
+
setHeaders: (res) => res.setHeader('Cache-Control', 'public, max-age=300, stale-while-revalidate=86400')
|
|
829
|
+
})
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
657
833
|
// ==============================================
|
|
658
834
|
// RUTAS DE API
|
|
659
835
|
// ==============================================
|
|
@@ -737,7 +913,8 @@ export function createSliceServer(options = {}) {
|
|
|
737
913
|
if (options.seo) {
|
|
738
914
|
seo = await prepareSeo({ ...options, port });
|
|
739
915
|
if (seo) {
|
|
740
|
-
|
|
916
|
+
const localeCount = seo.translator?.enabled ? seo.translator.locales.length : 0;
|
|
917
|
+
console.log(`🔎 SEO: ${seo.prerendered} pages prerendered${localeCount ? ` (${localeCount} locales)` : ''} (${seo.origin})`);
|
|
741
918
|
} else {
|
|
742
919
|
console.warn('[slice] seo options were given but no manifest was found; SEO is off.');
|
|
743
920
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slicejs-web-framework",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"./api/framework/express.js": "./api/framework/express.js",
|
|
32
32
|
"./api/framework/server.js": "./api/framework/server.js",
|
|
33
33
|
"./api/framework/seo.js": "./api/framework/seo.js",
|
|
34
|
+
"./api/framework/content.js": "./api/framework/content.js",
|
|
34
35
|
"./api/framework/devDepsOptimizer.js": "./api/framework/devDepsOptimizer.js"
|
|
35
36
|
},
|
|
36
37
|
"types": "./types/index.d.ts",
|
package/sliceConfig.schema.json
CHANGED
|
@@ -1,122 +1,150 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://slicejs.local/sliceConfig.schema.json",
|
|
4
|
-
"title": "Slice.js Config",
|
|
5
|
-
"type": "object",
|
|
6
|
-
"additionalProperties": true,
|
|
7
|
-
"properties": {
|
|
8
|
-
"debugger": {
|
|
9
|
-
"type": "object",
|
|
10
|
-
"additionalProperties": true,
|
|
11
|
-
"properties": {
|
|
12
|
-
"enabled": { "type": "boolean" },
|
|
13
|
-
"click": { "type": "string", "enum": ["right", "left"] },
|
|
14
|
-
"leakInspector": {
|
|
15
|
-
"type": "object",
|
|
16
|
-
"additionalProperties": true,
|
|
17
|
-
"description": "Dev-only memory-leak panel. Disabled in production regardless of this flag.",
|
|
18
|
-
"properties": {
|
|
19
|
-
"enabled": { "type": "boolean" },
|
|
20
|
-
"shortcut": { "type": "string" }
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
"stylesManager": {
|
|
26
|
-
"type": "object",
|
|
27
|
-
"additionalProperties": true,
|
|
28
|
-
"properties": {
|
|
29
|
-
"requestedStyles": { "type": "array", "items": { "type": "string" } }
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"themeManager": {
|
|
33
|
-
"type": "object",
|
|
34
|
-
"additionalProperties": true,
|
|
35
|
-
"properties": {
|
|
36
|
-
"enabled": { "type": "boolean" },
|
|
37
|
-
"defaultTheme": { "type": "string" },
|
|
38
|
-
"saveThemeLocally": { "type": "boolean" },
|
|
39
|
-
"useBrowserTheme": { "type": "boolean" }
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"logger": {
|
|
43
|
-
"type": "object",
|
|
44
|
-
"additionalProperties": true,
|
|
45
|
-
"properties": {
|
|
46
|
-
"enabled": { "type": "boolean" },
|
|
47
|
-
"showLogs": { "type": "object", "additionalProperties": true },
|
|
48
|
-
"ui": {
|
|
49
|
-
"type": "object",
|
|
50
|
-
"additionalProperties": true,
|
|
51
|
-
"properties": {
|
|
52
|
-
"enabled": { "type": "boolean" },
|
|
53
|
-
"shortcut": { "type": "string" }
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
"events": {
|
|
59
|
-
"type": "object",
|
|
60
|
-
"additionalProperties": true,
|
|
61
|
-
"properties": {
|
|
62
|
-
"enabled": { "type": "boolean" },
|
|
63
|
-
"ui": {
|
|
64
|
-
"type": "object",
|
|
65
|
-
"additionalProperties": true,
|
|
66
|
-
"properties": {
|
|
67
|
-
"enabled": { "type": "boolean" },
|
|
68
|
-
"shortcut": { "type": "string" }
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
"context": {
|
|
74
|
-
"type": "object",
|
|
75
|
-
"additionalProperties": true,
|
|
76
|
-
"properties": {
|
|
77
|
-
"enabled": { "type": "boolean" },
|
|
78
|
-
"ui": {
|
|
79
|
-
"type": "object",
|
|
80
|
-
"additionalProperties": true,
|
|
81
|
-
"properties": {
|
|
82
|
-
"enabled": { "type": "boolean" },
|
|
83
|
-
"shortcut": { "type": "string" }
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
"paths": {
|
|
89
|
-
"type": "object",
|
|
90
|
-
"additionalProperties": true,
|
|
91
|
-
"properties": {
|
|
92
|
-
"components": { "type": "object", "additionalProperties": true },
|
|
93
|
-
"themes": { "type": "string" },
|
|
94
|
-
"styles": { "type": "string" },
|
|
95
|
-
"routesFile": { "type": "string" }
|
|
96
|
-
},
|
|
97
|
-
"required": ["components", "themes", "styles", "routesFile"]
|
|
98
|
-
},
|
|
99
|
-
"router": {
|
|
100
|
-
"type": "object",
|
|
101
|
-
"additionalProperties": true,
|
|
102
|
-
"properties": {
|
|
103
|
-
"defaultRoute": { "type": "string" }
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://slicejs.local/sliceConfig.schema.json",
|
|
4
|
+
"title": "Slice.js Config",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": true,
|
|
7
|
+
"properties": {
|
|
8
|
+
"debugger": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": true,
|
|
11
|
+
"properties": {
|
|
12
|
+
"enabled": { "type": "boolean" },
|
|
13
|
+
"click": { "type": "string", "enum": ["right", "left"] },
|
|
14
|
+
"leakInspector": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": true,
|
|
17
|
+
"description": "Dev-only memory-leak panel. Disabled in production regardless of this flag.",
|
|
18
|
+
"properties": {
|
|
19
|
+
"enabled": { "type": "boolean" },
|
|
20
|
+
"shortcut": { "type": "string" }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"stylesManager": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": true,
|
|
28
|
+
"properties": {
|
|
29
|
+
"requestedStyles": { "type": "array", "items": { "type": "string" } }
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"themeManager": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"additionalProperties": true,
|
|
35
|
+
"properties": {
|
|
36
|
+
"enabled": { "type": "boolean" },
|
|
37
|
+
"defaultTheme": { "type": "string" },
|
|
38
|
+
"saveThemeLocally": { "type": "boolean" },
|
|
39
|
+
"useBrowserTheme": { "type": "boolean" }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"logger": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": true,
|
|
45
|
+
"properties": {
|
|
46
|
+
"enabled": { "type": "boolean" },
|
|
47
|
+
"showLogs": { "type": "object", "additionalProperties": true },
|
|
48
|
+
"ui": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": true,
|
|
51
|
+
"properties": {
|
|
52
|
+
"enabled": { "type": "boolean" },
|
|
53
|
+
"shortcut": { "type": "string" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"events": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"additionalProperties": true,
|
|
61
|
+
"properties": {
|
|
62
|
+
"enabled": { "type": "boolean" },
|
|
63
|
+
"ui": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"additionalProperties": true,
|
|
66
|
+
"properties": {
|
|
67
|
+
"enabled": { "type": "boolean" },
|
|
68
|
+
"shortcut": { "type": "string" }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"context": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"additionalProperties": true,
|
|
76
|
+
"properties": {
|
|
77
|
+
"enabled": { "type": "boolean" },
|
|
78
|
+
"ui": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"additionalProperties": true,
|
|
81
|
+
"properties": {
|
|
82
|
+
"enabled": { "type": "boolean" },
|
|
83
|
+
"shortcut": { "type": "string" }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"paths": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"additionalProperties": true,
|
|
91
|
+
"properties": {
|
|
92
|
+
"components": { "type": "object", "additionalProperties": true },
|
|
93
|
+
"themes": { "type": "string" },
|
|
94
|
+
"styles": { "type": "string" },
|
|
95
|
+
"routesFile": { "type": "string" }
|
|
96
|
+
},
|
|
97
|
+
"required": ["components", "themes", "styles", "routesFile"]
|
|
98
|
+
},
|
|
99
|
+
"router": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"additionalProperties": true,
|
|
102
|
+
"properties": {
|
|
103
|
+
"defaultRoute": { "type": "string" },
|
|
104
|
+
"updateTitles": { "type": "boolean", "description": "Write document.title and the description from route metadata. Default true." },
|
|
105
|
+
"titlePrefix": { "type": "string", "description": "Text before every route title. The separator is part of the string (\"Acme · \")." },
|
|
106
|
+
"titleSuffix": { "type": "string", "description": "Text after every route title. The separator is part of the string (\" · Acme\")." }
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"translator": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"additionalProperties": false,
|
|
112
|
+
"description": "slice.translator: dictionaries in <path>/<locale>.json, data-t in templates, slice.t() for text and slice.t.ref() for props.",
|
|
113
|
+
"properties": {
|
|
114
|
+
"enabled": { "type": "boolean", "description": "Off: slice.t() returns the key and nothing is loaded." },
|
|
115
|
+
"defaultLocale": { "type": "string", "minLength": 1 },
|
|
116
|
+
"fallbackLocale": { "type": ["string", "null"], "description": "Asked when the active locale lacks a key." },
|
|
117
|
+
"locales": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
|
|
118
|
+
"path": { "type": "string", "description": "Where dictionaries are served from. Default /locales." },
|
|
119
|
+
"saveLocally": { "type": "boolean", "description": "Remember the user's choice in localStorage." },
|
|
120
|
+
"useBrowserLocale": { "type": "boolean", "description": "First visit: the browser's language when supported." },
|
|
121
|
+
"localePrefix": {
|
|
122
|
+
"enum": [false, "always", "except-default"],
|
|
123
|
+
"description": "Locale in the URL. false keeps it out; always puts it in every URL; except-default leaves the default locale unprefixed."
|
|
124
|
+
},
|
|
125
|
+
"ui": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"additionalProperties": true,
|
|
128
|
+
"properties": { "enabled": { "type": "boolean" }, "shortcut": { "type": "string" } }
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"if": { "properties": { "enabled": { "const": true } }, "required": ["enabled"] },
|
|
132
|
+
"then": { "required": ["defaultLocale"] }
|
|
133
|
+
},
|
|
134
|
+
"loading": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"additionalProperties": true,
|
|
137
|
+
"properties": {
|
|
138
|
+
"enabled": { "type": "boolean" }
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"server": {
|
|
142
|
+
"type": "object",
|
|
143
|
+
"additionalProperties": true,
|
|
144
|
+
"properties": {
|
|
145
|
+
"port": { "type": "number" },
|
|
146
|
+
"host": { "type": "string" }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|