@eagami/ui 5.74.0 → 5.76.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/README.md +13 -1
- package/fesm2022/eagami-ui-router.mjs +124 -0
- package/fesm2022/eagami-ui-router.mjs.map +1 -0
- package/fesm2022/eagami-ui.mjs +1342 -1
- package/fesm2022/eagami-ui.mjs.map +1 -1
- package/package.json +13 -2
- package/router/package.json +4 -0
- package/schematics/collection.json +1 -1
- package/schematics/ng-add/index.js +14 -2
- package/types/eagami-ui-router.d.ts +54 -0
- package/types/eagami-ui.d.ts +100 -2
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
ng add @eagami/ui
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
The schematic installs the package and registers the global stylesheet
|
|
39
|
+
The schematic installs the package and registers the global stylesheet, the fonts, and `provideStaleBuildReload()`. To wire it up by hand, see [Setup](https://eagami.com/ui/setup).
|
|
40
40
|
|
|
41
41
|
## Use
|
|
42
42
|
|
|
@@ -87,6 +87,18 @@ Set it app-wide with `forms`, or on a single field with the `showErrorsOn` input
|
|
|
87
87
|
provideEagamiUi({ forms: { showErrorsOn: 'touch' } });
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
## Deploys
|
|
91
|
+
|
|
92
|
+
A tab left open across a deploy still runs the old build, so the next page it lazy loads asks for a file the deploy removed. `provideStaleBuildReload()` from `@eagami/ui/router` loads that page in full on the new build instead, and tries only once:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
bootstrapApplication(AppComponent, {
|
|
96
|
+
providers: [provideRouter(routes), provideStaleBuildReload()],
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Pass `version` and `onReport` to log how each reload turned out, and check `isStaleBuildReloadError(error)` to keep the error it answered out of your error reporting.
|
|
101
|
+
|
|
90
102
|
## Testing
|
|
91
103
|
|
|
92
104
|
`<ea-dialog>` and `<ea-drawer>` are built on the native `<dialog>` element, and `@eagami/ui/testing` keeps them working in jsdom, which lacks `showModal()` and `close()`. Call `installNativeDialogShim()` once from your test setup file:
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { DOCUMENT, Location, isPlatformBrowser } from "@angular/common";
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
import { Injectable, InjectionToken, PLATFORM_ID, inject, makeEnvironmentProviders, provideEnvironmentInitializer } from "@angular/core";
|
|
4
|
+
import { NavigationEnd, NavigationError, Router } from "@angular/router";
|
|
5
|
+
const PENDING_RELOAD_KEY = "ea-pending-reload";
|
|
6
|
+
const CHUNK_LOAD_FAILURES = [
|
|
7
|
+
"Failed to fetch dynamically imported module",
|
|
8
|
+
"error loading dynamically imported module",
|
|
9
|
+
"Importing a module script failed",
|
|
10
|
+
"is not a valid JavaScript MIME type"
|
|
11
|
+
];
|
|
12
|
+
const STALE_BUILD_RELOAD_CONFIG = new InjectionToken("STALE_BUILD_RELOAD_CONFIG");
|
|
13
|
+
const LOAD_PAGE = new InjectionToken("LOAD_PAGE", {
|
|
14
|
+
providedIn: "root",
|
|
15
|
+
factory: () => {
|
|
16
|
+
const location = inject(DOCUMENT).location;
|
|
17
|
+
return (url, replace) => replace ? location.replace(url) : location.assign(url);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const reloadedErrors = /* @__PURE__ */ new WeakSet();
|
|
21
|
+
function isStaleBuildReloadError(error) {
|
|
22
|
+
return error instanceof Error && reloadedErrors.has(error);
|
|
23
|
+
}
|
|
24
|
+
function isChunkLoadFailure(error) {
|
|
25
|
+
return error instanceof TypeError && CHUNK_LOAD_FAILURES.some((failure) => error.message.includes(failure));
|
|
26
|
+
}
|
|
27
|
+
function isSessionStorageAvailable() {
|
|
28
|
+
const probeKey = `${PENDING_RELOAD_KEY}-probe`;
|
|
29
|
+
try {
|
|
30
|
+
sessionStorage.setItem(probeKey, probeKey);
|
|
31
|
+
sessionStorage.removeItem(probeKey);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var StaleBuildReloader = class StaleBuildReloader {
|
|
38
|
+
config = inject(STALE_BUILD_RELOAD_CONFIG);
|
|
39
|
+
loadPage = inject(LOAD_PAGE);
|
|
40
|
+
location = inject(Location);
|
|
41
|
+
router = inject(Router);
|
|
42
|
+
canTrackReloads = isSessionStorageAvailable();
|
|
43
|
+
pageId = Math.random().toString(36).slice(2);
|
|
44
|
+
constructor() {
|
|
45
|
+
this.router.events.subscribe((event) => {
|
|
46
|
+
if (event instanceof NavigationEnd) this.settlePendingReload();
|
|
47
|
+
else if (event instanceof NavigationError && isChunkLoadFailure(event.error)) this.reloadIntoCurrentBuild(event.url, event.error);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
settlePendingReload() {
|
|
51
|
+
const pending = this.takePendingReload();
|
|
52
|
+
if (pending) this.report(pending.pageId === this.pageId ? "stayed" : "recovered", pending);
|
|
53
|
+
}
|
|
54
|
+
reloadIntoCurrentBuild(url, error) {
|
|
55
|
+
const pending = this.takePendingReload();
|
|
56
|
+
if (pending) {
|
|
57
|
+
const isAfterReload = pending.pageId !== this.pageId;
|
|
58
|
+
this.report(isAfterReload ? "failed-after-reload" : "stayed", pending);
|
|
59
|
+
if (isAfterReload) return;
|
|
60
|
+
}
|
|
61
|
+
if (!this.canTrackReloads || !navigator.onLine) return;
|
|
62
|
+
const navigation = this.router.currentNavigation();
|
|
63
|
+
const replace = !this.router.navigated || navigation?.trigger !== "imperative" || !!navigation.extras.replaceUrl;
|
|
64
|
+
const reload = {
|
|
65
|
+
pageId: this.pageId,
|
|
66
|
+
route: url.replace(/[?#].*/, ""),
|
|
67
|
+
version: this.config.version
|
|
68
|
+
};
|
|
69
|
+
sessionStorage.setItem(PENDING_RELOAD_KEY, JSON.stringify(reload));
|
|
70
|
+
reloadedErrors.add(error);
|
|
71
|
+
this.loadPage(this.location.prepareExternalUrl(url), replace);
|
|
72
|
+
}
|
|
73
|
+
takePendingReload() {
|
|
74
|
+
const value = this.canTrackReloads ? sessionStorage.getItem(PENDING_RELOAD_KEY) : null;
|
|
75
|
+
if (!value) return null;
|
|
76
|
+
sessionStorage.removeItem(PENDING_RELOAD_KEY);
|
|
77
|
+
return JSON.parse(value);
|
|
78
|
+
}
|
|
79
|
+
report(outcome, pending) {
|
|
80
|
+
this.config.onReport?.({
|
|
81
|
+
outcome,
|
|
82
|
+
route: pending.route,
|
|
83
|
+
fromVersion: pending.version,
|
|
84
|
+
toVersion: this.config.version
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
88
|
+
minVersion: "12.0.0",
|
|
89
|
+
version: "22.2.3",
|
|
90
|
+
ngImport: i0,
|
|
91
|
+
type: StaleBuildReloader,
|
|
92
|
+
deps: [],
|
|
93
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
94
|
+
});
|
|
95
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
96
|
+
minVersion: "12.0.0",
|
|
97
|
+
version: "22.2.3",
|
|
98
|
+
ngImport: i0,
|
|
99
|
+
type: StaleBuildReloader
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
103
|
+
minVersion: "12.0.0",
|
|
104
|
+
version: "22.2.3",
|
|
105
|
+
ngImport: i0,
|
|
106
|
+
type: StaleBuildReloader,
|
|
107
|
+
decorators: [{ type: Injectable }],
|
|
108
|
+
ctorParameters: () => []
|
|
109
|
+
});
|
|
110
|
+
function provideStaleBuildReload(config = {}) {
|
|
111
|
+
return makeEnvironmentProviders([
|
|
112
|
+
{
|
|
113
|
+
provide: STALE_BUILD_RELOAD_CONFIG,
|
|
114
|
+
useValue: config
|
|
115
|
+
},
|
|
116
|
+
StaleBuildReloader,
|
|
117
|
+
provideEnvironmentInitializer(() => {
|
|
118
|
+
if (isPlatformBrowser(inject(PLATFORM_ID))) inject(StaleBuildReloader);
|
|
119
|
+
})
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
export { isStaleBuildReloadError, provideStaleBuildReload };
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=eagami-ui-router.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eagami-ui-router.mjs","names":[],"sources":["../../../router/src/stale-build-reload.ts"],"sourcesContent":["import { DOCUMENT, Location, isPlatformBrowser } from '@angular/common';\nimport {\n type EnvironmentProviders,\n Injectable,\n InjectionToken,\n PLATFORM_ID,\n inject,\n makeEnvironmentProviders,\n provideEnvironmentInitializer,\n} from '@angular/core';\nimport { NavigationEnd, NavigationError, Router } from '@angular/router';\n\n/**\n * How a reload into the current build turned out:\n *\n * - `'recovered'`: the reloaded page opened\n * - `'failed-after-reload'`: the reloaded page failed to open too, so the error is left to surface\n * - `'stayed'`: the visitor chose to stay when the browser asked them to confirm leaving\n */\nexport type StaleBuildReloadOutcome = 'recovered' | 'failed-after-reload' | 'stayed';\n\nexport interface StaleBuildReloadReport {\n outcome: StaleBuildReloadOutcome;\n /** The path that failed to open, without its query or fragment. */\n route: string;\n /** The `version` configured in the build that failed to open the page. */\n fromVersion?: string;\n /** The `version` configured in the build making the report. */\n toVersion?: string;\n}\n\nexport interface StaleBuildReloadConfig {\n /** The app's version, carried across the reload so a report names both builds. */\n version?: string;\n /** Called once per reload with how it turned out, for example to log it. */\n onReport?: (report: StaleBuildReloadReport) => void;\n}\n\ninterface PendingReload {\n pageId: string;\n route: string;\n version?: string;\n}\n\nconst PENDING_RELOAD_KEY = 'ea-pending-reload';\n\n// Each browser engine words a failed dynamic import differently\nconst CHUNK_LOAD_FAILURES = [\n 'Failed to fetch dynamically imported module', // Chromium\n 'error loading dynamically imported module', // Firefox\n 'Importing a module script failed', // WebKit\n 'is not a valid JavaScript MIME type', // WebKit, when the host answers a removed file with the app's page\n];\n\nconst STALE_BUILD_RELOAD_CONFIG = new InjectionToken<StaleBuildReloadConfig>(\n 'STALE_BUILD_RELOAD_CONFIG',\n);\n\n// Swappable in tests, since jsdom cannot navigate\nexport const LOAD_PAGE = new InjectionToken<(url: string, replace: boolean) => void>(\n 'LOAD_PAGE',\n {\n providedIn: 'root',\n factory: () => {\n const location = inject(DOCUMENT).location;\n return (url, replace) => (replace ? location.replace(url) : location.assign(url));\n },\n },\n);\n\nconst reloadedErrors = new WeakSet<Error>();\n\n/**\n * Whether `error` is a failed page load that `provideStaleBuildReload()` has\n * already answered by reloading. Error reporters can drop it, since the\n * reload's outcome reaches `onReport` instead:\n *\n * ```ts\n * Sentry.init({\n * beforeSend: (event, hint) =>\n * isStaleBuildReloadError(hint.originalException) ? null : event,\n * });\n * ```\n */\nexport function isStaleBuildReloadError(error: unknown): boolean {\n return error instanceof Error && reloadedErrors.has(error);\n}\n\nfunction isChunkLoadFailure(error: unknown): error is TypeError {\n return (\n error instanceof TypeError &&\n CHUNK_LOAD_FAILURES.some(failure => error.message.includes(failure))\n );\n}\n\nfunction isSessionStorageAvailable(): boolean {\n const probeKey = `${PENDING_RELOAD_KEY}-probe`;\n try {\n sessionStorage.setItem(probeKey, probeKey);\n sessionStorage.removeItem(probeKey);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * A pending reload left by another page means this page is that reload. One\n * left by this same page means the visitor stayed when asked to confirm leaving.\n */\n@Injectable()\nclass StaleBuildReloader {\n private readonly config = inject(STALE_BUILD_RELOAD_CONFIG);\n private readonly loadPage = inject(LOAD_PAGE);\n private readonly location = inject(Location);\n private readonly router = inject(Router);\n\n private readonly canTrackReloads = isSessionStorageAvailable();\n private readonly pageId = Math.random().toString(36).slice(2);\n\n constructor() {\n this.router.events.subscribe(event => {\n if (event instanceof NavigationEnd) {\n this.settlePendingReload();\n } else if (event instanceof NavigationError && isChunkLoadFailure(event.error)) {\n this.reloadIntoCurrentBuild(event.url, event.error);\n }\n });\n }\n\n private settlePendingReload(): void {\n const pending = this.takePendingReload();\n\n if (pending) {\n this.report(pending.pageId === this.pageId ? 'stayed' : 'recovered', pending);\n }\n }\n\n private reloadIntoCurrentBuild(url: string, error: TypeError): void {\n const pending = this.takePendingReload();\n\n if (pending) {\n const isAfterReload = pending.pageId !== this.pageId;\n this.report(isAfterReload ? 'failed-after-reload' : 'stayed', pending);\n\n if (isAfterReload) {\n return;\n }\n }\n\n // Untracked, a failing reload could repeat forever, and offline it would only leave the app\n if (!this.canTrackReloads || !navigator.onLine) {\n return;\n }\n\n const navigation = this.router.currentNavigation();\n // A first load, Back or Forward, or a replacing navigation already has its history entry\n const replace =\n !this.router.navigated ||\n navigation?.trigger !== 'imperative' ||\n !!navigation.extras.replaceUrl;\n\n const reload: PendingReload = {\n pageId: this.pageId,\n route: url.replace(/[?#].*/, ''),\n version: this.config.version,\n };\n sessionStorage.setItem(PENDING_RELOAD_KEY, JSON.stringify(reload));\n reloadedErrors.add(error);\n\n this.loadPage(this.location.prepareExternalUrl(url), replace);\n }\n\n private takePendingReload(): PendingReload | null {\n const value = this.canTrackReloads\n ? sessionStorage.getItem(PENDING_RELOAD_KEY)\n : null;\n\n if (!value) {\n return null;\n }\n\n sessionStorage.removeItem(PENDING_RELOAD_KEY);\n return JSON.parse(value);\n }\n\n private report(outcome: StaleBuildReloadOutcome, pending: PendingReload): void {\n this.config.onReport?.({\n outcome,\n route: pending.route,\n fromVersion: pending.version,\n toVersion: this.config.version,\n });\n }\n}\n\n/**\n * Loads the current build in full when the code of a page being opened fails\n * to load, which is what a tab still running a build that a deploy has since\n * replaced runs into. The reload opens the page the visitor asked for, and\n * stops after one attempt if the new build fails as well.\n *\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [provideRouter(routes), provideStaleBuildReload()],\n * });\n * ```\n *\n * Does nothing during server rendering.\n */\nexport function provideStaleBuildReload(\n config: StaleBuildReloadConfig = {},\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: STALE_BUILD_RELOAD_CONFIG, useValue: config },\n StaleBuildReloader,\n // Created ahead of the first navigation, which a tab restored on an old build can fail\n provideEnvironmentInitializer(() => {\n if (isPlatformBrowser(inject(PLATFORM_ID))) {\n inject(StaleBuildReloader);\n }\n }),\n ]);\n}\n"],"mappings":"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"}
|