@webjsdev/server 0.8.63 → 0.8.64
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/index.d.ts +7 -2
- package/package.json +2 -2
- package/src/browser-entries.js +64 -0
- package/src/check/helpers.js +365 -0
- package/src/check/rules-actions.js +174 -0
- package/src/check/rules-components.js +275 -0
- package/src/check/rules-imports.js +166 -0
- package/src/check/rules-registry.js +128 -0
- package/src/check/rules-routing.js +224 -0
- package/src/check/rules-typescript.js +177 -0
- package/src/check/rules.js +127 -0
- package/src/check/runner-support.js +405 -0
- package/src/check/runner.js +113 -0
- package/src/check.js +4 -1774
- package/src/component-elision.js +1 -1
- package/src/dev/config.js +338 -0
- package/src/dev/handler.js +1372 -0
- package/src/dev/helpers.js +542 -0
- package/src/dev/serve.js +840 -0
- package/src/dev/server.js +322 -0
- package/src/dev-classify.js +174 -0
- package/src/dev-reload-worker.js +133 -7
- package/src/dev-styles.js +97 -0
- package/src/dev.js +22 -3077
- package/src/form-dispatch.js +12 -3
- package/src/importmap.js +6 -3
- package/src/listener-core.js +16 -2
- package/src/module-graph.js +112 -7
- package/src/router.js +11 -1
- package/src/ssr/client-router-flag.js +42 -0
- package/src/ssr/document.js +361 -0
- package/src/ssr/env-shim.js +51 -0
- package/src/ssr/escape.js +29 -0
- package/src/ssr/head.js +857 -0
- package/src/ssr/preloads.js +195 -0
- package/src/ssr/render.js +1481 -0
- package/src/ssr/responses.js +201 -0
- package/src/ssr.js +28 -2445
- package/src/vendor/audit.js +400 -0
- package/src/vendor/integrity.js +245 -0
- package/src/vendor/jspm.js +348 -0
- package/src/vendor/manifest.js +96 -0
- package/src/vendor/pins.js +892 -0
- package/src/vendor/providers.js +35 -0
- package/src/vendor/resolver.js +225 -0
- package/src/vendor/scanner.js +132 -0
- package/src/vendor.js +29 -2371
package/src/form-dispatch.js
CHANGED
|
@@ -561,13 +561,22 @@ export async function runFormAction(route, params, url, req, ssrOpts, deps) {
|
|
|
561
561
|
// the SUCCESS-result path below.
|
|
562
562
|
return new Response(null, { status: e.status || 307, headers: { location: e.url } });
|
|
563
563
|
}
|
|
564
|
+
// `route` and `params` ride along so the boundary renders inside its
|
|
565
|
+
// layouts, exactly as it does on the page-render path (#1298).
|
|
564
566
|
if (isNotFound(err)) {
|
|
565
|
-
|
|
567
|
+
// NEAREST-wins, like the page-render path (#848). `ssrOpts.notFoundFile`
|
|
568
|
+
// is the ROOT one only, so a nested form action throwing notFound()
|
|
569
|
+
// rendered the root boundary while the same throw from the page render
|
|
570
|
+
// rendered the nested one. Two paths, one url, different 404s.
|
|
571
|
+
const nearestNotFound = route.notFounds && route.notFounds.length
|
|
572
|
+
? route.notFounds[route.notFounds.length - 1]
|
|
573
|
+
: (ssrOpts.notFoundFile ?? null);
|
|
574
|
+
return ssrNotFound(nearestNotFound, { ...ssrOpts, req, url, route, params });
|
|
566
575
|
}
|
|
567
576
|
// forbidden()/unauthorized() from a form action render the same 403/401
|
|
568
577
|
// boundary as the page-render path (#848), not a generic 500.
|
|
569
|
-
if (isForbidden(err)) return ssrForbidden(route, { ...ssrOpts, req, url });
|
|
570
|
-
if (isUnauthorized(err)) return ssrUnauthorized(route, { ...ssrOpts, req, url });
|
|
578
|
+
if (isForbidden(err)) return ssrForbidden(route, { ...ssrOpts, req, url, route, params });
|
|
579
|
+
if (isUnauthorized(err)) return ssrUnauthorized(route, { ...ssrOpts, req, url, route, params });
|
|
571
580
|
if (typeof onError === 'function') onError(err);
|
|
572
581
|
return await formActionErrorResponse(err, ssrOpts.dev);
|
|
573
582
|
}
|
package/src/importmap.js
CHANGED
|
@@ -6,9 +6,12 @@ import { jsonForScriptTag } from './script-tag-json.js';
|
|
|
6
6
|
import { withBasePath } from './base-path.js';
|
|
7
7
|
import { withAssetHash } from './asset-hash.js';
|
|
8
8
|
|
|
9
|
-
// Local attribute escaper
|
|
10
|
-
// of truth for HTML attribute escaping in this package
|
|
11
|
-
// to avoid a cross-file dependency for one small helper
|
|
9
|
+
// Local attribute escaper, byte-identical to the pair in `ssr/escape.js`,
|
|
10
|
+
// which is the source of truth for HTML attribute escaping in this package.
|
|
11
|
+
// Kept inline to avoid a cross-file dependency for one small helper, so the
|
|
12
|
+
// two must stay in step: this one escapes an attribute in the served head
|
|
13
|
+
// (the importmap script's nonce), so a divergence here moves served bytes
|
|
14
|
+
// exactly as a divergence there would.
|
|
12
15
|
function escapeAttr(s) {
|
|
13
16
|
return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
14
17
|
}
|
package/src/listener-core.js
CHANGED
|
@@ -365,8 +365,22 @@ export class SseHub {
|
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
/**
|
|
369
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Push a live-reload event to every open tab.
|
|
370
|
+
*
|
|
371
|
+
* The frame carries the change's `{ v, by, why }` classification (#1398) as a
|
|
372
|
+
* single-line JSON payload, matching `devError` below. The browser relay
|
|
373
|
+
* resolves ANY payload it cannot read to `reload`, and an absent or malformed
|
|
374
|
+
* verdict is emitted as `reload` here too, so the fail-safe holds on both ends
|
|
375
|
+
* of the wire and a tab running against a restarted server can never be
|
|
376
|
+
* skewed into a lighter response than the change deserves.
|
|
377
|
+
*
|
|
378
|
+
* @param {{ v?: string, by?: string, why?: string } | null} [verdict]
|
|
379
|
+
*/
|
|
380
|
+
reload(verdict) {
|
|
381
|
+
const v = verdict && typeof verdict.v === 'string' ? verdict : { v: 'reload' };
|
|
382
|
+
this._raw(`event: reload\ndata: ${JSON.stringify(v)}\n\n`);
|
|
383
|
+
}
|
|
370
384
|
|
|
371
385
|
/** Push a dev-error overlay frame (#264) to every open tab. @param {object} frame */
|
|
372
386
|
devError(frame) { this._raw(`event: webjs-error\ndata: ${JSON.stringify(frame)}\n\n`); }
|
package/src/module-graph.js
CHANGED
|
@@ -174,6 +174,28 @@ export function bareImports(graph) {
|
|
|
174
174
|
return BARE_EDGES.get(graph) || new Map();
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Bare (npm vendor) specifiers imported DYNAMICALLY (`await import('dayjs')`),
|
|
179
|
+
* kept apart from `BARE_EDGES` for the same reason `DYNAMIC_EDGES` is kept
|
|
180
|
+
* apart from the static graph: a lazily-imported vendor must RESOLVE when the
|
|
181
|
+
* import finally runs, so it belongs in the importmap, but it must not be
|
|
182
|
+
* preloaded (that would fetch a bundle the author deliberately deferred). So
|
|
183
|
+
* `reachableBareSpecifiers` (the importmap) unions both maps while
|
|
184
|
+
* `bareImports` (the #754 modulepreload source) stays static-only.
|
|
185
|
+
* @type {WeakMap<ModuleGraph, Map<string, Set<string>>>}
|
|
186
|
+
*/
|
|
187
|
+
const BARE_DYNAMIC_EDGES = new WeakMap();
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The dynamically-imported bare specifiers per file for a built graph, or an
|
|
191
|
+
* empty map if none.
|
|
192
|
+
* @param {ModuleGraph} graph
|
|
193
|
+
* @returns {Map<string, Set<string>>}
|
|
194
|
+
*/
|
|
195
|
+
export function dynamicBareImports(graph) {
|
|
196
|
+
return BARE_DYNAMIC_EDGES.get(graph) || new Map();
|
|
197
|
+
}
|
|
198
|
+
|
|
177
199
|
/**
|
|
178
200
|
* Every app source file walked while building a graph (#899). `graph` itself
|
|
179
201
|
* holds only files that HAVE imports, so a leaf (e.g. a no-import `.server.ts`)
|
|
@@ -224,12 +246,16 @@ export async function buildModuleGraph(appDir) {
|
|
|
224
246
|
const dynamic = new Map();
|
|
225
247
|
/** @type {Map<string, Set<string>>} bare vendor specifiers per file (#754) */
|
|
226
248
|
const bare = new Map();
|
|
249
|
+
/** @type {Map<string, Set<string>>} DYNAMICALLY-imported bare vendor specifiers
|
|
250
|
+
* per file: importmap-bound but never preloaded (#1399) */
|
|
251
|
+
const bareDynamic = new Map();
|
|
227
252
|
/** @type {Set<string>} every file walked this build (graph holds only files
|
|
228
253
|
* with deps, so a separate set is needed to know what is still live). */
|
|
229
254
|
const seen = new Set();
|
|
230
|
-
await walk(appDir, appDir, graph, seen, dynamic, bare);
|
|
255
|
+
await walk(appDir, appDir, graph, seen, dynamic, bare, bareDynamic);
|
|
231
256
|
if (dynamic.size) DYNAMIC_EDGES.set(graph, dynamic);
|
|
232
257
|
if (bare.size) BARE_EDGES.set(graph, bare);
|
|
258
|
+
if (bareDynamic.size) BARE_DYNAMIC_EDGES.set(graph, bareDynamic);
|
|
233
259
|
if (seen.size) SEEN_FILES.set(graph, seen);
|
|
234
260
|
// Evict parse-cache entries for files no longer in the tree (a rebuild after
|
|
235
261
|
// a rename or delete), so a long dev session does not accumulate dead
|
|
@@ -374,13 +400,82 @@ export function reachableFromEntries(graph, entryFiles, appDir) {
|
|
|
374
400
|
return visited;
|
|
375
401
|
}
|
|
376
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Bare npm vendor specifiers reachable from `entryFiles`, the importmap
|
|
405
|
+
* analogue of ssr.js's per-page `reachedVendorSpecifiers` (#754).
|
|
406
|
+
*
|
|
407
|
+
* Walks the SAME edges the authorization gate walks (static + dynamic, #751,
|
|
408
|
+
* stopping at `.server.*` boundaries) so the answer is exactly "what a browser
|
|
409
|
+
* could load from these entries", then unions each reached file's recorded
|
|
410
|
+
* bare edges, static AND dynamic. Both dynamic forms are included here even
|
|
411
|
+
* though `transitiveDeps` (preload) ignores them, for the same reason: a
|
|
412
|
+
* lazily imported module is not preloaded, but its bare specifier still has to
|
|
413
|
+
* resolve when the import finally runs, so it belongs in the importmap. That
|
|
414
|
+
* is why a dynamically-imported VENDOR gets its own map (`dynamicBareImports`)
|
|
415
|
+
* rather than joining `bareImports`, which feeds the preload hints.
|
|
416
|
+
*
|
|
417
|
+
* `.server.*` files are reached (the browser fetches a stub at that URL) but
|
|
418
|
+
* their bare imports are NOT collected: the source never ships, so a DB driver
|
|
419
|
+
* imported by a server file must never enter the importmap.
|
|
420
|
+
*
|
|
421
|
+
* `skip` files are neither collected nor traversed into, so a specifier
|
|
422
|
+
* reachable ONLY through an elided component drops out along with the subtree
|
|
423
|
+
* behind it (#197 / #170). Omit `skip` for the un-pruned superset the pin path
|
|
424
|
+
* needs.
|
|
425
|
+
*
|
|
426
|
+
* @param {ModuleGraph} graph
|
|
427
|
+
* @param {string[]} entryFiles absolute paths
|
|
428
|
+
* @param {string} appDir
|
|
429
|
+
* @param {Set<string>} [skip] absolute paths to exclude and not traverse
|
|
430
|
+
* @returns {Set<string>}
|
|
431
|
+
*/
|
|
432
|
+
export function reachableBareSpecifiers(graph, entryFiles, appDir, skip) {
|
|
433
|
+
/** @type {Set<string>} */
|
|
434
|
+
const specs = new Set();
|
|
435
|
+
const bare = BARE_EDGES.get(graph);
|
|
436
|
+
const bareDynamic = BARE_DYNAMIC_EDGES.get(graph);
|
|
437
|
+
if (!bare?.size && !bareDynamic?.size) return specs;
|
|
438
|
+
const dynamic = DYNAMIC_EDGES.get(graph);
|
|
439
|
+
/** @type {Set<string>} */
|
|
440
|
+
const visited = new Set();
|
|
441
|
+
/** @type {string[]} */
|
|
442
|
+
const queue = [];
|
|
443
|
+
for (const entry of entryFiles) {
|
|
444
|
+
if (!entry || !entry.startsWith(appDir)) continue;
|
|
445
|
+
if (skip && skip.has(entry)) continue;
|
|
446
|
+
visited.add(entry);
|
|
447
|
+
queue.push(entry);
|
|
448
|
+
}
|
|
449
|
+
while (queue.length) {
|
|
450
|
+
const file = /** @type {string} */ (queue.shift());
|
|
451
|
+
if (SERVER_FILE_RE.test(file)) continue;
|
|
452
|
+
for (const map of [bare, bareDynamic]) {
|
|
453
|
+
const fileBare = map?.get(file);
|
|
454
|
+
if (fileBare) for (const spec of fileBare) specs.add(spec);
|
|
455
|
+
}
|
|
456
|
+
const staticDeps = graph.get(file);
|
|
457
|
+
const dynDeps = dynamic && dynamic.get(file);
|
|
458
|
+
for (const set of [staticDeps, dynDeps]) {
|
|
459
|
+
if (!set) continue;
|
|
460
|
+
for (const dep of set) {
|
|
461
|
+
if (visited.has(dep)) continue;
|
|
462
|
+
if (!dep.startsWith(appDir)) continue;
|
|
463
|
+
if (skip && skip.has(dep)) continue;
|
|
464
|
+
visited.add(dep);
|
|
465
|
+
queue.push(dep);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return specs;
|
|
470
|
+
}
|
|
471
|
+
|
|
377
472
|
/**
|
|
378
473
|
* Recursively walk a directory, parse imports, and populate the graph.
|
|
379
474
|
* @param {string} dir
|
|
380
475
|
* @param {string} appDir
|
|
381
476
|
* @param {ModuleGraph} graph
|
|
382
477
|
*/
|
|
383
|
-
async function walk(dir, appDir, graph, seen, dynamic, bare) {
|
|
478
|
+
async function walk(dir, appDir, graph, seen, dynamic, bare, bareDynamic) {
|
|
384
479
|
let entries;
|
|
385
480
|
try { entries = await readdir(dir, { withFileTypes: true }); }
|
|
386
481
|
catch { return; }
|
|
@@ -398,9 +493,9 @@ async function walk(dir, appDir, graph, seen, dynamic, bare) {
|
|
|
398
493
|
if (e.name.startsWith('.')) continue;
|
|
399
494
|
const full = join(dir, e.name);
|
|
400
495
|
if (e.isDirectory()) {
|
|
401
|
-
await walk(full, appDir, graph, seen, dynamic, bare);
|
|
496
|
+
await walk(full, appDir, graph, seen, dynamic, bare, bareDynamic);
|
|
402
497
|
} else if (/\.(js|ts|mjs|mts)$/.test(e.name)) {
|
|
403
|
-
await parseFile(full, appDir, graph, seen, dynamic, bare);
|
|
498
|
+
await parseFile(full, appDir, graph, seen, dynamic, bare, bareDynamic);
|
|
404
499
|
}
|
|
405
500
|
}
|
|
406
501
|
}
|
|
@@ -428,7 +523,7 @@ export function _parseCacheHas(file) { return PARSE_CACHE.has(file); }
|
|
|
428
523
|
* @param {string} appDir
|
|
429
524
|
* @param {ModuleGraph} graph
|
|
430
525
|
*/
|
|
431
|
-
async function parseFile(file, appDir, graph, seen, dynamic, bare) {
|
|
526
|
+
async function parseFile(file, appDir, graph, seen, dynamic, bare, bareDynamic) {
|
|
432
527
|
let mtimeMs, size;
|
|
433
528
|
try { const st = await stat(file); mtimeMs = st.mtimeMs; size = st.size; }
|
|
434
529
|
catch { return; }
|
|
@@ -438,6 +533,7 @@ async function parseFile(file, appDir, graph, seen, dynamic, bare) {
|
|
|
438
533
|
if (cached.deps.size) graph.set(file, cached.deps);
|
|
439
534
|
if (cached.dynDeps && cached.dynDeps.size && dynamic) dynamic.set(file, cached.dynDeps);
|
|
440
535
|
if (cached.bareDeps && cached.bareDeps.size && bare) bare.set(file, cached.bareDeps);
|
|
536
|
+
if (cached.bareDynDeps && cached.bareDynDeps.size && bareDynamic) bareDynamic.set(file, cached.bareDynDeps);
|
|
441
537
|
return;
|
|
442
538
|
}
|
|
443
539
|
|
|
@@ -541,19 +637,28 @@ async function parseFile(file, appDir, graph, seen, dynamic, bare) {
|
|
|
541
637
|
// `,`/`)`), preserving the documented limitation.
|
|
542
638
|
const { redacted: holeAware, literals } = redactToPlaceholders(src);
|
|
543
639
|
const dynDeps = new Set();
|
|
640
|
+
const bareDynDeps = new Set();
|
|
544
641
|
for (const m of holeAware.matchAll(DYNAMIC_IMPORT_RE)) {
|
|
545
642
|
const token = m[1];
|
|
546
643
|
const pm = /^__STR_(\d+)__$/.exec(token);
|
|
547
644
|
const spec = pm ? literals[Number(pm[1])] : token;
|
|
548
645
|
if (spec == null) continue;
|
|
549
|
-
if (!spec.startsWith('.') && !spec.startsWith('/') && !expandImportAlias(spec, appDir))
|
|
646
|
+
if (!spec.startsWith('.') && !spec.startsWith('/') && !expandImportAlias(spec, appDir)) {
|
|
647
|
+
// A dynamically-imported VENDOR (`await import('dayjs')`). Not a graph
|
|
648
|
+
// edge (there is no local file to serve) and not a preload (lazy by
|
|
649
|
+
// intent), but it still needs an importmap entry or the import fails to
|
|
650
|
+
// resolve when it runs. Recorded in its own map for that one consumer.
|
|
651
|
+
if (isVendorSpecifier(spec)) bareDynDeps.add(spec);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
550
654
|
const resolved = resolveImport(spec, file, appDir);
|
|
551
655
|
if (resolved && !deps.has(resolved)) dynDeps.add(resolved);
|
|
552
656
|
}
|
|
553
|
-
PARSE_CACHE.set(file, { mtimeMs, size, deps, dynDeps, bareDeps });
|
|
657
|
+
PARSE_CACHE.set(file, { mtimeMs, size, deps, dynDeps, bareDeps, bareDynDeps });
|
|
554
658
|
if (deps.size) graph.set(file, deps);
|
|
555
659
|
if (dynDeps.size && dynamic) dynamic.set(file, dynDeps);
|
|
556
660
|
if (bareDeps.size && bare) bare.set(file, bareDeps);
|
|
661
|
+
if (bareDynDeps.size && bareDynamic) bareDynamic.set(file, bareDynDeps);
|
|
557
662
|
}
|
|
558
663
|
|
|
559
664
|
/**
|
package/src/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, relative, sep, posix } from 'node:path';
|
|
2
2
|
import { walk } from './fs-walk.js';
|
|
3
|
+
import { findInstrumentationClient } from './instrumentation.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {{
|
|
@@ -35,6 +36,7 @@ import { walk } from './fs-walk.js';
|
|
|
35
36
|
* globalError: string | null,
|
|
36
37
|
* globalNotFound: string | null,
|
|
37
38
|
* metadataRoutes: MetadataRoute[],
|
|
39
|
+
* instrumentationClient: string | null,
|
|
38
40
|
* appDir: string
|
|
39
41
|
* }} RouteTable
|
|
40
42
|
*/
|
|
@@ -185,7 +187,15 @@ export async function buildRouteTable(appDir) {
|
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
pages.sort(compareSpecificity);
|
|
188
|
-
|
|
190
|
+
// `instrumentation-client.*` is an app-ROOT convention file, not a router
|
|
191
|
+
// stem, but it is a browser-bound entry (the boot imports it first), so the
|
|
192
|
+
// table is where every consumer already looks for one. Resolved HERE rather
|
|
193
|
+
// than by each caller: it used to be attached by the dev server alone, and a
|
|
194
|
+
// second consumer that built its own table (the vendor scan) silently lost
|
|
195
|
+
// the entry, which inverted the pin-is-a-superset invariant. Deriving it in
|
|
196
|
+
// the one place that builds the table makes that class of miss impossible.
|
|
197
|
+
const instrumentationClient = await findInstrumentationClient(appDir);
|
|
198
|
+
return { pages, apis, notFound, notFounds, globalError, globalNotFound, metadataRoutes, instrumentationClient, appDir };
|
|
189
199
|
}
|
|
190
200
|
|
|
191
201
|
/**
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app-wide client-router opt-out flag (#629).
|
|
3
|
+
*
|
|
4
|
+
* Default ON (the framework's automatic-nav thesis): the router auto-enables in
|
|
5
|
+
* the browser when `@webjsdev/core` loads. `webjs.clientRouter: false` flips it
|
|
6
|
+
* off app-wide; the dev server reads the config at boot and on each rebuild and
|
|
7
|
+
* calls `setClientRouterEnabled`, and `wrapHead` then emits a
|
|
8
|
+
* `window.__WEBJS_CLIENT_ROUTER__=false` flag BEFORE the deferred boot module
|
|
9
|
+
* so the bundle's module-end auto-enable skips.
|
|
10
|
+
*
|
|
11
|
+
* A module-level switch (mirroring `setBasePath` / `setElisionFingerprint`) so
|
|
12
|
+
* no opt has to thread through every render path. Default true keeps every
|
|
13
|
+
* existing app and test byte-identical.
|
|
14
|
+
*
|
|
15
|
+
* It sits in its own module rather than beside its readers because it has TWO
|
|
16
|
+
* of them, in `head.js` and `render.js`, and parking it in either made the
|
|
17
|
+
* other import across what was otherwise a one-way edge. That was the second
|
|
18
|
+
* half of the ssr import cycle. The rule the pre-split file stated, that module
|
|
19
|
+
* state belongs with the code that uses and writes it, is unchanged for state
|
|
20
|
+
* with a single reader: `_metadataIconRoutes` still lives in `head.js`, whose
|
|
21
|
+
* `wrapHead` is the only thing that reads it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
let _clientRouterEnabled = true;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Set the app-wide client-router flag.
|
|
28
|
+
*
|
|
29
|
+
* @param {boolean} enabled anything but `false` enables
|
|
30
|
+
*/
|
|
31
|
+
export function setClientRouterEnabled(enabled) {
|
|
32
|
+
_clientRouterEnabled = enabled !== false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether the client router is enabled app-wide.
|
|
37
|
+
*
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
export function clientRouterEnabled() {
|
|
41
|
+
return _clientRouterEnabled;
|
|
42
|
+
}
|