@aegisjsproject/atlas 0.0.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/atlas.cjs ADDED
@@ -0,0 +1,669 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @type {Map<URLPattern, string>}
5
+ */
6
+ const reg = new Map();
7
+
8
+ /**
9
+ * @typedef RouteMatch
10
+ * @property {URLPatternResult|null} result The results of `pattern.exec(url)`
11
+ * @property {string|null} specifier The module specifier mapped to the URL
12
+ * @property {boolean} hasRegExpGroups
13
+ * @readonly
14
+ */
15
+
16
+ /**
17
+ * @type RouteMatch
18
+ */
19
+ const invalidMatchResult = Object.freeze({ result: null, specifier: null, hasRegExpGroups: false });
20
+
21
+ /**
22
+ * Finds the URLPattern that corresponds to the given URL
23
+ *
24
+ * @param {string} url
25
+ * @returns {URLPattern|undefined}
26
+ */
27
+ const getRegistryKey = url => reg.keys().find(pattern => pattern.test(url));
28
+
29
+ /**
30
+ *
31
+ * @param {URLPattern} key
32
+ * @returns {string|null} The module specifier
33
+ */
34
+ const getRegistrySpecifier = key => reg.get(key);
35
+
36
+ /**
37
+ *
38
+ * @param {string} url
39
+ * @returns {RouteMatch}
40
+ */
41
+ function lookupRoute(url) {
42
+ const key = getRegistryKey(url);
43
+
44
+ if (key instanceof URLPattern) {
45
+ return Object.freeze({
46
+ result: key.exec(url),
47
+ specifier: reg.get(key),
48
+ hasRegExpGroups: key.hasRegExpGroups,
49
+ });
50
+ } else {
51
+ return invalidMatchResult;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Registers module `specifier` to handle routes matching `pattern`
57
+ *
58
+ * @param {string|URLPattern} pattern The pattern to handle
59
+ * @param {string|URL} specifier The module to register to the pattern
60
+ */
61
+ function registerModule(pattern, specifier) {
62
+ if (typeof specifier !== 'string' && ! (specifier instanceof URL)) {
63
+ throw new TypeError(`Invalid specifier type ${typeof specifier}.`);
64
+ } else if (typeof pattern === 'string') {
65
+ reg.set(
66
+ URL.canParse(pattern) ? new URLPattern(pattern) : new URLPattern({ pathname: pattern }),
67
+ specifier.toString()
68
+ );
69
+ } else if (! (pattern instanceof URLPattern)) {
70
+ throw new TypeError(`Invalid pattner "${pattern}".`);
71
+ } else if (specifier instanceof URL) {
72
+ reg.set(pattern, specifier.href);
73
+ } else {
74
+ reg.set(pattern, specifier);
75
+ }
76
+ }
77
+
78
+ function _loadLink(href, {
79
+ relList = [],
80
+ crossOrigin = 'anonymous',
81
+ referrerPolicy = 'no-referrer',
82
+ fetchPriority = 'auto',
83
+ signal: passedSignal,
84
+ as,
85
+ integrity,
86
+ media,
87
+ type,
88
+ } = {}) {
89
+ const { promise, resolve, reject } = Promise.withResolvers();
90
+ const link = document.createElement('link');
91
+
92
+ if (passedSignal instanceof AbortSignal && passedSignal.aborted) {
93
+ reject(passedSignal.reason);
94
+ } else if (typeof href !== 'string' && ! (href instanceof URL)) {
95
+ reject(new TypeError(`Invalid href to preload: "${href}.`));
96
+ } else {
97
+ link.relList.add(...relList);
98
+
99
+ if (typeof fetchPriority === 'string') {
100
+ link.fetchPriority = fetchPriority;
101
+ }
102
+
103
+ if (typeof crossOrigin === 'string') {
104
+ link.crossOrigin = crossOrigin;
105
+ }
106
+
107
+ if (typeof type === 'string') {
108
+ link.type = type;
109
+ }
110
+
111
+ if (typeof media === 'string') {
112
+ link.media = media;
113
+ } else if (media instanceof MediaQueryList) {
114
+ link.media = media.media;
115
+ }
116
+
117
+ if (typeof as === 'string') {
118
+ link.as = as;
119
+ }
120
+
121
+ if (typeof integrity === 'string') {
122
+ link.integrity = integrity;
123
+ }
124
+
125
+ if (link.relList.contains('preload') || link.relList.contains('modulepreload')) {
126
+ const controller = new AbortController();
127
+ const signal = passedSignal instanceof AbortSignal ? AbortSignal.any([controller.signal, passedSignal]) : controller.signal;
128
+
129
+ if (passedSignal instanceof AbortSignal) {
130
+ passedSignal.addEventListener('abort', ({ target }) => {
131
+ reject(target.reason);
132
+ }, { signal: controller.signal, once: true });
133
+ }
134
+
135
+ link.referrerPolicy = referrerPolicy;
136
+
137
+ link.addEventListener('load', () => {
138
+ resolve();
139
+ controller.abort();
140
+ }, { signal });
141
+
142
+ link.addEventListener('error', () => {
143
+ reject(new DOMException(`Error loading ${href}`, 'NotFoundError'));
144
+ controller.abort();
145
+ }, { signal });
146
+
147
+ link.href = href;
148
+
149
+ document.head.append(link);
150
+
151
+ return promise.catch(reportError).finally(() => link.isConnected && link.remove());
152
+ } else {
153
+ link.href = href;
154
+ document.head.append(link);
155
+ resolve();
156
+ return promise;
157
+ }
158
+ }
159
+ }
160
+
161
+ function _handlePreloadMutations(target) {
162
+ if (target instanceof MutationRecord) {
163
+ _handlePreloadMutations(target.target);
164
+ } else if (target.tagName === 'A' && ! target.classList.contains('no-router')) {
165
+ preloadOnHover(target, target.dataset);
166
+ } else {
167
+ target.querySelectorAll('a:not(.no-router)').forEach(a => preloadOnHover(a, a.dataset));
168
+ }
169
+ }
170
+
171
+ const preloadObserver = new MutationObserver(entries => entries.forEach(_handlePreloadMutations));
172
+
173
+ /**
174
+ * Preloads a module asynchronously.
175
+ *
176
+ * @param {string} src - The URL or specifier to the module to preload.
177
+ * @param {object} [options] - Optional options for the preload element.
178
+ * @param {string} [options.crossOrigin="anonymous"] - The CORS mode to use when fetching the module. Defaults to 'anonymous'.
179
+ * @param {string} [options.referrerPolicy="no-referrer"] - The referrer policy to use when fetching the module. Defaults to 'no-referrer'.
180
+ * @param {string} [options.fetchPriority="low"] - The fetch priority for the preload request. Defaults to 'auto'.
181
+ * @param {string} [options.as="script"] - The type of resource to preload. Defaults to 'script'.
182
+ * @param {AbortSignal} [options.signal] - An AbortSignal to abort the preload request. Defaults to a 5-second timeout.
183
+ * @param {string} [options.integrity] - A base64-encoded cryptographic hash of the resource
184
+ * @returns {Promise<void>} A promise that resolves when the module is preloaded or rejects on error or signal is aborted.
185
+ * @throws {Error} Throws if the signal is aborted or if an `error` event is fired on the preload.
186
+ */
187
+ async function preloadModule(src, {
188
+ crossOrigin = 'anonymous',
189
+ referrerPolicy = 'no-referrer',
190
+ fetchPriority = 'low',
191
+ as = 'script',
192
+ signal,
193
+ integrity,
194
+ } = {}) {
195
+ await _loadLink(src, {
196
+ relList: ['modulepreload'],
197
+ crossOrigin, referrerPolicy, fetchPriority, as, signal, integrity,
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Preloads a resource asynchronously.
203
+
204
+ * @param {string|URL} href - The URL or specifier to the resource to preload.
205
+ * @param {Object} [options] - Optional options for the preload element.
206
+ * @param {string} [options.crossOrigin="anonymous"] - The CORS mode to use when fetching the resource. Defaults to 'anonymous'.
207
+ * @param {string} [options.referrerPolicy="no-referrer"] - The referrer policy to use when fetching the resource. Defaults to 'no-referrer'.
208
+ * @param {string} [options.fetchPriority="auto"] - The fetch priority for the preload request. Defaults to 'auto'.
209
+ * @param {AbortSignal} [options.signal] - An AbortSignal to abort the preload request. Defaults to a 5-second timeout.
210
+ * @param {string} [options.integrity] - A base64-encoded cryptographic hash of the resource
211
+ * @param {string} [options.as] - The type of resource to preload.
212
+ * @param {string} [options.type] - The MIME type of the resource to preload.
213
+ * @param {(string|MediaQueryList)} [options.media] - A media query string or a MediaQueryList object.
214
+ * @returns {Promise<void>} A promise that resolves when the resource is preloaded or rejects on error or signal is aborted.
215
+ * @throws {Error} Throws if the signal is aborted or if an `error` event is fired on the preload.
216
+ */
217
+ async function preload(href, {
218
+ crossOrigin = 'anonymous',
219
+ referrerPolicy = 'no-referrer',
220
+ fetchPriority = 'auto',
221
+ signal,
222
+ as,
223
+ integrity,
224
+ media,
225
+ type,
226
+ } = {}) {
227
+
228
+ await _loadLink(href, {
229
+ relList: ['preload'],
230
+ crossOrigin, referrerPolicy, fetchPriority, as, signal, type, media, integrity,
231
+ });
232
+ }
233
+ /**
234
+ * Preloads resources associated with an element or selector when hovered over, with optional configuration.
235
+ *
236
+ * @param {string|HTMLElement} target - A CSS selector string or an HTMLElement that triggers preloading.
237
+ * @param {object} [options={}] - Configuration options for preloading.
238
+ * @param {string} [options.crossOrigin='anonymous'] - The cross-origin attribute for the request, useful for fetching from other origins.
239
+ * @param {string} [options.referrerPolicy='no-referrer'] - The referrer policy to apply to the request.
240
+ * @param {string} [options.fetchPriority='high'] - The priority level of the fetch operation.
241
+ * @param {AbortSignal} [options.signal] - Optional signal to abort the preload operation if needed.
242
+ * @returns {Promise<void>} A promise that resolves once preloading completes.
243
+ * @throws {TypeError} Throws if the target is not a valid selector or an HTMLElement with a valid `href` attribute.
244
+ */
245
+ async function preloadOnHover(target, {
246
+ crossOrigin = 'anonymous',
247
+ referrerPolicy = 'no-referrer',
248
+ fetchPriority = 'high',
249
+ signal,
250
+ } = {}) {
251
+ const { resolve, reject, promise } = Promise.withResolvers();
252
+
253
+ if (typeof target === 'string') {
254
+ await Promise.all(Array.from(
255
+ document.querySelectorAll(target),
256
+ link => preloadOnHover(link)
257
+ )).then(resolve, reject);
258
+ } else if (
259
+ target instanceof HTMLElement
260
+ && ! target.classList.contains('no-router')
261
+ && typeof target.href === 'string'
262
+ && target.origin === location.origin
263
+ && target.download.length === 0
264
+ && URL.canParse(target.href)
265
+ ) {
266
+ target.addEventListener('mouseover', async ({ currentTarget }) => {
267
+ const pattern = getRegistryKey(currentTarget.href);
268
+
269
+ if (pattern instanceof URLPattern) {
270
+ const specifier = getRegistrySpecifier(pattern);
271
+ const resolved = undefined(specifier);
272
+
273
+ await preloadModule(resolved, {
274
+ fetchPriority,
275
+ referrerPolicy,
276
+ crossOrigin,
277
+ integrity: currentTarget.dataset.integrity,
278
+ signal,
279
+ });
280
+ resolve();
281
+ } else {
282
+ await preload(currentTarget.href, {
283
+ fetchPriority,
284
+ crossOrigin,
285
+ referrerPolicy,
286
+ as: currentTarget.dataset.preloadAs ?? 'fetch',
287
+ type: currentTarget.dataset.preloadType ?? 'text/html',
288
+ integrity: currentTarget.dataset.integrity,
289
+ signal,
290
+ });
291
+ resolve();
292
+ }
293
+ }, { once: true, passive: true, signal });
294
+ } else {
295
+ resolve();
296
+ }
297
+
298
+ await promise;
299
+ }
300
+
301
+ /**
302
+ * Adds `mouseenter` listeners to preload links/handlers via a `MutationObserver`
303
+ *
304
+ * @param {HTMLElement|ShadowRoot|string} target Target for the mutation observer or its selector
305
+ * @param {HTMLElement|ShadowRoot} [base=document] The element to query from if `target` is a selector
306
+ */
307
+ function observePreloadsOn(target, base = document.documentElement) {
308
+ if (typeof target === 'string') {
309
+ observePreloadsOn(base.querySelector(target));
310
+ } else if (target instanceof HTMLElement || target instanceof ShadowRoot) {
311
+ preloadObserver.observe(target, { childList : true, subtree: true });
312
+ _handlePreloadMutations(target);
313
+ } else {
314
+ throw new TypeError('`observePreloadsOn` requires a selector or HTMLElement or ShadowRoot.');
315
+ }
316
+ }
317
+
318
+ /**
319
+ * This is necessary since an HTML response from a same-origin
320
+ * request should result in the same document state as if
321
+ * it were initial load. CSP/Trusted Types requires `TrustedHTML`
322
+ * for `Document.parseHTMLUnsage` (or `innerHTML`), and `setHTML()`
323
+ * would filter out any `<iframe>` or `onclick` or `<form action>`.
324
+ */
325
+ const policy = 'trustedTypes' in globalThis
326
+ ? trustedTypes.createPolicy('aegis-atlas#html', {
327
+ createHTML(input) {
328
+ return input;
329
+ }
330
+ }) : Object.freeze({
331
+ createHTML(input) {
332
+ return input;
333
+ }
334
+ });
335
+
336
+ const DESC_SELECTOR = 'meta[name="description"], meta[itemprop="description"], meta[property="og:description"], meta[name="twitter:description"]';
337
+
338
+ /**
339
+ * @typedef RouteContextObject
340
+ * @property {URLPatternResult} result
341
+ * @property {Record<string, string>} params
342
+ * @property {DisposableStack} stack
343
+ * @property {AbortController} controller
344
+ * @property {AbortSignal} signal
345
+ * @property {NavigationType} type
346
+ * @property {URL} url
347
+ * @property {any} state
348
+ * @property {any} info
349
+ * @property {number} timestamp
350
+ * @readonly
351
+ */
352
+
353
+ /** @typedef {Response|DocumentFragment|Element|HTMLDocument|URL} HandlerResult */
354
+ /** @typedef {(request: Request, context: RouteContextObject) => Promise<HandlerResult>} RouteHandler */
355
+
356
+ /** @typedef {Readonly<Record<string, unknown>> & {default?: RouteHandler|HandlerResult, title?: string, description?: string, styles?: CSSStyleSheet|CSSStyleSheet[]}} Module */
357
+
358
+ /**
359
+ * @type HTMLElement
360
+ */
361
+ let root = document.body;
362
+
363
+ /**
364
+ *
365
+ * @param {string|HTMLElement} newRoot
366
+ * @param {DocumentOrShadowRoot} base
367
+ */
368
+ function setRoot(newRoot, base = document) {
369
+ if (typeof newRoot === 'string') {
370
+ setRoot(base.getElementById(newRoot));
371
+ } else if (newRoot instanceof HTMLElement) {
372
+ root = newRoot;
373
+ } else {
374
+ throw new TypeError('New root must be an `Element` or `id` of an element.');
375
+ }
376
+ }
377
+
378
+ /**
379
+ *
380
+ * @param {HTMLFormElement|HTMLButtonElement|HTMLAnchorElement} source
381
+ * @returns {"GET"|"POST"}
382
+ */
383
+ function getRequestMethod(source) {
384
+ if (! (source instanceof HTMLElement) || source instanceof HTMLAnchorElement) {
385
+ return 'GET';
386
+ } else if (source instanceof HTMLFormElement) {
387
+ return source.method.toUpperCase();
388
+ } else if (! (source instanceof HTMLButtonElement)) {
389
+ return 'GET';
390
+ } else if (source.hasAttribute('formmethod') && source.formMethod.length !== 0) {
391
+ return source.formMethod.toUpperCase();
392
+ } else if (source.form instanceof HTMLFormElement) {
393
+ return source.form.method.toUpperCase();
394
+ } else {
395
+ console.warn('Not sure this should be possible...');
396
+ return 'GET';
397
+ }
398
+ }
399
+
400
+ /**
401
+ *
402
+ * @param {NavigationEvent} event
403
+ */
404
+ async function handleNavigation(event) {
405
+ if (! (event instanceof NavigateEvent)) {
406
+ throw new TypeError('Not a navigation event.');
407
+ } else if (event.signal.aborted) {
408
+ throw event.signal.reason;
409
+ } else {
410
+ const method = getRequestMethod(event.sourceElement);
411
+ const request = new Request(event.destination.url, {
412
+ // `sourceElement` could be a form, a `<button type="submit">`, or an `<a>
413
+ method: method,
414
+ body: method === 'GET' ? undefined : event.formData,// ?? new FormData(event.sourceElement?.form ?? event.sourceElement),
415
+ signal: event.signal,
416
+ });
417
+
418
+ const { result, specifier, hasRegExpGroups } = lookupRoute(event.destination.url);
419
+
420
+ if (typeof specifier !== 'string' || result === null) {
421
+ const resp = await fetch(request);
422
+ await updateContent(resp);
423
+ } else {
424
+ const params = hasRegExpGroups ? {
425
+ ...result.protocol.groups, ...result.username.groups, ...result.password.groups, ...result.hostname.groups,
426
+ ...result.port.groups, ...result.pathname.groups, ...result.search.groups, ...result.hash.groups,
427
+ }: {};
428
+
429
+ delete params['0'];
430
+ const module = await import(specifier);
431
+ const stack = new DisposableStack();
432
+ const controller = stack.adopt(
433
+ new AbortController(),
434
+ controller => controller.abort(new DOMException('Stack was disposed.', 'AbortError')),
435
+ );
436
+
437
+ const timestamp = performance.now();
438
+ const signal = AbortSignal.any([controller.signal, request.signal]);
439
+
440
+ /**
441
+ * @type {RouteContextObject}
442
+ */
443
+ const context = Object.freeze({
444
+ timestamp,
445
+ stack,
446
+ controller,
447
+ type: event.navigationType,
448
+ state: event.destination.getState(),
449
+ info: event.info,
450
+ url: new URL(event.destination.url),
451
+ signal,
452
+ result,
453
+ params,
454
+ });
455
+
456
+ try {
457
+ return await handleRequestModule(request, context, module);
458
+ } catch(err) {
459
+ reportError(err);
460
+ } finally {
461
+ stack.dispose();
462
+ }
463
+ }
464
+ }
465
+ }
466
+
467
+ /**
468
+ *
469
+ * @param {unknown} routes
470
+ * @param {object} config
471
+ * @param {HTMLElement|string} [config.root]
472
+ * @param {boolean} [config.preload=false]
473
+ * @param {AbortSignal} [config.signal]
474
+ */
475
+ function init(routes, {
476
+ root,
477
+ preload = false,
478
+ signal,
479
+ } = {}) {
480
+ if (typeof routes === 'string') {
481
+ init(JSON.parse(document.scripts.namedItem(routes).textContent), { root, preload, signal });
482
+ } else if (typeof routes === 'number') {
483
+ init(JSON.parse(document.scripts.item(routes).textContent), { root, preload, signal });
484
+ } else if (routes instanceof HTMLScriptElement) {
485
+ init(JSON.parse(routes.textContent), { root, preload, signal });
486
+ } else if (typeof routes === 'object') {
487
+ Object.entries(routes).forEach(([key, val]) => registerModule(key, val));
488
+
489
+ if (typeof root === 'string' || root instanceof HTMLElement) {
490
+ setRoot(root);
491
+ }
492
+
493
+ navigation.addEventListener('navigate', event => {
494
+ if (event.canIntercept && event.destination.url.startsWith(location.origin) && ! event.sourceElement?.classList?.contains?.('no-router')) {
495
+ event.intercept({ handler: () => handleNavigation(event) });
496
+ }
497
+ }, { signal });
498
+
499
+ if (preload) {
500
+ observePreloadsOn(document.body);
501
+ }
502
+ } else {
503
+ throw new TypeError(`Routes must be an object, \`<script>\`, or name/index of \`document.scripts\`. Got a ${typeof routes}.`);
504
+ }
505
+ }
506
+
507
+ /**
508
+ *
509
+ * @param {object} options
510
+ * @param {AbortSignal} [options.signal]
511
+ * @returns {Promise<NavigationHistoryEntry>}
512
+ */
513
+ async function whenLoaded({ signal } = {}) {
514
+ const { resolve, reject, promise } = Promise.withResolvers();
515
+
516
+ if (signal?.aborted) {
517
+ reject(signal.reason);
518
+ } else {
519
+ const controller = new AbortController();
520
+ const opts = {
521
+ once: true,
522
+ signal: signal instanceof AbortSignal ? AbortSignal.any([signal, controller.signal]) : controller.signal,
523
+ };
524
+
525
+ navigation.addEventListener('navigatesuccess', () => {
526
+ resolve(navigation.currentEntry);
527
+ controller.abort();
528
+ }, opts);
529
+
530
+ navigation.addEventListener('navigateerror', event => {
531
+ reject(event.error);
532
+ controller.abort();
533
+ }, opts);
534
+
535
+ if (signal instanceof AbortSignal) {
536
+ signal.addEventListener('abort', ({ target }) => {
537
+ reject(target.reason);
538
+ controller.abort(target.reason);
539
+ }, { once: true, signal: controller.signal });
540
+ }
541
+ }
542
+
543
+ return promise;
544
+ }
545
+
546
+ /**
547
+ *
548
+ * @param {string|URL} newURL
549
+ * @param {NavigationOptions} options
550
+ * @returns {NavigationResult}
551
+ */
552
+ const navigate = (newURL, options) => navigation.navigate(newURL, options);
553
+
554
+ /**
555
+ *
556
+ * @param {NavigationOptions} options
557
+ * @returns {NavigationResult}
558
+ */
559
+ const back = (options) => navigation.back(options);
560
+
561
+ /**
562
+ *
563
+ * @param {NavigationOptions} options
564
+ * @returns {NavigationResult}
565
+ */
566
+ const forward = (options) => navigation.forward(options);
567
+
568
+ /**
569
+ *
570
+ * @param {NavigationReloadOptions} options
571
+ * @returns {NavigationResult}
572
+ */
573
+ const reload = (options) => navigation.reload(options);
574
+
575
+ /**
576
+ *
577
+ * @param {Request} request
578
+ * @param {RouteContextObject} context
579
+ * @param {Module} module
580
+ */
581
+ async function handleRequestModule(request, context, module) {
582
+ if (typeof module.default === 'undefined') {
583
+ throw new TypeError(`No default export in module for <${request.url}>.`);
584
+ } else if (typeof module.default === 'function') {
585
+ const result = await module.default(request, context);
586
+ await updateContent(result);
587
+ updateMeta(module);
588
+ } else {
589
+ await updateContent(module.default);
590
+ updateMeta(module);
591
+ }
592
+ }
593
+
594
+ function updateMeta({ title, description, styles }) {
595
+ if (typeof title === 'string') {
596
+ document.title = title;
597
+ }
598
+
599
+ if (typeof description === 'string') {
600
+ setDescription(description);
601
+ }
602
+
603
+ if (styles instanceof CSSStyleSheet) {
604
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, styles];
605
+ } else if (Array.isArray(styles) && styles.length !== 0) {
606
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, ...styles];
607
+ }
608
+ }
609
+
610
+ /**
611
+ *
612
+ * @param {HandlerResult} content
613
+ */
614
+ async function updateContent(content) {
615
+ if (content instanceof URL) {
616
+ navigate(content);
617
+ } else if (content instanceof Response) {
618
+ if (! content.ok) {
619
+ throw new DOMException(`${content.url} [${content.status}]`, 'NetworkError');
620
+ } else if (! content.headers.get('Content-Type')?.startsWith?.('text/html')) {
621
+ throw new TypeError(`Unsupported Content-Type for <${content.url}> - "${content.headers.get('Content-Type') ?? 'Unset'}".`);
622
+ } else {
623
+ const html = await content.text();
624
+ /** @type HTMLDocument */
625
+ const doc = Document.parseHTMLUnsafe(policy.createHTML(html)); // Unsafe, but necessary... Same-origin at least
626
+ await updateContent(doc);
627
+ }
628
+ } else if (content instanceof Element || content instanceof DocumentFragment) {
629
+ root.replaceChildren(content);
630
+ } else if (content instanceof HTMLDocument) {
631
+ document.title = content.title;
632
+ setDescription(content.head.querySelector(DESC_SELECTOR)?.content);
633
+
634
+ if (root instanceof HTMLBodyElement) {
635
+ root.replaceChildren(...content.body.childNodes);
636
+ } else if (root instanceof HTMLElement && typeof root.id === 'string') {
637
+ root.replaceChildren(...content.getElementById(root.id)?.childNodes ?? []);
638
+ } else {
639
+ throw new TypeError('Root must be `<body>` or an element with an `id`.');
640
+ }
641
+ } else {
642
+ throw new TypeError('Content must be an `Element`, `DocumentFragment`, `HTMLDocument`, or `Response`.');
643
+ }
644
+ }
645
+
646
+ /**
647
+ *
648
+ * @param {string} description
649
+ */
650
+ function setDescription(description = '') {
651
+ document.head.querySelectorAll(DESC_SELECTOR).forEach(el => el.content = description);
652
+ }
653
+
654
+ exports.back = back;
655
+ exports.forward = forward;
656
+ exports.getRegistryKey = getRegistryKey;
657
+ exports.getRegistrySpecifier = getRegistrySpecifier;
658
+ exports.handleNavigation = handleNavigation;
659
+ exports.init = init;
660
+ exports.lookupRoute = lookupRoute;
661
+ exports.navigate = navigate;
662
+ exports.observePreloadsOn = observePreloadsOn;
663
+ exports.preload = preload;
664
+ exports.preloadModule = preloadModule;
665
+ exports.preloadOnHover = preloadOnHover;
666
+ exports.registerModule = registerModule;
667
+ exports.reload = reload;
668
+ exports.setRoot = setRoot;
669
+ exports.whenLoaded = whenLoaded;
package/atlas.js ADDED
@@ -0,0 +1,3 @@
1
+ export { registerModule, lookupRoute, getRegistryKey, getRegistrySpecifier } from './routes.js';
2
+ export { preloadModule, preload, preloadOnHover, observePreloadsOn } from './preload.js';
3
+ export { setRoot, handleNavigation, init, whenLoaded, navigate, back, forward, reload } from './router.js';
package/atlas.min.js ADDED
@@ -0,0 +1,2 @@
1
+ const e=new Map,t=Object.freeze({result:null,specifier:null,hasRegExpGroups:!1}),n=t=>e.keys().find((e=>e.test(t))),r=t=>e.get(t);function o(r){const o=n(r);return o instanceof URLPattern?Object.freeze({result:o.exec(r),specifier:e.get(o),hasRegExpGroups:o.hasRegExpGroups}):t}function i(t,n){if(!("string"==typeof n||n instanceof URL))throw new TypeError(`Invalid specifier type ${typeof n}.`);if("string"==typeof t)e.set(URL.canParse(t)?new URLPattern(t):new URLPattern({pathname:t}),n.toString());else{if(!(t instanceof URLPattern))throw new TypeError(`Invalid pattner "${t}".`);n instanceof URL?e.set(t,n.href):e.set(t,n)}}function a(e,{relList:t=[],crossOrigin:n="anonymous",referrerPolicy:r="no-referrer",fetchPriority:o="auto",signal:i,as:a,integrity:s,media:c,type:l}={}){const{promise:f,resolve:d,reject:p}=Promise.withResolvers(),g=document.createElement("link");if(i instanceof AbortSignal&&i.aborted)p(i.reason);else{if("string"==typeof e||e instanceof URL){if(g.relList.add(...t),"string"==typeof o&&(g.fetchPriority=o),"string"==typeof n&&(g.crossOrigin=n),"string"==typeof l&&(g.type=l),"string"==typeof c?g.media=c:c instanceof MediaQueryList&&(g.media=c.media),"string"==typeof a&&(g.as=a),"string"==typeof s&&(g.integrity=s),g.relList.contains("preload")||g.relList.contains("modulepreload")){const t=new AbortController,n=i instanceof AbortSignal?AbortSignal.any([t.signal,i]):t.signal;return i instanceof AbortSignal&&i.addEventListener("abort",(({target:e})=>{p(e.reason)}),{signal:t.signal,once:!0}),g.referrerPolicy=r,g.addEventListener("load",(()=>{d(),t.abort()}),{signal:n}),g.addEventListener("error",(()=>{p(new DOMException(`Error loading ${e}`,"NotFoundError")),t.abort()}),{signal:n}),g.href=e,document.head.append(g),f.catch(reportError).finally((()=>g.isConnected&&g.remove()))}return g.href=e,document.head.append(g),d(),f}p(new TypeError(`Invalid href to preload: "${e}.`))}}function s(e){e instanceof MutationRecord?s(e.target):"A"!==e.tagName||e.classList.contains("no-router")?e.querySelectorAll("a:not(.no-router)").forEach((e=>d(e,e.dataset))):d(e,e.dataset)}const c=new MutationObserver((e=>e.forEach(s)));async function l(e,{crossOrigin:t="anonymous",referrerPolicy:n="no-referrer",fetchPriority:r="low",as:o="script",signal:i,integrity:s}={}){await a(e,{relList:["modulepreload"],crossOrigin:t,referrerPolicy:n,fetchPriority:r,as:o,signal:i,integrity:s})}async function f(e,{crossOrigin:t="anonymous",referrerPolicy:n="no-referrer",fetchPriority:r="auto",signal:o,as:i,integrity:s,media:c,type:l}={}){await a(e,{relList:["preload"],crossOrigin:t,referrerPolicy:n,fetchPriority:r,as:i,signal:o,type:l,media:c,integrity:s})}async function d(e,{crossOrigin:t="anonymous",referrerPolicy:o="no-referrer",fetchPriority:i="high",signal:a}={}){const{resolve:s,reject:c,promise:p}=Promise.withResolvers();"string"==typeof e?await Promise.all(Array.from(document.querySelectorAll(e),(e=>d(e)))).then(s,c):e instanceof HTMLElement&&!e.classList.contains("no-router")&&"string"==typeof e.href&&e.origin===location.origin&&0===e.download.length&&URL.canParse(e.href)?e.addEventListener("mouseover",(async({currentTarget:e})=>{const c=n(e.href);if(c instanceof URLPattern){const n=r(c),f=import.meta.resolve(n);await l(f,{fetchPriority:i,referrerPolicy:o,crossOrigin:t,integrity:e.dataset.integrity,signal:a}),s()}else await f(e.href,{fetchPriority:i,crossOrigin:t,referrerPolicy:o,as:e.dataset.preloadAs??"fetch",type:e.dataset.preloadType??"text/html",integrity:e.dataset.integrity,signal:a}),s()}),{once:!0,passive:!0,signal:a}):s(),await p}function p(e,t=document.documentElement){if("string"==typeof e)p(t.querySelector(e));else{if(!(e instanceof HTMLElement||e instanceof ShadowRoot))throw new TypeError("`observePreloadsOn` requires a selector or HTMLElement or ShadowRoot.");c.observe(e,{childList:!0,subtree:!0}),s(e)}}const g="trustedTypes"in globalThis?trustedTypes.createPolicy("aegis-atlas#html",{createHTML:e=>e}):Object.freeze({createHTML:e=>e}),u='meta[name="description"], meta[itemprop="description"], meta[property="og:description"], meta[name="twitter:description"]';let y=document.body;function m(e,t=document){if("string"==typeof e)m(t.getElementById(e));else{if(!(e instanceof HTMLElement))throw new TypeError("New root must be an `Element` or `id` of an element.");y=e}}async function h(e){if(!(e instanceof NavigateEvent))throw new TypeError("Not a navigation event.");if(e.signal.aborted)throw e.signal.reason;{const n=!((t=e.sourceElement)instanceof HTMLElement)||t instanceof HTMLAnchorElement?"GET":t instanceof HTMLFormElement?t.method.toUpperCase():t instanceof HTMLButtonElement?t.hasAttribute("formmethod")&&0!==t.formMethod.length?t.formMethod.toUpperCase():t.form instanceof HTMLFormElement?t.form.method.toUpperCase():(console.warn("Not sure this should be possible..."),"GET"):"GET",r=new Request(e.destination.url,{method:n,body:"GET"===n?void 0:e.formData,signal:e.signal}),{result:i,specifier:a,hasRegExpGroups:s}=o(e.destination.url);if("string"!=typeof a||null===i){const e=await fetch(r);await P(e)}else{const t=s?{...i.protocol.groups,...i.username.groups,...i.password.groups,...i.hostname.groups,...i.port.groups,...i.pathname.groups,...i.search.groups,...i.hash.groups}:{};delete t[0];const n=await import(a),o=new DisposableStack,c=o.adopt(new AbortController,(e=>e.abort(new DOMException("Stack was disposed.","AbortError")))),l=performance.now(),f=AbortSignal.any([c.signal,r.signal]),d=Object.freeze({timestamp:l,stack:o,controller:c,type:e.navigationType,state:e.destination.getState(),info:e.info,url:new URL(e.destination.url),signal:f,result:i,params:t});try{return await async function(e,t,n){if(void 0===n.default)throw new TypeError(`No default export in module for <${e.url}>.`);if("function"==typeof n.default){const r=await n.default(e,t);await P(r),S(n)}else await P(n.default),S(n)}(r,d,n)}catch(e){reportError(e)}finally{o.dispose()}}}var t}function w(e,{root:t,preload:n=!1,signal:r}={}){if("string"==typeof e)w(JSON.parse(document.scripts.namedItem(e).textContent),{root:t,preload:n,signal:r});else if("number"==typeof e)w(JSON.parse(document.scripts.item(e).textContent),{root:t,preload:n,signal:r});else if(e instanceof HTMLScriptElement)w(JSON.parse(e.textContent),{root:t,preload:n,signal:r});else{if("object"!=typeof e)throw new TypeError(`Routes must be an object, \`<script>\`, or name/index of \`document.scripts\`. Got a ${typeof e}.`);Object.entries(e).forEach((([e,t])=>i(e,t))),("string"==typeof t||t instanceof HTMLElement)&&m(t),navigation.addEventListener("navigate",(e=>{e.canIntercept&&e.destination.url.startsWith(location.origin)&&!e.sourceElement?.classList?.contains?.("no-router")&&e.intercept({handler:()=>h(e)})}),{signal:r}),n&&p(document.body)}}async function E({signal:e}={}){const{resolve:t,reject:n,promise:r}=Promise.withResolvers();if(e?.aborted)n(e.reason);else{const r=new AbortController,o={once:!0,signal:e instanceof AbortSignal?AbortSignal.any([e,r.signal]):r.signal};navigation.addEventListener("navigatesuccess",(()=>{t(navigation.currentEntry),r.abort()}),o),navigation.addEventListener("navigateerror",(e=>{n(e.error),r.abort()}),o),e instanceof AbortSignal&&e.addEventListener("abort",(({target:e})=>{n(e.reason),r.abort(e.reason)}),{once:!0,signal:r.signal})}return r}const b=(e,t)=>navigation.navigate(e,t),L=e=>navigation.back(e),v=e=>navigation.forward(e),T=e=>navigation.reload(e);function S({title:e,description:t,styles:n}){"string"==typeof e&&(document.title=e),"string"==typeof t&&M(t),n instanceof CSSStyleSheet?document.adoptedStyleSheets=[...document.adoptedStyleSheets,n]:Array.isArray(n)&&0!==n.length&&(document.adoptedStyleSheets=[...document.adoptedStyleSheets,...n])}async function P(e){if(e instanceof URL)b(e);else if(e instanceof Response){if(!e.ok)throw new DOMException(`${e.url} [${e.status}]`,"NetworkError");if(!e.headers.get("Content-Type")?.startsWith?.("text/html"))throw new TypeError(`Unsupported Content-Type for <${e.url}> - "${e.headers.get("Content-Type")??"Unset"}".`);{const t=await e.text(),n=Document.parseHTMLUnsafe(g.createHTML(t));await P(n)}}else if(e instanceof Element||e instanceof DocumentFragment)y.replaceChildren(e);else{if(!(e instanceof HTMLDocument))throw new TypeError("Content must be an `Element`, `DocumentFragment`, `HTMLDocument`, or `Response`.");if(document.title=e.title,M(e.head.querySelector(u)?.content),y instanceof HTMLBodyElement)y.replaceChildren(...e.body.childNodes);else{if(!(y instanceof HTMLElement&&"string"==typeof y.id))throw new TypeError("Root must be `<body>` or an element with an `id`.");y.replaceChildren(...e.getElementById(y.id)?.childNodes??[])}}}function M(e=""){document.head.querySelectorAll(u).forEach((t=>t.content=e))}export{L as back,v as forward,n as getRegistryKey,r as getRegistrySpecifier,h as handleNavigation,w as init,o as lookupRoute,b as navigate,p as observePreloadsOn,f as preload,l as preloadModule,d as preloadOnHover,i as registerModule,T as reload,m as setRoot,E as whenLoaded};
2
+ //# sourceMappingURL=atlas.min.js.map