tempest-react-sdk 0.81.0 → 0.83.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 +9 -10
- package/bin/create-tempest-app.e2e.test.mjs +31 -0
- package/bin/create-tempest-app.mjs +11 -1
- package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
- package/bin/lib/doctor/vitest.mjs +120 -0
- package/bin/lib/doctor/vitest.test.mjs +139 -0
- package/bin/lib/fs/contain.mjs +7 -0
- package/bin/lib/openapi/escape.mjs +68 -4
- package/bin/lib/openapi/generate.hostile.test.mjs +228 -0
- package/bin/lib/openapi/generate.mjs +119 -15
- package/bin/lib/openapi/pattern-risk.mjs +134 -0
- package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
- package/bin/tempest-gen.test.mjs +100 -0
- package/bin/tempest.mjs +24 -8
- package/dist/access/use-can.cjs +1 -1
- package/dist/access/use-can.cjs.map +1 -1
- package/dist/access/use-can.js +25 -28
- package/dist/access/use-can.js.map +1 -1
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +189 -143
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/session-identity.cjs +2 -0
- package/dist/auth/session-identity.cjs.map +1 -0
- package/dist/auth/session-identity.js +12 -0
- package/dist/auth/session-identity.js.map +1 -0
- package/dist/br/boleto.cjs.map +1 -1
- package/dist/br/boleto.js.map +1 -1
- package/dist/components/AuthForm/AuthLinks.cjs +1 -1
- package/dist/components/AuthForm/AuthLinks.cjs.map +1 -1
- package/dist/components/AuthForm/AuthLinks.js +18 -15
- package/dist/components/AuthForm/AuthLinks.js.map +1 -1
- package/dist/components/AuthPage/AuthPage.cjs +1 -1
- package/dist/components/AuthPage/AuthPage.cjs.map +1 -1
- package/dist/components/AuthPage/AuthPage.js +58 -52
- package/dist/components/AuthPage/AuthPage.js.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
- package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/dist/components/Button/Button.cjs +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js +41 -40
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +38 -38
- package/dist/components/Form/Form.cjs +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js +30 -32
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/Layout/Layout.cjs +1 -1
- package/dist/components/Layout/Layout.cjs.map +1 -1
- package/dist/components/Layout/Layout.js +25 -24
- package/dist/components/Layout/Layout.js.map +1 -1
- package/dist/components/Layout/gap.cjs +2 -0
- package/dist/components/Layout/gap.cjs.map +1 -0
- package/dist/components/Layout/gap.js +15 -0
- package/dist/components/Layout/gap.js.map +1 -0
- package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.js +30 -29
- package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +1 -2
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/forms/validate-form.cjs +1 -1
- package/dist/forms/validate-form.cjs.map +1 -1
- package/dist/forms/validate-form.js +6 -1
- package/dist/forms/validate-form.js.map +1 -1
- package/dist/forms/zod-resolver.cjs +1 -1
- package/dist/forms/zod-resolver.cjs.map +1 -1
- package/dist/forms/zod-resolver.js +8 -3
- package/dist/forms/zod-resolver.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +90 -72
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/credential-scope.cjs +1 -1
- package/dist/http/credential-scope.cjs.map +1 -1
- package/dist/http/credential-scope.js +33 -9
- package/dist/http/credential-scope.js.map +1 -1
- package/dist/http/presigned-post.cjs.map +1 -1
- package/dist/http/presigned-post.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +167 -112
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js +100 -44
- package/dist/http/upload-with-progress.js.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js +5 -4
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/offline/create-offline-sync.cjs +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js +87 -69
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +67 -44
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/sse/create-event-stream.cjs +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js +48 -41
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/sse/ticket-event-stream.cjs +1 -1
- package/dist/sse/ticket-event-stream.cjs.map +1 -1
- package/dist/sse/ticket-event-stream.js +48 -19
- package/dist/sse/ticket-event-stream.js.map +1 -1
- package/dist/sse/use-event-stream.cjs +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/sse/use-event-stream.js +19 -14
- package/dist/sse/use-event-stream.js.map +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +10 -8
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +111 -55
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/create-push-handler.cjs +1 -1
- package/dist/sw/create-push-handler.cjs.map +1 -1
- package/dist/sw/create-push-handler.js +20 -10
- package/dist/sw/create-push-handler.js.map +1 -1
- package/dist/sw.d.ts +65 -14
- package/dist/tabular/compact.cjs +1 -1
- package/dist/tabular/compact.cjs.map +1 -1
- package/dist/tabular/compact.js +35 -16
- package/dist/tabular/compact.js.map +1 -1
- package/dist/tabular/manifest.cjs +1 -1
- package/dist/tabular/manifest.cjs.map +1 -1
- package/dist/tabular/manifest.js +33 -15
- package/dist/tabular/manifest.js.map +1 -1
- package/dist/tabular.d.ts +11 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +543 -70
- package/dist/tempest-react-sdk.js +224 -224
- package/dist/theme/initial-theme.cjs +4 -4
- package/dist/theme/initial-theme.cjs.map +1 -1
- package/dist/theme/initial-theme.js +8 -5
- package/dist/theme/initial-theme.js.map +1 -1
- package/dist/utils/compressed-storage.cjs +1 -1
- package/dist/utils/compressed-storage.cjs.map +1 -1
- package/dist/utils/compressed-storage.js +13 -7
- package/dist/utils/compressed-storage.js.map +1 -1
- package/dist/utils/objects.cjs +1 -1
- package/dist/utils/objects.cjs.map +1 -1
- package/dist/utils/objects.js +13 -7
- package/dist/utils/objects.js.map +1 -1
- package/dist/utils/safe-url.cjs +1 -1
- package/dist/utils/safe-url.cjs.map +1 -1
- package/dist/utils/safe-url.js +4 -1
- package/dist/utils/safe-url.js.map +1 -1
- package/dist/vite/base-url.cjs +1 -1
- package/dist/vite/base-url.cjs.map +1 -1
- package/dist/vite/base-url.js +5 -1
- package/dist/vite/base-url.js.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.js +96 -31
- package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
- package/dist/vite/tempest-pwa-manifest.cjs +2 -2
- package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
- package/dist/vite/tempest-pwa-manifest.js +32 -16
- package/dist/vite/tempest-pwa-manifest.js.map +1 -1
- package/dist/vite.cjs +1 -1
- package/dist/vite.d.ts +63 -12
- package/dist/vite.js +4 -4
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +7 -1
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +77 -61
- package/dist/ws/create-web-socket.js.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +4 -3
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +42 -0
- package/template-pwa/src/sw.ts +5 -4
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
const e=require("../utils/storage.cjs");function t(t={}){let{storageKey:n=`tempest-theme`,defaultTheme:r=`system`}=t;if(typeof window>`u`)return r===`dark`?`dark`:`light`;let i=e.storage.getRaw(n)??r;return i===`dark`||i===`light`?i:window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function n(e={}){let t=e.storageKey??`tempest-theme`,
|
|
1
|
+
const e=require("../utils/storage.cjs");function t(t={}){let{storageKey:n=`tempest-theme`,defaultTheme:r=`system`}=t;if(typeof window>`u`)return r===`dark`?`dark`:`light`;let i=e.storage.getRaw(n)??r;return i===`dark`||i===`light`?i:window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function n(e){return JSON.stringify(e).replace(/[<>&\u2028\u2029]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,`0`)}`)}function r(e={}){let t=e.storageKey??`tempest-theme`,r=e.defaultTheme??`system`;return`
|
|
2
2
|
(function(){try{
|
|
3
|
-
var key=${
|
|
4
|
-
var def=${
|
|
3
|
+
var key=${n(t)};
|
|
4
|
+
var def=${n(r)};
|
|
5
5
|
var stored=localStorage.getItem(key);
|
|
6
6
|
var mode=stored||def;
|
|
7
7
|
var resolved=mode==="dark"||mode==="light"?mode:(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");
|
|
8
8
|
document.documentElement.setAttribute("data-tempest-theme",resolved);
|
|
9
9
|
}catch(e){}})();
|
|
10
|
-
`.trim()}exports.getInitialTheme=t,exports.themeInitScript=
|
|
10
|
+
`.trim()}exports.getInitialTheme=t,exports.themeInitScript=r;
|
|
11
11
|
//# sourceMappingURL=initial-theme.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initial-theme.cjs","names":["storage","require_storage","getInitialTheme","options","defaultTheme","mode","storageKey","themeInitScript"],"sources":["../../src/theme/initial-theme.ts"],"sourcesContent":["import { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface GetInitialThemeOptions {\n /** localStorage key. Default: `\"tempest-theme\"`. */\n storageKey?: string;\n /** Theme used when nothing is stored and the user has no system preference. */\n defaultTheme?: ThemeMode;\n}\n\n/**\n * Inline-safe theme resolution intended for early bootstrap (e.g. inside the\n * `<head>` script that prevents the flash of incorrect theme). Reads from\n * localStorage and falls back to `prefers-color-scheme`.\n */\nexport function getInitialTheme(options: GetInitialThemeOptions = {}): ResolvedTheme {\n const { storageKey = \"tempest-theme\", defaultTheme = \"system\" } = options;\n\n if (typeof window === \"undefined\") {\n return defaultTheme === \"dark\" ? \"dark\" : \"light\";\n }\n\n const stored = storage.getRaw(storageKey) as ThemeMode | null;\n const mode: ThemeMode = stored ?? defaultTheme;\n if (mode === \"dark\" || mode === \"light\") return mode;\n\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * Plain HTML snippet that sets `data-tempest-theme` on `<html>` before React\n * hydrates. Inline this in `<head>` to avoid a flash of the wrong theme on\n * first paint.\n *\n * @example\n * <script dangerouslySetInnerHTML={{ __html: themeInitScript() }} />\n */\nexport function themeInitScript(options: GetInitialThemeOptions = {}): string {\n const storageKey = options.storageKey ?? \"tempest-theme\";\n const defaultTheme = options.defaultTheme ?? \"system\";\n return `\n(function(){try{\n var key=${
|
|
1
|
+
{"version":3,"file":"initial-theme.cjs","names":["storage","require_storage","getInitialTheme","options","defaultTheme","mode","storageKey","scriptLiteral","value","char","themeInitScript"],"sources":["../../src/theme/initial-theme.ts"],"sourcesContent":["import { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface GetInitialThemeOptions {\n /** localStorage key. Default: `\"tempest-theme\"`. */\n storageKey?: string;\n /** Theme used when nothing is stored and the user has no system preference. */\n defaultTheme?: ThemeMode;\n}\n\n/**\n * Inline-safe theme resolution intended for early bootstrap (e.g. inside the\n * `<head>` script that prevents the flash of incorrect theme). Reads from\n * localStorage and falls back to `prefers-color-scheme`.\n */\nexport function getInitialTheme(options: GetInitialThemeOptions = {}): ResolvedTheme {\n const { storageKey = \"tempest-theme\", defaultTheme = \"system\" } = options;\n\n if (typeof window === \"undefined\") {\n return defaultTheme === \"dark\" ? \"dark\" : \"light\";\n }\n\n const stored = storage.getRaw(storageKey) as ThemeMode | null;\n const mode: ThemeMode = stored ?? defaultTheme;\n if (mode === \"dark\" || mode === \"light\") return mode;\n\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * A string as a JavaScript literal that is also safe inside an HTML `<script>`.\n *\n * `JSON.stringify` produces a valid literal but leaves `<`, `>`, `&` and the\n * line separators U+2028/U+2029 as they are, and the HTML parser ends a script\n * element at the first `</script` whatever JavaScript thinks of it: a\n * `storageKey` of `\"</script><script>alert(1)</script>\"` closed the snippet and\n * ran the second script. Each of those characters is written as its `\\uXXXX`\n * escape, which JavaScript reads back to the same string.\n *\n * @param value - The option value to embed.\n * @returns The quoted, escaped literal.\n */\nfunction scriptLiteral(value: string): string {\n return JSON.stringify(value).replace(\n /[<>&\\u2028\\u2029]/g,\n (char) => `\\\\u${char.charCodeAt(0).toString(16).padStart(4, \"0\")}`,\n );\n}\n\n/**\n * Plain HTML snippet that sets `data-tempest-theme` on `<html>` before React\n * hydrates. Inline this in `<head>` to avoid a flash of the wrong theme on\n * first paint.\n *\n * Both options are embedded through an escaper that keeps them inside the\n * `<script>` element whatever they contain, so the snippet is safe to inline\n * even when the storage key comes from configuration.\n *\n * @example\n * <script dangerouslySetInnerHTML={{ __html: themeInitScript() }} />\n */\nexport function themeInitScript(options: GetInitialThemeOptions = {}): string {\n const storageKey = options.storageKey ?? \"tempest-theme\";\n const defaultTheme = options.defaultTheme ?? \"system\";\n return `\n(function(){try{\n var key=${scriptLiteral(storageKey)};\n var def=${scriptLiteral(defaultTheme)};\n var stored=localStorage.getItem(key);\n var mode=stored||def;\n var resolved=mode===\"dark\"||mode===\"light\"?mode:(matchMedia(\"(prefers-color-scheme: dark)\").matches?\"dark\":\"light\");\n document.documentElement.setAttribute(\"data-tempest-theme\",resolved);\n}catch(e){}})();\n`.trim();\n}\n"],"mappings":"A,wCAeA,SAAgBE,EAAgB,EAAkC,CAAC,EAAkB,CACjF,GAAM,CAAE,aAAa,gBAAiB,eAAe,UAAaC,EAElE,GAAI,OAAO,OAAW,IAClB,OAAOC,IAAiB,OAAS,OAAS,QAI9C,IAAMC,EADSL,EAAAA,QAAQ,OAAOM,CACN,GAAUF,EAGlC,OAFIC,IAAS,QAAUA,IAAS,QAAgBA,EAEzC,OAAO,WAAW,8BAA8B,CAAC,CAAC,QAAU,OAAS,OAChF,CAeA,SAASE,EAAc,EAAuB,CAC1C,OAAO,KAAK,UAAUC,CAAK,CAAC,CAAC,QACzB,qBACC,GAAS,MAAMC,EAAK,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,GACnE,CACJ,CAcA,SAAgBC,EAAgB,EAAkC,CAAC,EAAW,CAC1E,IAAMJ,EAAaH,EAAQ,YAAc,gBACnCC,EAAeD,EAAQ,cAAgB,SAC7C,MAAO;;YAECI,EAAcD,CAAU,EAAE;YAC1BC,EAAcH,CAAY,EAAE;;;;;;EAMtC,KAAK,CACP,C"}
|
|
@@ -6,12 +6,15 @@ function t(t = {}) {
|
|
|
6
6
|
let i = e.getRaw(n) ?? r;
|
|
7
7
|
return i === "dark" || i === "light" ? i : window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
8
8
|
}
|
|
9
|
-
function n(e
|
|
10
|
-
|
|
9
|
+
function n(e) {
|
|
10
|
+
return JSON.stringify(e).replace(/[<>&\u2028\u2029]/g, (e) => `\\u${e.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
11
|
+
}
|
|
12
|
+
function r(e = {}) {
|
|
13
|
+
let t = e.storageKey ?? "tempest-theme", r = e.defaultTheme ?? "system";
|
|
11
14
|
return `
|
|
12
15
|
(function(){try{
|
|
13
|
-
var key=${
|
|
14
|
-
var def=${
|
|
16
|
+
var key=${n(t)};
|
|
17
|
+
var def=${n(r)};
|
|
15
18
|
var stored=localStorage.getItem(key);
|
|
16
19
|
var mode=stored||def;
|
|
17
20
|
var resolved=mode==="dark"||mode==="light"?mode:(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");
|
|
@@ -20,6 +23,6 @@ function n(e = {}) {
|
|
|
20
23
|
`.trim();
|
|
21
24
|
}
|
|
22
25
|
//#endregion
|
|
23
|
-
export { t as getInitialTheme,
|
|
26
|
+
export { t as getInitialTheme, r as themeInitScript };
|
|
24
27
|
|
|
25
28
|
//# sourceMappingURL=initial-theme.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initial-theme.js","names":["getInitialTheme","options","defaultTheme","mode","storage","storageKey","themeInitScript"],"sources":["../../src/theme/initial-theme.ts"],"sourcesContent":["import { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface GetInitialThemeOptions {\n /** localStorage key. Default: `\"tempest-theme\"`. */\n storageKey?: string;\n /** Theme used when nothing is stored and the user has no system preference. */\n defaultTheme?: ThemeMode;\n}\n\n/**\n * Inline-safe theme resolution intended for early bootstrap (e.g. inside the\n * `<head>` script that prevents the flash of incorrect theme). Reads from\n * localStorage and falls back to `prefers-color-scheme`.\n */\nexport function getInitialTheme(options: GetInitialThemeOptions = {}): ResolvedTheme {\n const { storageKey = \"tempest-theme\", defaultTheme = \"system\" } = options;\n\n if (typeof window === \"undefined\") {\n return defaultTheme === \"dark\" ? \"dark\" : \"light\";\n }\n\n const stored = storage.getRaw(storageKey) as ThemeMode | null;\n const mode: ThemeMode = stored ?? defaultTheme;\n if (mode === \"dark\" || mode === \"light\") return mode;\n\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * Plain HTML snippet that sets `data-tempest-theme` on `<html>` before React\n * hydrates. Inline this in `<head>` to avoid a flash of the wrong theme on\n * first paint.\n *\n * @example\n * <script dangerouslySetInnerHTML={{ __html: themeInitScript() }} />\n */\nexport function themeInitScript(options: GetInitialThemeOptions = {}): string {\n const storageKey = options.storageKey ?? \"tempest-theme\";\n const defaultTheme = options.defaultTheme ?? \"system\";\n return `\n(function(){try{\n var key=${
|
|
1
|
+
{"version":3,"file":"initial-theme.js","names":["getInitialTheme","options","defaultTheme","mode","storage","storageKey","scriptLiteral","value","char","themeInitScript"],"sources":["../../src/theme/initial-theme.ts"],"sourcesContent":["import { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface GetInitialThemeOptions {\n /** localStorage key. Default: `\"tempest-theme\"`. */\n storageKey?: string;\n /** Theme used when nothing is stored and the user has no system preference. */\n defaultTheme?: ThemeMode;\n}\n\n/**\n * Inline-safe theme resolution intended for early bootstrap (e.g. inside the\n * `<head>` script that prevents the flash of incorrect theme). Reads from\n * localStorage and falls back to `prefers-color-scheme`.\n */\nexport function getInitialTheme(options: GetInitialThemeOptions = {}): ResolvedTheme {\n const { storageKey = \"tempest-theme\", defaultTheme = \"system\" } = options;\n\n if (typeof window === \"undefined\") {\n return defaultTheme === \"dark\" ? \"dark\" : \"light\";\n }\n\n const stored = storage.getRaw(storageKey) as ThemeMode | null;\n const mode: ThemeMode = stored ?? defaultTheme;\n if (mode === \"dark\" || mode === \"light\") return mode;\n\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * A string as a JavaScript literal that is also safe inside an HTML `<script>`.\n *\n * `JSON.stringify` produces a valid literal but leaves `<`, `>`, `&` and the\n * line separators U+2028/U+2029 as they are, and the HTML parser ends a script\n * element at the first `</script` whatever JavaScript thinks of it: a\n * `storageKey` of `\"</script><script>alert(1)</script>\"` closed the snippet and\n * ran the second script. Each of those characters is written as its `\\uXXXX`\n * escape, which JavaScript reads back to the same string.\n *\n * @param value - The option value to embed.\n * @returns The quoted, escaped literal.\n */\nfunction scriptLiteral(value: string): string {\n return JSON.stringify(value).replace(\n /[<>&\\u2028\\u2029]/g,\n (char) => `\\\\u${char.charCodeAt(0).toString(16).padStart(4, \"0\")}`,\n );\n}\n\n/**\n * Plain HTML snippet that sets `data-tempest-theme` on `<html>` before React\n * hydrates. Inline this in `<head>` to avoid a flash of the wrong theme on\n * first paint.\n *\n * Both options are embedded through an escaper that keeps them inside the\n * `<script>` element whatever they contain, so the snippet is safe to inline\n * even when the storage key comes from configuration.\n *\n * @example\n * <script dangerouslySetInnerHTML={{ __html: themeInitScript() }} />\n */\nexport function themeInitScript(options: GetInitialThemeOptions = {}): string {\n const storageKey = options.storageKey ?? \"tempest-theme\";\n const defaultTheme = options.defaultTheme ?? \"system\";\n return `\n(function(){try{\n var key=${scriptLiteral(storageKey)};\n var def=${scriptLiteral(defaultTheme)};\n var stored=localStorage.getItem(key);\n var mode=stored||def;\n var resolved=mode===\"dark\"||mode===\"light\"?mode:(matchMedia(\"(prefers-color-scheme: dark)\").matches?\"dark\":\"light\");\n document.documentElement.setAttribute(\"data-tempest-theme\",resolved);\n}catch(e){}})();\n`.trim();\n}\n"],"mappings":";;AAeA,SAAgBA,EAAgB,IAAkC,CAAC,GAAkB;CACjF,IAAM,EAAE,gBAAa,iBAAiB,kBAAe,aAAaC;CAElE,IAAI,OAAO,SAAW,KAClB,OAAOC,MAAiB,SAAS,SAAS;CAI9C,IAAMC,IADSC,EAAQ,OAAOC,CACN,KAAUH;CAGlC,OAFIC,MAAS,UAAUA,MAAS,UAAgBA,IAEzC,OAAO,WAAW,8BAA8B,CAAC,CAAC,UAAU,SAAS;AAChF;AAeA,SAASG,EAAc,GAAuB;CAC1C,OAAO,KAAK,UAAUC,CAAK,CAAC,CAAC,QACzB,uBACC,MAAS,MAAMC,EAAK,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,GACnE;AACJ;AAcA,SAAgBC,EAAgB,IAAkC,CAAC,GAAW;CAC1E,IAAMJ,IAAaJ,EAAQ,cAAc,iBACnCC,IAAeD,EAAQ,gBAAgB;CAC7C,OAAO;;YAECK,EAAcD,CAAU,EAAE;YAC1BC,EAAcJ,CAAY,EAAE;;;;;;EAMtC,KAAK;AACP"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./storage.cjs"),t=require("./base64.cjs");let n=require("fflate");var r=`~tgz1:`;function i(e){let i=JSON.stringify(e),a=(0,n.gzipSync)(new TextEncoder().encode(i));return`${r}${t.bytesToBase64(a)}`}function
|
|
1
|
+
const e=require("./storage.cjs"),t=require("./base64.cjs");let n=require("fflate");var r=`~tgz1:`;function i(e){let i=JSON.stringify(e),a=(0,n.gzipSync)(new TextEncoder().encode(i));return`${r}${t.bytesToBase64(a)}`}var a=67108864;function o(e){if(!e.startsWith(r))return JSON.parse(e);let i=t.base64ToBytes(e.slice(6));if(i.byteLength>=4){let e=new DataView(i.buffer,i.byteOffset,i.byteLength).getUint32(i.byteLength-4,!0);if(e>a)throw RangeError(`Compressed value declares ${e} bytes once inflated, above the ${a}-byte limit.`)}let o=(0,n.gunzipSync)(i);return JSON.parse(new TextDecoder().decode(o))}var s={serialize:i,deserialize:o},c=e.createJsonStorage(s);exports.compressToString=i,exports.compressedStorage=c,exports.compressedStorageCodec=s,exports.decompressFromString=o;
|
|
2
2
|
//# sourceMappingURL=compressed-storage.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compressed-storage.cjs","names":["gzipSync","bytesToBase64","
|
|
1
|
+
{"version":3,"file":"compressed-storage.cjs","names":["gzipSync","bytesToBase64","base64ToBytes","gunzipSync","createJsonStorage","require_storage","require_base64","fflate","MARKER","compressToString","json","value","bytes","MAX_DECOMPRESSED_BYTES","decompressFromString","raw","compressed","size","compressedStorageCodec","compressedStorage"],"sources":["../../src/utils/compressed-storage.ts"],"sourcesContent":["import { gunzipSync, gzipSync } from \"fflate\";\n\nimport { base64ToBytes, bytesToBase64 } from \"./base64\";\n\nimport { createJsonStorage, type JsonStorage, type StorageCodec } from \"./storage\";\n\n/**\n * Gzip-backed `localStorage`, for payloads that would otherwise eat the quota.\n *\n * Kept in its own module rather than folded into {@link storage} so that\n * importing the plain typed wrapper never drags `fflate` into the bundle.\n *\n * @tempest-limits empty-catch — every operation here is best-effort by\n * contract, exactly like {@link storage}. `localStorage` throws on quota\n * exhaustion, in Safari private mode, and when a cross-origin frame has storage\n * blocked; a caller persisting state has no recovery to run and no message to\n * show, so the write is dropped and the value stays in memory for the session.\n */\n\n/**\n * Prefix stamped on every value this module writes.\n *\n * It makes the format self-describing, which is what lets a read fall back to\n * plain JSON: a key that predates compression — or that a degraded write stored\n * uncompressed — is still readable, so turning compression on for an existing\n * key does not orphan the data already there. The character also cannot open a\n * JSON document, so the two cases can never be confused.\n */\nconst MARKER = \"~tgz1:\";\n\n/**\n * Serialize a value to a gzipped, base64 string carrying the format marker.\n *\n * Base64 costs a third more characters than the raw compressed bytes, and\n * `localStorage` bills two bytes per character on top of that. Packing the\n * bytes into UTF-16 code units directly would be denser, but lone surrogates\n * survive neither every storage implementation nor a JSON round-trip, and a\n * save that decodes to garbage is far worse than one that is bigger. Even with\n * that overhead a typical JSON document lands well under a third of its\n * uncompressed size.\n *\n * @typeParam T - The value being stored.\n * @param value - Any JSON-serializable value.\n * @returns The encoded string, ready for `localStorage`.\n */\nexport function compressToString<T>(value: T): string {\n const json = JSON.stringify(value);\n const bytes = gzipSync(new TextEncoder().encode(json));\n return `${MARKER}${bytesToBase64(bytes)}`;\n}\n\n/**\n * Largest decompressed size {@link decompressFromString} will allocate: 64 MiB.\n *\n * Deflate reaches ~1000:1 on repetitive input (measured with fflate: 20 MB of\n * `\"A\"` gzips to 19 551 bytes, ratio 1023), so the ~5 MB a `localStorage` origin\n * holds could ask for gigabytes on read. A legitimate value is far below this:\n * it had to pass through `JSON.stringify` and `gzipSync` in the same tab first.\n */\nconst MAX_DECOMPRESSED_BYTES = 64 * 1024 * 1024;\n\n/**\n * Decode a string produced by {@link compressToString}.\n *\n * A string without the marker is parsed as plain JSON, so values written before\n * compression was enabled — or by a write that fell back after `gzipSync`\n * failed — still read back.\n *\n * The output size is bounded before anything is inflated. fflate sizes its\n * output buffer from the gzip trailer (`ISIZE`) and never grows it — measured: a\n * 1 MB payload whose trailer claims 10 bytes inflates to exactly 10 — so\n * refusing a trailer above {@link MAX_DECOMPRESSED_BYTES} bounds the allocation,\n * whatever the trailer says about the real data. A corrupted or hostile value\n * fails fast instead of taking the tab's memory.\n *\n * @typeParam T - The expected value shape.\n * @param raw - The stored string.\n * @returns The decoded value.\n * @throws If the payload is neither valid compressed data nor valid JSON, or\n * declares a decompressed size above 64 MiB.\n */\nexport function decompressFromString<T>(raw: string): T {\n if (!raw.startsWith(MARKER)) return JSON.parse(raw) as T;\n const compressed = base64ToBytes(raw.slice(MARKER.length));\n if (compressed.byteLength >= 4) {\n const trailer = new DataView(\n compressed.buffer,\n compressed.byteOffset,\n compressed.byteLength,\n );\n const size = trailer.getUint32(compressed.byteLength - 4, true);\n if (size > MAX_DECOMPRESSED_BYTES) {\n throw new RangeError(\n `Compressed value declares ${size} bytes once inflated, above the ` +\n `${MAX_DECOMPRESSED_BYTES}-byte limit.`,\n );\n }\n }\n const bytes = gunzipSync(compressed);\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n}\n\n/**\n * Codec pair for {@link useLocalStorage}, so a compressed key gets the hook's\n * cross-tab sync and SSR guard for free.\n *\n * @example\n * const [save, setSave] = useLocalStorage(\"save\", EMPTY_SAVE, compressedStorageCodec);\n */\nexport const compressedStorageCodec: StorageCodec = {\n serialize: compressToString,\n deserialize: decompressFromString,\n};\n\n/**\n * Typed `localStorage` wrapper that gzips what it writes.\n *\n * {@link createJsonStorage} with {@link compressedStorageCodec}, so it is the\n * same implementation as {@link storage} and genuinely interchangeable with it —\n * `get`, `set` and `remove`, differing only in how the value is encoded. It used\n * to be a hand-written copy that had no `remove`, which made that promise false\n * for anybody who took the docstring at its word.\n *\n * When compression itself fails the value is written as plain JSON rather than\n * dropped: a slightly larger record still loads, an absent one does not. Only a\n * storage-level failure — quota, blocked storage — loses the write.\n */\nexport const compressedStorage: JsonStorage = createJsonStorage(compressedStorageCodec);\n"],"mappings":"A,mFA4BA,IAAMQ,EAAS,SAiBf,SAAgBC,EAAoB,EAAkB,CAClD,IAAMC,EAAO,KAAK,UAAUC,CAAK,EAC3BC,GAAA,EAAQZ,EAAAA,SAAAA,CAAS,IAAI,YAAY,CAAC,CAAC,OAAOU,CAAI,CAAC,EACrD,MAAO,GAAGF,IAASP,EAAAA,cAAcW,CAAK,GAC1C,CAUA,IAAMC,EAAyB,SAsB/B,SAAgBC,EAAwB,EAAgB,CACpD,GAAI,CAACC,EAAI,WAAWP,CAAM,EAAG,OAAO,KAAK,MAAMO,CAAG,EAClD,IAAMC,EAAad,EAAAA,cAAca,EAAI,MAAM,CAAa,CAAC,EACzD,GAAIC,EAAW,YAAc,EAAG,CAM5B,IAAMC,EAAO,IALO,SAChBD,EAAW,OACXA,EAAW,WACXA,EAAW,UAEF,CAAA,CAAQ,UAAUA,EAAW,WAAa,EAAG,EAAI,EAC9D,GAAIC,EAAOJ,EACP,MAAU,WACN,6BAA6BI,EAAK,kCAC3BJ,EAAuB,aAClC,CAER,CACA,IAAMD,GAAA,EAAQT,EAAAA,WAAAA,CAAWa,CAAU,EACnC,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC,OAAOJ,CAAK,CAAC,CACrD,CASA,IAAaM,EAAuC,CAChD,UAAWT,EACX,YAAaK,CACjB,EAeaK,EAAiCf,EAAAA,kBAAkBc,CAAsB,E"}
|
|
@@ -7,16 +7,22 @@ function o(e) {
|
|
|
7
7
|
let t = JSON.stringify(e), r = i(new TextEncoder().encode(t));
|
|
8
8
|
return `${a}${n(r)}`;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
var s = 67108864;
|
|
11
|
+
function c(e) {
|
|
11
12
|
if (!e.startsWith(a)) return JSON.parse(e);
|
|
12
|
-
let n =
|
|
13
|
-
|
|
13
|
+
let n = t(e.slice(6));
|
|
14
|
+
if (n.byteLength >= 4) {
|
|
15
|
+
let e = new DataView(n.buffer, n.byteOffset, n.byteLength).getUint32(n.byteLength - 4, !0);
|
|
16
|
+
if (e > s) throw RangeError(`Compressed value declares ${e} bytes once inflated, above the ${s}-byte limit.`);
|
|
17
|
+
}
|
|
18
|
+
let i = r(n);
|
|
19
|
+
return JSON.parse(new TextDecoder().decode(i));
|
|
14
20
|
}
|
|
15
|
-
var
|
|
21
|
+
var l = {
|
|
16
22
|
serialize: o,
|
|
17
|
-
deserialize:
|
|
18
|
-
},
|
|
23
|
+
deserialize: c
|
|
24
|
+
}, u = e(l);
|
|
19
25
|
//#endregion
|
|
20
|
-
export { o as compressToString,
|
|
26
|
+
export { o as compressToString, u as compressedStorage, l as compressedStorageCodec, c as decompressFromString };
|
|
21
27
|
|
|
22
28
|
//# sourceMappingURL=compressed-storage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compressed-storage.js","names":["MARKER","compressToString","json","value","bytes","gzipSync","bytesToBase64","decompressFromString","raw","
|
|
1
|
+
{"version":3,"file":"compressed-storage.js","names":["MARKER","compressToString","json","value","bytes","gzipSync","bytesToBase64","MAX_DECOMPRESSED_BYTES","decompressFromString","raw","compressed","base64ToBytes","size","gunzipSync","compressedStorageCodec","compressedStorage","createJsonStorage"],"sources":["../../src/utils/compressed-storage.ts"],"sourcesContent":["import { gunzipSync, gzipSync } from \"fflate\";\n\nimport { base64ToBytes, bytesToBase64 } from \"./base64\";\n\nimport { createJsonStorage, type JsonStorage, type StorageCodec } from \"./storage\";\n\n/**\n * Gzip-backed `localStorage`, for payloads that would otherwise eat the quota.\n *\n * Kept in its own module rather than folded into {@link storage} so that\n * importing the plain typed wrapper never drags `fflate` into the bundle.\n *\n * @tempest-limits empty-catch — every operation here is best-effort by\n * contract, exactly like {@link storage}. `localStorage` throws on quota\n * exhaustion, in Safari private mode, and when a cross-origin frame has storage\n * blocked; a caller persisting state has no recovery to run and no message to\n * show, so the write is dropped and the value stays in memory for the session.\n */\n\n/**\n * Prefix stamped on every value this module writes.\n *\n * It makes the format self-describing, which is what lets a read fall back to\n * plain JSON: a key that predates compression — or that a degraded write stored\n * uncompressed — is still readable, so turning compression on for an existing\n * key does not orphan the data already there. The character also cannot open a\n * JSON document, so the two cases can never be confused.\n */\nconst MARKER = \"~tgz1:\";\n\n/**\n * Serialize a value to a gzipped, base64 string carrying the format marker.\n *\n * Base64 costs a third more characters than the raw compressed bytes, and\n * `localStorage` bills two bytes per character on top of that. Packing the\n * bytes into UTF-16 code units directly would be denser, but lone surrogates\n * survive neither every storage implementation nor a JSON round-trip, and a\n * save that decodes to garbage is far worse than one that is bigger. Even with\n * that overhead a typical JSON document lands well under a third of its\n * uncompressed size.\n *\n * @typeParam T - The value being stored.\n * @param value - Any JSON-serializable value.\n * @returns The encoded string, ready for `localStorage`.\n */\nexport function compressToString<T>(value: T): string {\n const json = JSON.stringify(value);\n const bytes = gzipSync(new TextEncoder().encode(json));\n return `${MARKER}${bytesToBase64(bytes)}`;\n}\n\n/**\n * Largest decompressed size {@link decompressFromString} will allocate: 64 MiB.\n *\n * Deflate reaches ~1000:1 on repetitive input (measured with fflate: 20 MB of\n * `\"A\"` gzips to 19 551 bytes, ratio 1023), so the ~5 MB a `localStorage` origin\n * holds could ask for gigabytes on read. A legitimate value is far below this:\n * it had to pass through `JSON.stringify` and `gzipSync` in the same tab first.\n */\nconst MAX_DECOMPRESSED_BYTES = 64 * 1024 * 1024;\n\n/**\n * Decode a string produced by {@link compressToString}.\n *\n * A string without the marker is parsed as plain JSON, so values written before\n * compression was enabled — or by a write that fell back after `gzipSync`\n * failed — still read back.\n *\n * The output size is bounded before anything is inflated. fflate sizes its\n * output buffer from the gzip trailer (`ISIZE`) and never grows it — measured: a\n * 1 MB payload whose trailer claims 10 bytes inflates to exactly 10 — so\n * refusing a trailer above {@link MAX_DECOMPRESSED_BYTES} bounds the allocation,\n * whatever the trailer says about the real data. A corrupted or hostile value\n * fails fast instead of taking the tab's memory.\n *\n * @typeParam T - The expected value shape.\n * @param raw - The stored string.\n * @returns The decoded value.\n * @throws If the payload is neither valid compressed data nor valid JSON, or\n * declares a decompressed size above 64 MiB.\n */\nexport function decompressFromString<T>(raw: string): T {\n if (!raw.startsWith(MARKER)) return JSON.parse(raw) as T;\n const compressed = base64ToBytes(raw.slice(MARKER.length));\n if (compressed.byteLength >= 4) {\n const trailer = new DataView(\n compressed.buffer,\n compressed.byteOffset,\n compressed.byteLength,\n );\n const size = trailer.getUint32(compressed.byteLength - 4, true);\n if (size > MAX_DECOMPRESSED_BYTES) {\n throw new RangeError(\n `Compressed value declares ${size} bytes once inflated, above the ` +\n `${MAX_DECOMPRESSED_BYTES}-byte limit.`,\n );\n }\n }\n const bytes = gunzipSync(compressed);\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n}\n\n/**\n * Codec pair for {@link useLocalStorage}, so a compressed key gets the hook's\n * cross-tab sync and SSR guard for free.\n *\n * @example\n * const [save, setSave] = useLocalStorage(\"save\", EMPTY_SAVE, compressedStorageCodec);\n */\nexport const compressedStorageCodec: StorageCodec = {\n serialize: compressToString,\n deserialize: decompressFromString,\n};\n\n/**\n * Typed `localStorage` wrapper that gzips what it writes.\n *\n * {@link createJsonStorage} with {@link compressedStorageCodec}, so it is the\n * same implementation as {@link storage} and genuinely interchangeable with it —\n * `get`, `set` and `remove`, differing only in how the value is encoded. It used\n * to be a hand-written copy that had no `remove`, which made that promise false\n * for anybody who took the docstring at its word.\n *\n * When compression itself fails the value is written as plain JSON rather than\n * dropped: a slightly larger record still loads, an absent one does not. Only a\n * storage-level failure — quota, blocked storage — loses the write.\n */\nexport const compressedStorage: JsonStorage = createJsonStorage(compressedStorageCodec);\n"],"mappings":";;;;AA4BA,IAAMA,IAAS;AAiBf,SAAgBC,EAAoB,GAAkB;CAClD,IAAMC,IAAO,KAAK,UAAUC,CAAK,GAC3BC,IAAQC,EAAS,IAAI,YAAY,CAAC,CAAC,OAAOH,CAAI,CAAC;CACrD,OAAO,GAAGF,IAASM,EAAcF,CAAK;AAC1C;AAUA,IAAMG,IAAyB;AAsB/B,SAAgBC,EAAwB,GAAgB;CACpD,IAAI,CAACC,EAAI,WAAWT,CAAM,GAAG,OAAO,KAAK,MAAMS,CAAG;CAClD,IAAMC,IAAaC,EAAcF,EAAI,MAAM,CAAa,CAAC;CACzD,IAAIC,EAAW,cAAc,GAAG;EAM5B,IAAME,IAAO,IALO,SAChBF,EAAW,QACXA,EAAW,YACXA,EAAW,UAEF,CAAA,CAAQ,UAAUA,EAAW,aAAa,GAAG,EAAI;EAC9D,IAAIE,IAAOL,GACP,MAAU,WACN,6BAA6BK,EAAK,kCAC3BL,EAAuB,aAClC;CAER;CACA,IAAMH,IAAQS,EAAWH,CAAU;CACnC,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC,OAAON,CAAK,CAAC;AACrD;AASA,IAAaU,IAAuC;CAChD,WAAWb;CACX,aAAaO;AACjB,GAeaO,IAAiCC,EAAkBF,CAAsB"}
|
package/dist/utils/objects.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function e(e){if(typeof e!=`object`||!e)return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function t(e,t){let n={};for(let r of t)Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}function n(e,t){let n=new Set(t),r={};for(let t of Object.keys(e))n.has(t)||(r[t]=e[t]);return r}
|
|
1
|
+
function e(e){if(typeof e!=`object`||!e)return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function t(e,t){let n={};for(let r of t)Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}function n(e,t){let n=new Set(t),r={};for(let t of Object.keys(e))n.has(t)||(r[t]=e[t]);return r}var r=new Set([`__proto__`,`constructor`,`prototype`]);function i(t,n){if(!e(t)||!e(n))return n===void 0?t:n;let a={...t};for(let t of Object.keys(n)){if(r.has(t))continue;let o=n[t],s=a[t];a[t]=e(s)&&e(o)?i(s,o):o}return a}function a(t){return t==null?!0:typeof t==`string`||Array.isArray(t)?t.length===0:t instanceof Map||t instanceof Set?t.size===0:e(t)?Object.keys(t).length===0:!1}exports.deepMerge=i,exports.isEmpty=a,exports.omit=n,exports.pick=t;
|
|
2
2
|
//# sourceMappingURL=objects.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"objects.cjs","names":["isPlainRecord","value","proto","pick","result","key","keys","obj","omit","exclude","deepMerge","target","source","sourceValue","targetValue","isEmpty"],"sources":["../../src/utils/objects.ts"],"sourcesContent":["/**\n * Determine whether a value is a plain object literal.\n *\n * Returns `false` for arrays, `null`, dates, maps, sets, and class instances.\n *\n * @internal\n */\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Create a new object containing only the given `keys` from `obj`.\n *\n * Missing keys are skipped. The input is never mutated.\n *\n * @example\n * pick({ id: 1, name: \"Ana\", age: 30 }, [\"id\", \"name\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n\n/**\n * Create a new object with the given `keys` removed from `obj`.\n *\n * The input is never mutated.\n *\n * @example\n * omit({ id: 1, name: \"Ana\", age: 30 }, [\"age\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\n const exclude = new Set<PropertyKey>(keys);\n const result = {} as Record<PropertyKey, unknown>;\n for (const key of Object.keys(obj) as (keyof T)[]) {\n if (!exclude.has(key)) {\n result[key] = obj[key];\n }\n }\n return result as Omit<T, K>;\n}\n\n/**\n * Recursively merge `source` into `target`, returning a new object.\n *\n * - Plain objects are merged key by key, recursing into nested plain objects.\n * - Arrays and non-plain values (dates, class instances, primitives) replace\n * the target value entirely — they are not merged element by element.\n * - Neither `target` nor `source` is mutated.\n *\n * @example\n * deepMerge(\n * { a: 1, nested: { x: 1, y: 2 } },\n * { nested: { y: 20, z: 30 } },\n * );\n * // { a: 1, nested: { x: 1, y: 20, z: 30 } }\n *\n * @example\n * deepMerge({ tags: [\"a\", \"b\"] }, { tags: [\"c\"] });\n * // { tags: [\"c\"] } (arrays replaced, not merged)\n */\nexport function deepMerge<T>(target: T, source: Partial<T>): T {\n if (!isPlainRecord(target) || !isPlainRecord(source)) {\n return (source === undefined ? target : (source as unknown as T)) as T;\n }\n const result: Record<string, unknown> = { ...target };\n for (const key of Object.keys(source)) {\n const sourceValue = (source as Record<string, unknown>)[key];\n const targetValue = result[key];\n if (isPlainRecord(targetValue) && isPlainRecord(sourceValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n return result as T;\n}\n\n/**\n * Check whether a value is considered \"empty\".\n *\n * Returns `true` for `null`, `undefined`, empty strings, empty arrays, empty\n * plain objects, and empty `Map`/`Set` instances. Returns `false` for numbers\n * (including `0` and `NaN`), booleans, and dates, which are never empty.\n *\n * @example\n * isEmpty(null); // true\n * isEmpty(\"\"); // true\n * isEmpty([]); // true\n * isEmpty({}); // true\n * isEmpty(new Map()); // true\n * isEmpty(0); // false\n * isEmpty(false); // false\n * isEmpty(new Date()); // false\n */\nexport function isEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (value instanceof Map || value instanceof Set) return value.size === 0;\n if (isPlainRecord(value)) return Object.keys(value).length === 0;\n return false;\n}\n"],"mappings":"AAOA,SAASA,EAAc,EAAkD,CACrE,GAAI,OAAOC,GAAU,WAAYA,EAAgB,MAAO,GACxD,IAAMC,EAAQ,OAAO,eAAeD,CAAK,EACzC,OAAOC,IAAU,OAAO,WAAaA,IAAU,IACnD,CAWA,SAAgBC,EAA0C,EAAQ,EAAuB,CACrF,IAAMC,EAAS,CAAC,EAChB,IAAK,IAAMC,KAAOC,EACV,OAAO,UAAU,eAAe,KAAKC,EAAKF,CAAG,IAC7C,EAAOA,GAAOE,EAAIF,IAG1B,OAAOD,CACX,CAWA,SAAgBI,EAA0C,EAAQ,EAAuB,CACrF,IAAMC,EAAU,IAAI,IAAiBH,CAAI,EACnCF,EAAS,CAAC,EAChB,IAAK,IAAMC,KAAO,OAAO,KAAKE,CAAG,EACxBE,EAAQ,IAAIJ,CAAG,IAChB,EAAOA,GAAOE,EAAIF,IAG1B,OAAOD,CACX,
|
|
1
|
+
{"version":3,"file":"objects.cjs","names":["isPlainRecord","value","proto","pick","result","key","keys","obj","omit","exclude","UNSAFE_MERGE_KEYS","deepMerge","target","source","sourceValue","targetValue","isEmpty"],"sources":["../../src/utils/objects.ts"],"sourcesContent":["/**\n * Determine whether a value is a plain object literal.\n *\n * Returns `false` for arrays, `null`, dates, maps, sets, and class instances.\n *\n * @internal\n */\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Create a new object containing only the given `keys` from `obj`.\n *\n * Missing keys are skipped. The input is never mutated.\n *\n * @example\n * pick({ id: 1, name: \"Ana\", age: 30 }, [\"id\", \"name\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n\n/**\n * Create a new object with the given `keys` removed from `obj`.\n *\n * The input is never mutated.\n *\n * @example\n * omit({ id: 1, name: \"Ana\", age: 30 }, [\"age\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\n const exclude = new Set<PropertyKey>(keys);\n const result = {} as Record<PropertyKey, unknown>;\n for (const key of Object.keys(obj) as (keyof T)[]) {\n if (!exclude.has(key)) {\n result[key] = obj[key];\n }\n }\n return result as Omit<T, K>;\n}\n\n/** Keys {@link deepMerge} never copies from `source`. */\nconst UNSAFE_MERGE_KEYS: ReadonlySet<string> = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n/**\n * Recursively merge `source` into `target`, returning a new object.\n *\n * - Plain objects are merged key by key, recursing into nested plain objects.\n * - Arrays and non-plain values (dates, class instances, primitives) replace\n * the target value entirely — they are not merged element by element.\n * - Neither `target` nor `source` is mutated.\n * - `__proto__`, `constructor` and `prototype` keys in `source` are skipped.\n * Assigning `result[\"__proto__\"]` does not create a key: it **replaces the\n * result's prototype**, so a `JSON.parse`d settings blob carrying\n * `{\"__proto__\": {\"isAdmin\": true}}` produced an object whose `isAdmin` read\n * `true` through inheritance while `Object.keys` showed nothing. No global\n * prototype is touched either way; the skip removes the surprise, and the\n * other two names go with it because nothing legitimate merges into them.\n *\n * @example\n * deepMerge(\n * { a: 1, nested: { x: 1, y: 2 } },\n * { nested: { y: 20, z: 30 } },\n * );\n * // { a: 1, nested: { x: 1, y: 20, z: 30 } }\n *\n * @example\n * deepMerge({ tags: [\"a\", \"b\"] }, { tags: [\"c\"] });\n * // { tags: [\"c\"] } (arrays replaced, not merged)\n */\nexport function deepMerge<T>(target: T, source: Partial<T>): T {\n if (!isPlainRecord(target) || !isPlainRecord(source)) {\n return (source === undefined ? target : (source as unknown as T)) as T;\n }\n const result: Record<string, unknown> = { ...target };\n for (const key of Object.keys(source)) {\n if (UNSAFE_MERGE_KEYS.has(key)) continue;\n const sourceValue = (source as Record<string, unknown>)[key];\n const targetValue = result[key];\n if (isPlainRecord(targetValue) && isPlainRecord(sourceValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n return result as T;\n}\n\n/**\n * Check whether a value is considered \"empty\".\n *\n * Returns `true` for `null`, `undefined`, empty strings, empty arrays, empty\n * plain objects, and empty `Map`/`Set` instances. Returns `false` for numbers\n * (including `0` and `NaN`), booleans, and dates, which are never empty.\n *\n * @example\n * isEmpty(null); // true\n * isEmpty(\"\"); // true\n * isEmpty([]); // true\n * isEmpty({}); // true\n * isEmpty(new Map()); // true\n * isEmpty(0); // false\n * isEmpty(false); // false\n * isEmpty(new Date()); // false\n */\nexport function isEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (value instanceof Map || value instanceof Set) return value.size === 0;\n if (isPlainRecord(value)) return Object.keys(value).length === 0;\n return false;\n}\n"],"mappings":"AAOA,SAASA,EAAc,EAAkD,CACrE,GAAI,OAAOC,GAAU,WAAYA,EAAgB,MAAO,GACxD,IAAMC,EAAQ,OAAO,eAAeD,CAAK,EACzC,OAAOC,IAAU,OAAO,WAAaA,IAAU,IACnD,CAWA,SAAgBC,EAA0C,EAAQ,EAAuB,CACrF,IAAMC,EAAS,CAAC,EAChB,IAAK,IAAMC,KAAOC,EACV,OAAO,UAAU,eAAe,KAAKC,EAAKF,CAAG,IAC7C,EAAOA,GAAOE,EAAIF,IAG1B,OAAOD,CACX,CAWA,SAAgBI,EAA0C,EAAQ,EAAuB,CACrF,IAAMC,EAAU,IAAI,IAAiBH,CAAI,EACnCF,EAAS,CAAC,EAChB,IAAK,IAAMC,KAAO,OAAO,KAAKE,CAAG,EACxBE,EAAQ,IAAIJ,CAAG,IAChB,EAAOA,GAAOE,EAAIF,IAG1B,OAAOD,CACX,CAGA,IAAMM,EAAyC,IAAI,IAAI,CAAC,YAAa,cAAe,WAAW,CAAC,EA4BhG,SAAgBC,EAAa,EAAW,EAAuB,CAC3D,GAAI,CAACX,EAAcY,CAAM,GAAK,CAACZ,EAAca,CAAM,EAC/C,OAAQA,IAAW,IAAA,GAAYD,EAAUC,EAE7C,IAAMT,EAAkC,CAAE,GAAGQ,CAAO,EACpD,IAAK,IAAMP,KAAO,OAAO,KAAKQ,CAAM,EAAG,CACnC,GAAIH,EAAkB,IAAIL,CAAG,EAAG,SAChC,IAAMS,EAAeD,EAAmCR,GAClDU,EAAcX,EAAOC,GAC3B,AAGID,EAAOC,GAHPL,EAAce,CAAW,GAAKf,EAAcc,CAAW,EACzCH,EAAUI,EAAaD,CAAW,EAElCA,CAEtB,CACA,OAAOV,CACX,CAmBA,SAAgBY,EAAQ,EAAyB,CAM7C,OALIf,GAAU,KAAoC,GAC9C,OAAOA,GAAU,UACjB,MAAM,QAAQA,CAAK,EAAUA,EAAM,SAAW,EAC9CA,aAAiB,KAAOA,aAAiB,IAAYA,EAAM,OAAS,EACpED,EAAcC,CAAK,EAAU,OAAO,KAAKA,CAAK,CAAC,CAAC,SAAW,EACxD,EACX,C"}
|
package/dist/utils/objects.js
CHANGED
|
@@ -14,19 +14,25 @@ function n(e, t) {
|
|
|
14
14
|
for (let t of Object.keys(e)) n.has(t) || (r[t] = e[t]);
|
|
15
15
|
return r;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
var r = /* @__PURE__ */ new Set([
|
|
18
|
+
"__proto__",
|
|
19
|
+
"constructor",
|
|
20
|
+
"prototype"
|
|
21
|
+
]);
|
|
22
|
+
function i(t, n) {
|
|
18
23
|
if (!e(t) || !e(n)) return n === void 0 ? t : n;
|
|
19
|
-
let
|
|
24
|
+
let a = { ...t };
|
|
20
25
|
for (let t of Object.keys(n)) {
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
if (r.has(t)) continue;
|
|
27
|
+
let o = n[t], s = a[t];
|
|
28
|
+
a[t] = e(s) && e(o) ? i(s, o) : o;
|
|
23
29
|
}
|
|
24
|
-
return
|
|
30
|
+
return a;
|
|
25
31
|
}
|
|
26
|
-
function
|
|
32
|
+
function a(t) {
|
|
27
33
|
return t == null ? !0 : typeof t == "string" || Array.isArray(t) ? t.length === 0 : t instanceof Map || t instanceof Set ? t.size === 0 : e(t) ? Object.keys(t).length === 0 : !1;
|
|
28
34
|
}
|
|
29
35
|
//#endregion
|
|
30
|
-
export {
|
|
36
|
+
export { i as deepMerge, a as isEmpty, n as omit, t as pick };
|
|
31
37
|
|
|
32
38
|
//# sourceMappingURL=objects.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"objects.js","names":["isPlainRecord","value","proto","pick","result","key","keys","obj","omit","exclude","deepMerge","target","source","sourceValue","targetValue","isEmpty"],"sources":["../../src/utils/objects.ts"],"sourcesContent":["/**\n * Determine whether a value is a plain object literal.\n *\n * Returns `false` for arrays, `null`, dates, maps, sets, and class instances.\n *\n * @internal\n */\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Create a new object containing only the given `keys` from `obj`.\n *\n * Missing keys are skipped. The input is never mutated.\n *\n * @example\n * pick({ id: 1, name: \"Ana\", age: 30 }, [\"id\", \"name\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n\n/**\n * Create a new object with the given `keys` removed from `obj`.\n *\n * The input is never mutated.\n *\n * @example\n * omit({ id: 1, name: \"Ana\", age: 30 }, [\"age\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\n const exclude = new Set<PropertyKey>(keys);\n const result = {} as Record<PropertyKey, unknown>;\n for (const key of Object.keys(obj) as (keyof T)[]) {\n if (!exclude.has(key)) {\n result[key] = obj[key];\n }\n }\n return result as Omit<T, K>;\n}\n\n/**\n * Recursively merge `source` into `target`, returning a new object.\n *\n * - Plain objects are merged key by key, recursing into nested plain objects.\n * - Arrays and non-plain values (dates, class instances, primitives) replace\n * the target value entirely — they are not merged element by element.\n * - Neither `target` nor `source` is mutated.\n *\n * @example\n * deepMerge(\n * { a: 1, nested: { x: 1, y: 2 } },\n * { nested: { y: 20, z: 30 } },\n * );\n * // { a: 1, nested: { x: 1, y: 20, z: 30 } }\n *\n * @example\n * deepMerge({ tags: [\"a\", \"b\"] }, { tags: [\"c\"] });\n * // { tags: [\"c\"] } (arrays replaced, not merged)\n */\nexport function deepMerge<T>(target: T, source: Partial<T>): T {\n if (!isPlainRecord(target) || !isPlainRecord(source)) {\n return (source === undefined ? target : (source as unknown as T)) as T;\n }\n const result: Record<string, unknown> = { ...target };\n for (const key of Object.keys(source)) {\n const sourceValue = (source as Record<string, unknown>)[key];\n const targetValue = result[key];\n if (isPlainRecord(targetValue) && isPlainRecord(sourceValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n return result as T;\n}\n\n/**\n * Check whether a value is considered \"empty\".\n *\n * Returns `true` for `null`, `undefined`, empty strings, empty arrays, empty\n * plain objects, and empty `Map`/`Set` instances. Returns `false` for numbers\n * (including `0` and `NaN`), booleans, and dates, which are never empty.\n *\n * @example\n * isEmpty(null); // true\n * isEmpty(\"\"); // true\n * isEmpty([]); // true\n * isEmpty({}); // true\n * isEmpty(new Map()); // true\n * isEmpty(0); // false\n * isEmpty(false); // false\n * isEmpty(new Date()); // false\n */\nexport function isEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (value instanceof Map || value instanceof Set) return value.size === 0;\n if (isPlainRecord(value)) return Object.keys(value).length === 0;\n return false;\n}\n"],"mappings":";AAOA,SAASA,EAAc,GAAkD;CACrE,IAAI,OAAOC,KAAU,aAAYA,GAAgB,OAAO;CACxD,IAAMC,IAAQ,OAAO,eAAeD,CAAK;CACzC,OAAOC,MAAU,OAAO,aAAaA,MAAU;AACnD;AAWA,SAAgBC,EAA0C,GAAQ,GAAuB;CACrF,IAAMC,IAAS,CAAC;CAChB,KAAK,IAAMC,KAAOC,GACd,AAAI,OAAO,UAAU,eAAe,KAAKC,GAAKF,CAAG,MAC7C,EAAOA,KAAOE,EAAIF;CAG1B,OAAOD;AACX;AAWA,SAAgBI,EAA0C,GAAQ,GAAuB;CACrF,IAAMC,IAAU,IAAI,IAAiBH,CAAI,GACnCF,IAAS,CAAC;CAChB,KAAK,IAAMC,KAAO,OAAO,KAAKE,CAAG,GAC7B,AAAKE,EAAQ,IAAIJ,CAAG,MAChB,EAAOA,KAAOE,EAAIF;CAG1B,OAAOD;AACX;
|
|
1
|
+
{"version":3,"file":"objects.js","names":["isPlainRecord","value","proto","pick","result","key","keys","obj","omit","exclude","UNSAFE_MERGE_KEYS","deepMerge","target","source","sourceValue","targetValue","isEmpty"],"sources":["../../src/utils/objects.ts"],"sourcesContent":["/**\n * Determine whether a value is a plain object literal.\n *\n * Returns `false` for arrays, `null`, dates, maps, sets, and class instances.\n *\n * @internal\n */\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Create a new object containing only the given `keys` from `obj`.\n *\n * Missing keys are skipped. The input is never mutated.\n *\n * @example\n * pick({ id: 1, name: \"Ana\", age: 30 }, [\"id\", \"name\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n\n/**\n * Create a new object with the given `keys` removed from `obj`.\n *\n * The input is never mutated.\n *\n * @example\n * omit({ id: 1, name: \"Ana\", age: 30 }, [\"age\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\n const exclude = new Set<PropertyKey>(keys);\n const result = {} as Record<PropertyKey, unknown>;\n for (const key of Object.keys(obj) as (keyof T)[]) {\n if (!exclude.has(key)) {\n result[key] = obj[key];\n }\n }\n return result as Omit<T, K>;\n}\n\n/** Keys {@link deepMerge} never copies from `source`. */\nconst UNSAFE_MERGE_KEYS: ReadonlySet<string> = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n/**\n * Recursively merge `source` into `target`, returning a new object.\n *\n * - Plain objects are merged key by key, recursing into nested plain objects.\n * - Arrays and non-plain values (dates, class instances, primitives) replace\n * the target value entirely — they are not merged element by element.\n * - Neither `target` nor `source` is mutated.\n * - `__proto__`, `constructor` and `prototype` keys in `source` are skipped.\n * Assigning `result[\"__proto__\"]` does not create a key: it **replaces the\n * result's prototype**, so a `JSON.parse`d settings blob carrying\n * `{\"__proto__\": {\"isAdmin\": true}}` produced an object whose `isAdmin` read\n * `true` through inheritance while `Object.keys` showed nothing. No global\n * prototype is touched either way; the skip removes the surprise, and the\n * other two names go with it because nothing legitimate merges into them.\n *\n * @example\n * deepMerge(\n * { a: 1, nested: { x: 1, y: 2 } },\n * { nested: { y: 20, z: 30 } },\n * );\n * // { a: 1, nested: { x: 1, y: 20, z: 30 } }\n *\n * @example\n * deepMerge({ tags: [\"a\", \"b\"] }, { tags: [\"c\"] });\n * // { tags: [\"c\"] } (arrays replaced, not merged)\n */\nexport function deepMerge<T>(target: T, source: Partial<T>): T {\n if (!isPlainRecord(target) || !isPlainRecord(source)) {\n return (source === undefined ? target : (source as unknown as T)) as T;\n }\n const result: Record<string, unknown> = { ...target };\n for (const key of Object.keys(source)) {\n if (UNSAFE_MERGE_KEYS.has(key)) continue;\n const sourceValue = (source as Record<string, unknown>)[key];\n const targetValue = result[key];\n if (isPlainRecord(targetValue) && isPlainRecord(sourceValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n return result as T;\n}\n\n/**\n * Check whether a value is considered \"empty\".\n *\n * Returns `true` for `null`, `undefined`, empty strings, empty arrays, empty\n * plain objects, and empty `Map`/`Set` instances. Returns `false` for numbers\n * (including `0` and `NaN`), booleans, and dates, which are never empty.\n *\n * @example\n * isEmpty(null); // true\n * isEmpty(\"\"); // true\n * isEmpty([]); // true\n * isEmpty({}); // true\n * isEmpty(new Map()); // true\n * isEmpty(0); // false\n * isEmpty(false); // false\n * isEmpty(new Date()); // false\n */\nexport function isEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (value instanceof Map || value instanceof Set) return value.size === 0;\n if (isPlainRecord(value)) return Object.keys(value).length === 0;\n return false;\n}\n"],"mappings":";AAOA,SAASA,EAAc,GAAkD;CACrE,IAAI,OAAOC,KAAU,aAAYA,GAAgB,OAAO;CACxD,IAAMC,IAAQ,OAAO,eAAeD,CAAK;CACzC,OAAOC,MAAU,OAAO,aAAaA,MAAU;AACnD;AAWA,SAAgBC,EAA0C,GAAQ,GAAuB;CACrF,IAAMC,IAAS,CAAC;CAChB,KAAK,IAAMC,KAAOC,GACd,AAAI,OAAO,UAAU,eAAe,KAAKC,GAAKF,CAAG,MAC7C,EAAOA,KAAOE,EAAIF;CAG1B,OAAOD;AACX;AAWA,SAAgBI,EAA0C,GAAQ,GAAuB;CACrF,IAAMC,IAAU,IAAI,IAAiBH,CAAI,GACnCF,IAAS,CAAC;CAChB,KAAK,IAAMC,KAAO,OAAO,KAAKE,CAAG,GAC7B,AAAKE,EAAQ,IAAIJ,CAAG,MAChB,EAAOA,KAAOE,EAAIF;CAG1B,OAAOD;AACX;AAGA,IAAMM,oBAAyC,IAAI,IAAI;CAAC;CAAa;CAAe;AAAW,CAAC;AA4BhG,SAAgBC,EAAa,GAAW,GAAuB;CAC3D,IAAI,CAACX,EAAcY,CAAM,KAAK,CAACZ,EAAca,CAAM,GAC/C,OAAQA,MAAW,KAAA,IAAYD,IAAUC;CAE7C,IAAMT,IAAkC,EAAE,GAAGQ,EAAO;CACpD,KAAK,IAAMP,KAAO,OAAO,KAAKQ,CAAM,GAAG;EACnC,IAAIH,EAAkB,IAAIL,CAAG,GAAG;EAChC,IAAMS,IAAeD,EAAmCR,IAClDU,IAAcX,EAAOC;EAC3B,AAGID,EAAOC,KAHPL,EAAce,CAAW,KAAKf,EAAcc,CAAW,IACzCH,EAAUI,GAAaD,CAAW,IAElCA;CAEtB;CACA,OAAOV;AACX;AAmBA,SAAgBY,EAAQ,GAAyB;CAM7C,OALIf,KAAU,OAAoC,KAC9C,OAAOA,KAAU,YACjB,MAAM,QAAQA,CAAK,IAAUA,EAAM,WAAW,IAC9CA,aAAiB,OAAOA,aAAiB,MAAYA,EAAM,SAAS,IACpED,EAAcC,CAAK,IAAU,OAAO,KAAKA,CAAK,CAAC,CAAC,WAAW,IACxD;AACX"}
|
package/dist/utils/safe-url.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var e=new Set([`http:`,`https:`,`mailto:`,`tel:`,`sms:`]),t=new Set([`http:`,`https:`,`blob:`]),n=/^data:image\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\s]+$/i;function r(e){return e.replace(/[\u0000-\u0020\u007F]/g,``).trim()}function i(e){return/^[a-z][a-z0-9+.-]*:/i.test(e)?e.slice(0,e.indexOf(`:`)+1).toLowerCase():null}function a(t){let n=r(t);if(!n)return null;let a=i(n);return a===null||e.has(a)?n:null}function o(e){let t=r(e);return t?n.test(t)?t:/^data:/i.test(t)?null:a(t):null}function s(e){let a=r(e);if(!a)return null;if(n.test(a))return e;let o=i(a);return o===null||t.has(o)?e:null}exports.safeAttachmentUrl=s,exports.safeImageUrl=o,exports.safeLinkUrl=a;
|
|
1
|
+
var e=new Set([`http:`,`https:`,`mailto:`,`tel:`,`sms:`]),t=new Set([`http:`,`https:`,`blob:`]),n=/^data:image\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\s]+$/i;function r(e){return e.replace(/[\u0000-\u0020\u007F]/g,``).trim()}function i(e){return/^[a-z][a-z0-9+.-]*:/i.test(e)?e.slice(0,e.indexOf(`:`)+1).toLowerCase():null}function a(t){let n=r(t);if(!n)return null;let a=i(n);return a===null||e.has(a)?n:null}function o(e){let t=r(e);return t?n.test(t)?t:/^data:/i.test(t)?null:a(t):null}function s(e){let a=r(e);if(!a)return null;if(n.test(a))return e;let o=i(a);return o===null||t.has(o)?e:null}function c(e){if(e!==void 0&&a(e)!==null)return e}exports.safeAttachmentUrl=s,exports.safeImageUrl=o,exports.safeLinkUrl=a,exports.vettedHref=c;
|
|
2
2
|
//# sourceMappingURL=safe-url.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-url.cjs","names":["LINK_SCHEMES","RESOURCE_SCHEMES","IMAGE_DATA_TYPES","normalize","raw","schemeOf","url","safeLinkUrl","scheme","safeImageUrl","safeAttachmentUrl"],"sources":["../../src/utils/safe-url.ts"],"sourcesContent":["/**\n * URL vetting for every `href` and `src` the SDK fills from somebody else's data:\n * links and images in rendered Markdown, and the file row of a chat attachment.\n *\n * This is a security boundary. Text that arrives from a user — a comment, a ticket\n * description, a chat attachment — reaches the DOM as a real `href`, and\n * `javascript:alert(1)` in a link is script execution on click. React 19 replaces\n * a `javascript:` href with a throwing stub, but the peer range admits React 18,\n * which only warns: measured with react-dom 18.3.1, `renderToStaticMarkup` wrote\n * `href=\"javascript:...\"` verbatim, and clicking it ran the script in Chromium 153\n * and Firefox 155 — `download` on the anchor did not stop it (#447). So the\n * scheme is checked against an allowlist rather than a blocklist: a blocklist has\n * to enumerate `javascript:`, `JaVaScRiPt:`, `java\\tscript:`, `javascript:`\n * and whatever comes next, and misses the one nobody thought of.\n */\n\n/** Schemes a link may use. Anything else is dropped. */\nconst LINK_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"sms:\"]);\n\n/**\n * Schemes a fetched resource — an attachment, a download — may use.\n *\n * `blob:` is here because an app shows a message while its upload is still in\n * flight by pointing at `URL.createObjectURL(file)`. A blob URL is same-origin\n * bytes the page already holds; it cannot run code by being followed.\n */\nconst RESOURCE_SCHEMES = new Set([\"http:\", \"https:\", \"blob:\"]);\n\n/**\n * Image `data:` types that are safe to inline.\n *\n * `svg+xml` is deliberately absent: an SVG is a document, it can carry `<script>`\n * and event handlers, and browsers execute them when it loads from a `data:` URL in\n * some contexts. The raster formats cannot carry code.\n */\nconst IMAGE_DATA_TYPES = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\\s]+$/i;\n\n/**\n * Strip the characters a scheme can be smuggled through.\n *\n * `java\\tscript:` and `java\\nscript:` are both parsed as `javascript:` by browsers,\n * and a leading control character defeats a naive `startsWith` check.\n */\nfunction normalize(raw: string): string {\n // eslint-disable-next-line no-control-regex -- stripping control characters is the point\n return raw.replace(/[\\u0000-\\u0020\\u007F]/g, \"\").trim();\n}\n\n/**\n * The scheme of a normalized URL, lower-cased with its colon, or `null` for a\n * relative URL.\n */\nfunction schemeOf(url: string): string | null {\n if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) return null;\n return url.slice(0, url.indexOf(\":\") + 1).toLowerCase();\n}\n\n/**\n * The href to render, or `null` when the URL must not be linked.\n *\n * Relative URLs (`/docs`, `./x`, `#anchor`, `foo/bar`) are kept: they cannot carry\n * a scheme, so they cannot execute anything, and dropping them would break every\n * in-app link. Protocol-relative (`//host/path`) inherits the page scheme and is\n * kept for the same reason.\n *\n * @example\n * safeLinkUrl(\"https://x.dev\"); // \"https://x.dev\"\n * safeLinkUrl(\"java\\tscript:alert(1)\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe href, or `null`.\n */\nexport function safeLinkUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n const scheme = schemeOf(url);\n return scheme === null || LINK_SCHEMES.has(scheme) ? url : null;\n}\n\n/**\n * The `src` to render for an image, or `null`.\n *\n * Accepts what {@link safeLinkUrl} accepts, plus raster `data:image/...;base64`.\n *\n * @example\n * safeImageUrl(\"data:image/png;base64,iVBORw0KGgo=\"); // kept\n * safeImageUrl(\"data:image/svg+xml;base64,PHN2Zz4=\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe src, or `null`.\n */\nexport function safeImageUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return url;\n if (/^data:/i.test(url)) return null;\n return safeLinkUrl(url);\n}\n\n/**\n * The URL to fetch or download an attachment from, or `null`.\n *\n * The allowlist is `http:`, `https:`, `blob:`, relative URLs and the raster\n * `data:image/` types {@link safeImageUrl} accepts. A file row is an `<a href\n * download>`, and the `download` attribute does not stop a `javascript:` href\n * from running (measured in Chromium 153 and Firefox 155, #447), so the scheme\n * has to be checked before the anchor exists. `mailto:`/`tel:` are left out: an\n * attachment is bytes, not a contact.\n *\n * The scheme is judged on the normalized form, but the URL comes back **as given**:\n * normalizing deletes every space, and `/files/relatório final.pdf` is a real\n * attachment path. Returning the original is safe because the normalization only\n * removes characters, a superset of what the browser's URL parser strips, so it\n * can never hide a scheme the browser would see.\n *\n * @example\n * const href = safeAttachmentUrl(attachment.url);\n * return href ? <a href={href} download>{name}</a> : <span>{name}</span>;\n *\n * @param raw - URL as the sender supplied it.\n * @returns A safe URL, or `null` when it must not reach an `href`.\n */\nexport function safeAttachmentUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return raw;\n const scheme = schemeOf(url);\n return scheme === null || RESOURCE_SCHEMES.has(scheme) ? raw : null;\n}\n"],"mappings":"AAiBA,IAAMA,EAAe,IAAI,IAAI,CAAC,QAAS,SAAU,UAAW,OAAQ,MAAM,CAAC,EASrEC,EAAmB,IAAI,IAAI,CAAC,QAAS,SAAU,OAAO,CAAC,EASvDC,EAAmB,qEAQzB,SAASC,EAAU,EAAqB,CAEpC,OAAOC,EAAI,QAAQ,yBAA0B,EAAE,CAAC,CAAC,KAAK,CAC1D,CAMA,SAASC,EAAS,EAA4B,CAE1C,MADK,uBAAuB,KAAKC,CAAG,EAC7BA,EAAI,MAAM,EAAGA,EAAI,QAAQ,GAAG,EAAI,CAAC,CAAC,CAAC,YAAY,EADR,IAElD,CAiBA,SAAgBC,EAAY,EAA4B,CACpD,IAAMD,EAAMH,EAAUC,CAAG,EACzB,GAAI,CAACE,EAAK,OAAO,KACjB,IAAME,EAASH,EAASC,CAAG,EAC3B,OAAOE,IAAW,MAAQR,EAAa,IAAIQ,CAAM,EAAIF,EAAM,IAC/D,CAcA,SAAgBG,EAAa,EAA4B,CACrD,IAAMH,EAAMH,EAAUC,CAAG,EAIzB,OAHKE,EACDJ,EAAiB,KAAKI,CAAG,EAAUA,EACnC,UAAU,KAAKA,CAAG,EAAU,KACzBC,EAAYD,CAAG,EAHL,IAIrB,CAyBA,SAAgBI,EAAkB,EAA4B,CAC1D,IAAMJ,EAAMH,EAAUC,CAAG,EACzB,GAAI,CAACE,EAAK,OAAO,KACjB,GAAIJ,EAAiB,KAAKI,CAAG,EAAG,OAAOF,EACvC,IAAMI,EAASH,EAASC,CAAG,EAC3B,OAAOE,IAAW,MAAQP,EAAiB,IAAIO,CAAM,EAAIJ,EAAM,IACnE,C"}
|
|
1
|
+
{"version":3,"file":"safe-url.cjs","names":["LINK_SCHEMES","RESOURCE_SCHEMES","IMAGE_DATA_TYPES","normalize","raw","schemeOf","url","safeLinkUrl","scheme","safeImageUrl","safeAttachmentUrl","vettedHref"],"sources":["../../src/utils/safe-url.ts"],"sourcesContent":["/**\n * URL vetting for every `href` and `src` the SDK fills from somebody else's data:\n * links and images in rendered Markdown, and the file row of a chat attachment.\n *\n * This is a security boundary. Text that arrives from a user — a comment, a ticket\n * description, a chat attachment — reaches the DOM as a real `href`, and\n * `javascript:alert(1)` in a link is script execution on click. React 19 replaces\n * a `javascript:` href with a throwing stub, but the peer range admits React 18,\n * which only warns: measured with react-dom 18.3.1, `renderToStaticMarkup` wrote\n * `href=\"javascript:...\"` verbatim, and clicking it ran the script in Chromium 153\n * and Firefox 155 — `download` on the anchor did not stop it (#447). So the\n * scheme is checked against an allowlist rather than a blocklist: a blocklist has\n * to enumerate `javascript:`, `JaVaScRiPt:`, `java\\tscript:`, `javascript:`\n * and whatever comes next, and misses the one nobody thought of.\n */\n\n/** Schemes a link may use. Anything else is dropped. */\nconst LINK_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"sms:\"]);\n\n/**\n * Schemes a fetched resource — an attachment, a download — may use.\n *\n * `blob:` is here because an app shows a message while its upload is still in\n * flight by pointing at `URL.createObjectURL(file)`. A blob URL is same-origin\n * bytes the page already holds; it cannot run code by being followed.\n */\nconst RESOURCE_SCHEMES = new Set([\"http:\", \"https:\", \"blob:\"]);\n\n/**\n * Image `data:` types that are safe to inline.\n *\n * `svg+xml` is deliberately absent: an SVG is a document, it can carry `<script>`\n * and event handlers, and browsers execute them when it loads from a `data:` URL in\n * some contexts. The raster formats cannot carry code.\n */\nconst IMAGE_DATA_TYPES = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\\s]+$/i;\n\n/**\n * Strip the characters a scheme can be smuggled through.\n *\n * `java\\tscript:` and `java\\nscript:` are both parsed as `javascript:` by browsers,\n * and a leading control character defeats a naive `startsWith` check.\n */\nfunction normalize(raw: string): string {\n // eslint-disable-next-line no-control-regex -- stripping control characters is the point\n return raw.replace(/[\\u0000-\\u0020\\u007F]/g, \"\").trim();\n}\n\n/**\n * The scheme of a normalized URL, lower-cased with its colon, or `null` for a\n * relative URL.\n */\nfunction schemeOf(url: string): string | null {\n if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) return null;\n return url.slice(0, url.indexOf(\":\") + 1).toLowerCase();\n}\n\n/**\n * The href to render, or `null` when the URL must not be linked.\n *\n * Relative URLs (`/docs`, `./x`, `#anchor`, `foo/bar`) are kept: they cannot carry\n * a scheme, so they cannot execute anything, and dropping them would break every\n * in-app link. Protocol-relative (`//host/path`) inherits the page scheme and is\n * kept for the same reason.\n *\n * @example\n * safeLinkUrl(\"https://x.dev\"); // \"https://x.dev\"\n * safeLinkUrl(\"java\\tscript:alert(1)\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe href, or `null`.\n */\nexport function safeLinkUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n const scheme = schemeOf(url);\n return scheme === null || LINK_SCHEMES.has(scheme) ? url : null;\n}\n\n/**\n * The `src` to render for an image, or `null`.\n *\n * Accepts what {@link safeLinkUrl} accepts, plus raster `data:image/...;base64`.\n *\n * @example\n * safeImageUrl(\"data:image/png;base64,iVBORw0KGgo=\"); // kept\n * safeImageUrl(\"data:image/svg+xml;base64,PHN2Zz4=\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe src, or `null`.\n */\nexport function safeImageUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return url;\n if (/^data:/i.test(url)) return null;\n return safeLinkUrl(url);\n}\n\n/**\n * The URL to fetch or download an attachment from, or `null`.\n *\n * The allowlist is `http:`, `https:`, `blob:`, relative URLs and the raster\n * `data:image/` types {@link safeImageUrl} accepts. A file row is an `<a href\n * download>`, and the `download` attribute does not stop a `javascript:` href\n * from running (measured in Chromium 153 and Firefox 155, #447), so the scheme\n * has to be checked before the anchor exists. `mailto:`/`tel:` are left out: an\n * attachment is bytes, not a contact.\n *\n * The scheme is judged on the normalized form, but the URL comes back **as given**:\n * normalizing deletes every space, and `/files/relatório final.pdf` is a real\n * attachment path. Returning the original is safe because the normalization only\n * removes characters, a superset of what the browser's URL parser strips, so it\n * can never hide a scheme the browser would see.\n *\n * @example\n * const href = safeAttachmentUrl(attachment.url);\n * return href ? <a href={href} download>{name}</a> : <span>{name}</span>;\n *\n * @param raw - URL as the sender supplied it.\n * @returns A safe URL, or `null` when it must not reach an `href`.\n */\nexport function safeAttachmentUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return raw;\n const scheme = schemeOf(url);\n return scheme === null || RESOURCE_SCHEMES.has(scheme) ? raw : null;\n}\n\n/**\n * The `href` a component renders from a navigation prop, or `undefined`.\n *\n * {@link safeLinkUrl}'s allowlist, but the URL comes back **as given**, like\n * {@link safeAttachmentUrl}: an app route such as `/arquivos/relatório final` must\n * keep its space, and the normalization only removes characters the browser's\n * parser also strips, so it cannot hide a scheme. `Button`, `Breadcrumbs`,\n * `NavigationMenu`, `Sidebar`, `AuthForm` and `AuthPage` call it so a menu or a\n * call to action built from API data never puts a clickable `javascript:` link\n * on the page — React 19 blocks one, React 18 (inside the peer range) does not.\n *\n * @internal\n * @param raw - The prop as the app passed it.\n * @returns `raw` when it is safe to link, `undefined` when it is absent or not.\n */\nexport function vettedHref(raw: string | undefined): string | undefined {\n if (raw === undefined || safeLinkUrl(raw) === null) return undefined;\n return raw;\n}\n"],"mappings":"AAiBA,IAAMA,EAAe,IAAI,IAAI,CAAC,QAAS,SAAU,UAAW,OAAQ,MAAM,CAAC,EASrEC,EAAmB,IAAI,IAAI,CAAC,QAAS,SAAU,OAAO,CAAC,EASvDC,EAAmB,qEAQzB,SAASC,EAAU,EAAqB,CAEpC,OAAOC,EAAI,QAAQ,yBAA0B,EAAE,CAAC,CAAC,KAAK,CAC1D,CAMA,SAASC,EAAS,EAA4B,CAE1C,MADK,uBAAuB,KAAKC,CAAG,EAC7BA,EAAI,MAAM,EAAGA,EAAI,QAAQ,GAAG,EAAI,CAAC,CAAC,CAAC,YAAY,EADR,IAElD,CAiBA,SAAgBC,EAAY,EAA4B,CACpD,IAAMD,EAAMH,EAAUC,CAAG,EACzB,GAAI,CAACE,EAAK,OAAO,KACjB,IAAME,EAASH,EAASC,CAAG,EAC3B,OAAOE,IAAW,MAAQR,EAAa,IAAIQ,CAAM,EAAIF,EAAM,IAC/D,CAcA,SAAgBG,EAAa,EAA4B,CACrD,IAAMH,EAAMH,EAAUC,CAAG,EAIzB,OAHKE,EACDJ,EAAiB,KAAKI,CAAG,EAAUA,EACnC,UAAU,KAAKA,CAAG,EAAU,KACzBC,EAAYD,CAAG,EAHL,IAIrB,CAyBA,SAAgBI,EAAkB,EAA4B,CAC1D,IAAMJ,EAAMH,EAAUC,CAAG,EACzB,GAAI,CAACE,EAAK,OAAO,KACjB,GAAIJ,EAAiB,KAAKI,CAAG,EAAG,OAAOF,EACvC,IAAMI,EAASH,EAASC,CAAG,EAC3B,OAAOE,IAAW,MAAQP,EAAiB,IAAIO,CAAM,EAAIJ,EAAM,IACnE,CAiBA,SAAgBO,EAAW,EAA6C,CACpE,GAAIP,IAAQ,IAAA,IAAaG,EAAYH,CAAG,IAAM,KAC9C,OAAOA,CACX,C"}
|
package/dist/utils/safe-url.js
CHANGED
|
@@ -33,7 +33,10 @@ function s(e) {
|
|
|
33
33
|
let o = i(a);
|
|
34
34
|
return o === null || t.has(o) ? e : null;
|
|
35
35
|
}
|
|
36
|
+
function c(e) {
|
|
37
|
+
if (e !== void 0 && a(e) !== null) return e;
|
|
38
|
+
}
|
|
36
39
|
//#endregion
|
|
37
|
-
export { s as safeAttachmentUrl, o as safeImageUrl, a as safeLinkUrl };
|
|
40
|
+
export { s as safeAttachmentUrl, o as safeImageUrl, a as safeLinkUrl, c as vettedHref };
|
|
38
41
|
|
|
39
42
|
//# sourceMappingURL=safe-url.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-url.js","names":["LINK_SCHEMES","RESOURCE_SCHEMES","IMAGE_DATA_TYPES","normalize","raw","schemeOf","url","safeLinkUrl","scheme","safeImageUrl","safeAttachmentUrl"],"sources":["../../src/utils/safe-url.ts"],"sourcesContent":["/**\n * URL vetting for every `href` and `src` the SDK fills from somebody else's data:\n * links and images in rendered Markdown, and the file row of a chat attachment.\n *\n * This is a security boundary. Text that arrives from a user — a comment, a ticket\n * description, a chat attachment — reaches the DOM as a real `href`, and\n * `javascript:alert(1)` in a link is script execution on click. React 19 replaces\n * a `javascript:` href with a throwing stub, but the peer range admits React 18,\n * which only warns: measured with react-dom 18.3.1, `renderToStaticMarkup` wrote\n * `href=\"javascript:...\"` verbatim, and clicking it ran the script in Chromium 153\n * and Firefox 155 — `download` on the anchor did not stop it (#447). So the\n * scheme is checked against an allowlist rather than a blocklist: a blocklist has\n * to enumerate `javascript:`, `JaVaScRiPt:`, `java\\tscript:`, `javascript:`\n * and whatever comes next, and misses the one nobody thought of.\n */\n\n/** Schemes a link may use. Anything else is dropped. */\nconst LINK_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"sms:\"]);\n\n/**\n * Schemes a fetched resource — an attachment, a download — may use.\n *\n * `blob:` is here because an app shows a message while its upload is still in\n * flight by pointing at `URL.createObjectURL(file)`. A blob URL is same-origin\n * bytes the page already holds; it cannot run code by being followed.\n */\nconst RESOURCE_SCHEMES = new Set([\"http:\", \"https:\", \"blob:\"]);\n\n/**\n * Image `data:` types that are safe to inline.\n *\n * `svg+xml` is deliberately absent: an SVG is a document, it can carry `<script>`\n * and event handlers, and browsers execute them when it loads from a `data:` URL in\n * some contexts. The raster formats cannot carry code.\n */\nconst IMAGE_DATA_TYPES = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\\s]+$/i;\n\n/**\n * Strip the characters a scheme can be smuggled through.\n *\n * `java\\tscript:` and `java\\nscript:` are both parsed as `javascript:` by browsers,\n * and a leading control character defeats a naive `startsWith` check.\n */\nfunction normalize(raw: string): string {\n // eslint-disable-next-line no-control-regex -- stripping control characters is the point\n return raw.replace(/[\\u0000-\\u0020\\u007F]/g, \"\").trim();\n}\n\n/**\n * The scheme of a normalized URL, lower-cased with its colon, or `null` for a\n * relative URL.\n */\nfunction schemeOf(url: string): string | null {\n if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) return null;\n return url.slice(0, url.indexOf(\":\") + 1).toLowerCase();\n}\n\n/**\n * The href to render, or `null` when the URL must not be linked.\n *\n * Relative URLs (`/docs`, `./x`, `#anchor`, `foo/bar`) are kept: they cannot carry\n * a scheme, so they cannot execute anything, and dropping them would break every\n * in-app link. Protocol-relative (`//host/path`) inherits the page scheme and is\n * kept for the same reason.\n *\n * @example\n * safeLinkUrl(\"https://x.dev\"); // \"https://x.dev\"\n * safeLinkUrl(\"java\\tscript:alert(1)\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe href, or `null`.\n */\nexport function safeLinkUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n const scheme = schemeOf(url);\n return scheme === null || LINK_SCHEMES.has(scheme) ? url : null;\n}\n\n/**\n * The `src` to render for an image, or `null`.\n *\n * Accepts what {@link safeLinkUrl} accepts, plus raster `data:image/...;base64`.\n *\n * @example\n * safeImageUrl(\"data:image/png;base64,iVBORw0KGgo=\"); // kept\n * safeImageUrl(\"data:image/svg+xml;base64,PHN2Zz4=\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe src, or `null`.\n */\nexport function safeImageUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return url;\n if (/^data:/i.test(url)) return null;\n return safeLinkUrl(url);\n}\n\n/**\n * The URL to fetch or download an attachment from, or `null`.\n *\n * The allowlist is `http:`, `https:`, `blob:`, relative URLs and the raster\n * `data:image/` types {@link safeImageUrl} accepts. A file row is an `<a href\n * download>`, and the `download` attribute does not stop a `javascript:` href\n * from running (measured in Chromium 153 and Firefox 155, #447), so the scheme\n * has to be checked before the anchor exists. `mailto:`/`tel:` are left out: an\n * attachment is bytes, not a contact.\n *\n * The scheme is judged on the normalized form, but the URL comes back **as given**:\n * normalizing deletes every space, and `/files/relatório final.pdf` is a real\n * attachment path. Returning the original is safe because the normalization only\n * removes characters, a superset of what the browser's URL parser strips, so it\n * can never hide a scheme the browser would see.\n *\n * @example\n * const href = safeAttachmentUrl(attachment.url);\n * return href ? <a href={href} download>{name}</a> : <span>{name}</span>;\n *\n * @param raw - URL as the sender supplied it.\n * @returns A safe URL, or `null` when it must not reach an `href`.\n */\nexport function safeAttachmentUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return raw;\n const scheme = schemeOf(url);\n return scheme === null || RESOURCE_SCHEMES.has(scheme) ? raw : null;\n}\n"],"mappings":";AAiBA,IAAMA,oBAAe,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;CAAQ;AAAM,CAAC,GASrEC,oBAAmB,IAAI,IAAI;CAAC;CAAS;CAAU;AAAO,CAAC,GASvDC,IAAmB;AAQzB,SAASC,EAAU,GAAqB;CAEpC,OAAOC,EAAI,QAAQ,0BAA0B,EAAE,CAAC,CAAC,KAAK;AAC1D;AAMA,SAASC,EAAS,GAA4B;CAE1C,OADK,uBAAuB,KAAKC,CAAG,IAC7BA,EAAI,MAAM,GAAGA,EAAI,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY,IADR;AAElD;AAiBA,SAAgBC,EAAY,GAA4B;CACpD,IAAMD,IAAMH,EAAUC,CAAG;CACzB,IAAI,CAACE,GAAK,OAAO;CACjB,IAAME,IAASH,EAASC,CAAG;CAC3B,OAAOE,MAAW,QAAQR,EAAa,IAAIQ,CAAM,IAAIF,IAAM;AAC/D;AAcA,SAAgBG,EAAa,GAA4B;CACrD,IAAMH,IAAMH,EAAUC,CAAG;CAIzB,OAHKE,IACDJ,EAAiB,KAAKI,CAAG,IAAUA,IACnC,UAAU,KAAKA,CAAG,IAAU,OACzBC,EAAYD,CAAG,IAHL;AAIrB;AAyBA,SAAgBI,EAAkB,GAA4B;CAC1D,IAAMJ,IAAMH,EAAUC,CAAG;CACzB,IAAI,CAACE,GAAK,OAAO;CACjB,IAAIJ,EAAiB,KAAKI,CAAG,GAAG,OAAOF;CACvC,IAAMI,IAASH,EAASC,CAAG;CAC3B,OAAOE,MAAW,QAAQP,EAAiB,IAAIO,CAAM,IAAIJ,IAAM;AACnE"}
|
|
1
|
+
{"version":3,"file":"safe-url.js","names":["LINK_SCHEMES","RESOURCE_SCHEMES","IMAGE_DATA_TYPES","normalize","raw","schemeOf","url","safeLinkUrl","scheme","safeImageUrl","safeAttachmentUrl","vettedHref"],"sources":["../../src/utils/safe-url.ts"],"sourcesContent":["/**\n * URL vetting for every `href` and `src` the SDK fills from somebody else's data:\n * links and images in rendered Markdown, and the file row of a chat attachment.\n *\n * This is a security boundary. Text that arrives from a user — a comment, a ticket\n * description, a chat attachment — reaches the DOM as a real `href`, and\n * `javascript:alert(1)` in a link is script execution on click. React 19 replaces\n * a `javascript:` href with a throwing stub, but the peer range admits React 18,\n * which only warns: measured with react-dom 18.3.1, `renderToStaticMarkup` wrote\n * `href=\"javascript:...\"` verbatim, and clicking it ran the script in Chromium 153\n * and Firefox 155 — `download` on the anchor did not stop it (#447). So the\n * scheme is checked against an allowlist rather than a blocklist: a blocklist has\n * to enumerate `javascript:`, `JaVaScRiPt:`, `java\\tscript:`, `javascript:`\n * and whatever comes next, and misses the one nobody thought of.\n */\n\n/** Schemes a link may use. Anything else is dropped. */\nconst LINK_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"sms:\"]);\n\n/**\n * Schemes a fetched resource — an attachment, a download — may use.\n *\n * `blob:` is here because an app shows a message while its upload is still in\n * flight by pointing at `URL.createObjectURL(file)`. A blob URL is same-origin\n * bytes the page already holds; it cannot run code by being followed.\n */\nconst RESOURCE_SCHEMES = new Set([\"http:\", \"https:\", \"blob:\"]);\n\n/**\n * Image `data:` types that are safe to inline.\n *\n * `svg+xml` is deliberately absent: an SVG is a document, it can carry `<script>`\n * and event handlers, and browsers execute them when it loads from a `data:` URL in\n * some contexts. The raster formats cannot carry code.\n */\nconst IMAGE_DATA_TYPES = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\\s]+$/i;\n\n/**\n * Strip the characters a scheme can be smuggled through.\n *\n * `java\\tscript:` and `java\\nscript:` are both parsed as `javascript:` by browsers,\n * and a leading control character defeats a naive `startsWith` check.\n */\nfunction normalize(raw: string): string {\n // eslint-disable-next-line no-control-regex -- stripping control characters is the point\n return raw.replace(/[\\u0000-\\u0020\\u007F]/g, \"\").trim();\n}\n\n/**\n * The scheme of a normalized URL, lower-cased with its colon, or `null` for a\n * relative URL.\n */\nfunction schemeOf(url: string): string | null {\n if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) return null;\n return url.slice(0, url.indexOf(\":\") + 1).toLowerCase();\n}\n\n/**\n * The href to render, or `null` when the URL must not be linked.\n *\n * Relative URLs (`/docs`, `./x`, `#anchor`, `foo/bar`) are kept: they cannot carry\n * a scheme, so they cannot execute anything, and dropping them would break every\n * in-app link. Protocol-relative (`//host/path`) inherits the page scheme and is\n * kept for the same reason.\n *\n * @example\n * safeLinkUrl(\"https://x.dev\"); // \"https://x.dev\"\n * safeLinkUrl(\"java\\tscript:alert(1)\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe href, or `null`.\n */\nexport function safeLinkUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n const scheme = schemeOf(url);\n return scheme === null || LINK_SCHEMES.has(scheme) ? url : null;\n}\n\n/**\n * The `src` to render for an image, or `null`.\n *\n * Accepts what {@link safeLinkUrl} accepts, plus raster `data:image/...;base64`.\n *\n * @example\n * safeImageUrl(\"data:image/png;base64,iVBORw0KGgo=\"); // kept\n * safeImageUrl(\"data:image/svg+xml;base64,PHN2Zz4=\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe src, or `null`.\n */\nexport function safeImageUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return url;\n if (/^data:/i.test(url)) return null;\n return safeLinkUrl(url);\n}\n\n/**\n * The URL to fetch or download an attachment from, or `null`.\n *\n * The allowlist is `http:`, `https:`, `blob:`, relative URLs and the raster\n * `data:image/` types {@link safeImageUrl} accepts. A file row is an `<a href\n * download>`, and the `download` attribute does not stop a `javascript:` href\n * from running (measured in Chromium 153 and Firefox 155, #447), so the scheme\n * has to be checked before the anchor exists. `mailto:`/`tel:` are left out: an\n * attachment is bytes, not a contact.\n *\n * The scheme is judged on the normalized form, but the URL comes back **as given**:\n * normalizing deletes every space, and `/files/relatório final.pdf` is a real\n * attachment path. Returning the original is safe because the normalization only\n * removes characters, a superset of what the browser's URL parser strips, so it\n * can never hide a scheme the browser would see.\n *\n * @example\n * const href = safeAttachmentUrl(attachment.url);\n * return href ? <a href={href} download>{name}</a> : <span>{name}</span>;\n *\n * @param raw - URL as the sender supplied it.\n * @returns A safe URL, or `null` when it must not reach an `href`.\n */\nexport function safeAttachmentUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return raw;\n const scheme = schemeOf(url);\n return scheme === null || RESOURCE_SCHEMES.has(scheme) ? raw : null;\n}\n\n/**\n * The `href` a component renders from a navigation prop, or `undefined`.\n *\n * {@link safeLinkUrl}'s allowlist, but the URL comes back **as given**, like\n * {@link safeAttachmentUrl}: an app route such as `/arquivos/relatório final` must\n * keep its space, and the normalization only removes characters the browser's\n * parser also strips, so it cannot hide a scheme. `Button`, `Breadcrumbs`,\n * `NavigationMenu`, `Sidebar`, `AuthForm` and `AuthPage` call it so a menu or a\n * call to action built from API data never puts a clickable `javascript:` link\n * on the page — React 19 blocks one, React 18 (inside the peer range) does not.\n *\n * @internal\n * @param raw - The prop as the app passed it.\n * @returns `raw` when it is safe to link, `undefined` when it is absent or not.\n */\nexport function vettedHref(raw: string | undefined): string | undefined {\n if (raw === undefined || safeLinkUrl(raw) === null) return undefined;\n return raw;\n}\n"],"mappings":";AAiBA,IAAMA,oBAAe,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;CAAQ;AAAM,CAAC,GASrEC,oBAAmB,IAAI,IAAI;CAAC;CAAS;CAAU;AAAO,CAAC,GASvDC,IAAmB;AAQzB,SAASC,EAAU,GAAqB;CAEpC,OAAOC,EAAI,QAAQ,0BAA0B,EAAE,CAAC,CAAC,KAAK;AAC1D;AAMA,SAASC,EAAS,GAA4B;CAE1C,OADK,uBAAuB,KAAKC,CAAG,IAC7BA,EAAI,MAAM,GAAGA,EAAI,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY,IADR;AAElD;AAiBA,SAAgBC,EAAY,GAA4B;CACpD,IAAMD,IAAMH,EAAUC,CAAG;CACzB,IAAI,CAACE,GAAK,OAAO;CACjB,IAAME,IAASH,EAASC,CAAG;CAC3B,OAAOE,MAAW,QAAQR,EAAa,IAAIQ,CAAM,IAAIF,IAAM;AAC/D;AAcA,SAAgBG,EAAa,GAA4B;CACrD,IAAMH,IAAMH,EAAUC,CAAG;CAIzB,OAHKE,IACDJ,EAAiB,KAAKI,CAAG,IAAUA,IACnC,UAAU,KAAKA,CAAG,IAAU,OACzBC,EAAYD,CAAG,IAHL;AAIrB;AAyBA,SAAgBI,EAAkB,GAA4B;CAC1D,IAAMJ,IAAMH,EAAUC,CAAG;CACzB,IAAI,CAACE,GAAK,OAAO;CACjB,IAAIJ,EAAiB,KAAKI,CAAG,GAAG,OAAOF;CACvC,IAAMI,IAASH,EAASC,CAAG;CAC3B,OAAOE,MAAW,QAAQP,EAAiB,IAAIO,CAAM,IAAIJ,IAAM;AACnE;AAiBA,SAAgBO,EAAW,GAA6C;CACpE,IAAIP,MAAQ,KAAA,KAAaG,EAAYH,CAAG,MAAM,MAC9C,OAAOA;AACX"}
|
package/dist/vite/base-url.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function e(e){return e.endsWith(`/`)?e:`${e}/`}exports.basePrefix=e;
|
|
1
|
+
function e(e){return e.endsWith(`/`)?e:`${e}/`}function t(t,n){let r=e(t),i=n.split(/[?#]/)[0];return r!==`/`&&i.startsWith(r)?i.slice(r.length):i.replace(/^\/+/,``)}exports.basePrefix=e,exports.toOutputFile=t;
|
|
2
2
|
//# sourceMappingURL=base-url.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-url.cjs","names":["basePrefix","base"],"sources":["../../src/vite/base-url.ts"],"sourcesContent":["/**\n * The resolved Vite `base` as a prefix that always ends in a slash.\n *\n * One rule, three call sites: the manifest prefixes the URLs it precaches, and\n * the dev middleware matches the paths the browser asks for. They have to agree\n * by definition — when they drift, dev serves `/app/sw.js` while the manifest\n * precached `/sw.js`, and the symptom only appears on a subpath deploy, which is\n * the least exercised configuration there is.\n *\n * @param base - Vite's resolved `base`, with or without a trailing slash.\n * @returns The same base, guaranteed to end in `/`.\n */\nexport function basePrefix(base: string): string {\n return base.endsWith(\"/\") ? base : `${base}/`;\n}\n"],"mappings":"AAYA,SAAgBA,EAAW,EAAsB,CAC7C,OAAOC,EAAK,SAAS,GAAG,EAAIA,EAAO,GAAGA,EAAK,EAC/C,C"}
|
|
1
|
+
{"version":3,"file":"base-url.cjs","names":["basePrefix","base","toOutputFile","prefix","path","url"],"sources":["../../src/vite/base-url.ts"],"sourcesContent":["/**\n * The resolved Vite `base` as a prefix that always ends in a slash.\n *\n * One rule, three call sites: the manifest prefixes the URLs it precaches, and\n * the dev middleware matches the paths the browser asks for. They have to agree\n * by definition — when they drift, dev serves `/app/sw.js` while the manifest\n * precached `/sw.js`, and the symptom only appears on a subpath deploy, which is\n * the least exercised configuration there is.\n *\n * @param base - Vite's resolved `base`, with or without a trailing slash.\n * @returns The same base, guaranteed to end in `/`.\n */\nexport function basePrefix(base: string): string {\n return base.endsWith(\"/\") ? base : `${base}/`;\n}\n\n/**\n * The output file a root-absolute URL resolves to, relative to `outDir`.\n *\n * Emitted files are named relative to the build output, while the options that\n * point at them (`swUrl`, `additionalUrls`) are URLs that may or may not carry\n * the base. Both spellings land on the same file, so a check that a URL was\n * emitted does not depend on which one the config used.\n *\n * @param base - Vite's resolved `base`.\n * @param url - A root-absolute URL, with or without the base.\n * @returns The path of the file under `outDir`, without a leading slash.\n */\nexport function toOutputFile(base: string, url: string): string {\n const prefix = basePrefix(base);\n const path = url.split(/[?#]/)[0];\n if (prefix !== \"/\" && path.startsWith(prefix)) return path.slice(prefix.length);\n return path.replace(/^\\/+/, \"\");\n}\n"],"mappings":"AAYA,SAAgBA,EAAW,EAAsB,CAC7C,OAAOC,EAAK,SAAS,GAAG,EAAIA,EAAO,GAAGA,EAAK,EAC/C,CAcA,SAAgBC,EAAa,EAAc,EAAqB,CAC5D,IAAMC,EAASH,EAAWC,CAAI,EACxBG,EAAOC,EAAI,MAAM,MAAM,CAAC,CAAC,GAE/B,OADIF,IAAW,KAAOC,EAAK,WAAWD,CAAM,EAAUC,EAAK,MAAMD,EAAO,MAAM,EACvEC,EAAK,QAAQ,OAAQ,EAAE,CAClC,C"}
|
package/dist/vite/base-url.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
function e(e) {
|
|
3
3
|
return e.endsWith("/") ? e : `${e}/`;
|
|
4
4
|
}
|
|
5
|
+
function t(t, n) {
|
|
6
|
+
let r = e(t), i = n.split(/[?#]/)[0];
|
|
7
|
+
return r !== "/" && i.startsWith(r) ? i.slice(r.length) : i.replace(/^\/+/, "");
|
|
8
|
+
}
|
|
5
9
|
//#endregion
|
|
6
|
-
export { e as basePrefix };
|
|
10
|
+
export { e as basePrefix, t as toOutputFile };
|
|
7
11
|
|
|
8
12
|
//# sourceMappingURL=base-url.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-url.js","names":["basePrefix","base"],"sources":["../../src/vite/base-url.ts"],"sourcesContent":["/**\n * The resolved Vite `base` as a prefix that always ends in a slash.\n *\n * One rule, three call sites: the manifest prefixes the URLs it precaches, and\n * the dev middleware matches the paths the browser asks for. They have to agree\n * by definition — when they drift, dev serves `/app/sw.js` while the manifest\n * precached `/sw.js`, and the symptom only appears on a subpath deploy, which is\n * the least exercised configuration there is.\n *\n * @param base - Vite's resolved `base`, with or without a trailing slash.\n * @returns The same base, guaranteed to end in `/`.\n */\nexport function basePrefix(base: string): string {\n return base.endsWith(\"/\") ? base : `${base}/`;\n}\n"],"mappings":";AAYA,SAAgBA,EAAW,GAAsB;CAC7C,OAAOC,EAAK,SAAS,GAAG,IAAIA,IAAO,GAAGA,EAAK;AAC/C"}
|
|
1
|
+
{"version":3,"file":"base-url.js","names":["basePrefix","base","toOutputFile","prefix","path","url"],"sources":["../../src/vite/base-url.ts"],"sourcesContent":["/**\n * The resolved Vite `base` as a prefix that always ends in a slash.\n *\n * One rule, three call sites: the manifest prefixes the URLs it precaches, and\n * the dev middleware matches the paths the browser asks for. They have to agree\n * by definition — when they drift, dev serves `/app/sw.js` while the manifest\n * precached `/sw.js`, and the symptom only appears on a subpath deploy, which is\n * the least exercised configuration there is.\n *\n * @param base - Vite's resolved `base`, with or without a trailing slash.\n * @returns The same base, guaranteed to end in `/`.\n */\nexport function basePrefix(base: string): string {\n return base.endsWith(\"/\") ? base : `${base}/`;\n}\n\n/**\n * The output file a root-absolute URL resolves to, relative to `outDir`.\n *\n * Emitted files are named relative to the build output, while the options that\n * point at them (`swUrl`, `additionalUrls`) are URLs that may or may not carry\n * the base. Both spellings land on the same file, so a check that a URL was\n * emitted does not depend on which one the config used.\n *\n * @param base - Vite's resolved `base`.\n * @param url - A root-absolute URL, with or without the base.\n * @returns The path of the file under `outDir`, without a leading slash.\n */\nexport function toOutputFile(base: string, url: string): string {\n const prefix = basePrefix(base);\n const path = url.split(/[?#]/)[0];\n if (prefix !== \"/\" && path.startsWith(prefix)) return path.slice(prefix.length);\n return path.replace(/^\\/+/, \"\");\n}\n"],"mappings":";AAYA,SAAgBA,EAAW,GAAsB;CAC7C,OAAOC,EAAK,SAAS,GAAG,IAAIA,IAAO,GAAGA,EAAK;AAC/C;AAcA,SAAgBC,EAAa,GAAc,GAAqB;CAC5D,IAAMC,IAASH,EAAWC,CAAI,GACxBG,IAAOC,EAAI,MAAM,MAAM,CAAC,CAAC;CAE/B,OADIF,MAAW,OAAOC,EAAK,WAAWD,CAAM,IAAUC,EAAK,MAAMD,EAAO,MAAM,IACvEC,EAAK,QAAQ,QAAQ,EAAE;AAClC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./base-url.cjs");let t=require("node:path");function n(e){return async(t,n,r)=>{let i=(t.url??``).split(`?`)[0];if(e.matches(i,e.swUrl))try{let t=await e.buildWorker();n.setHeader(`Content-Type`,`application/javascript`),n.setHeader(`Service-Worker-Allowed`,`/`),n.setHeader(`Cache-Control`,`no-cache`),n.end(t)}catch(e){n.statusCode=500,n.end(`// SW dev build failed:\n// ${String(e)}`)}else e.matches(i,e.manifestUrl)?(n.setHeader(`Content-Type`,`application/json`),n.setHeader(`Cache-Control`,`no-cache`),n.end(JSON.stringify({version:`dev`,urls:[]}))):r()}}function
|
|
1
|
+
const e=require("./base-url.cjs");let t=require("node:path");var n=`tempest-pwa-dev-sw`;function r(e){return`/* tempest-precache-version: ${e} */\n`}function i(e,t){let n=e[t];if(n?.type!==`asset`)return;let r=typeof n.source==`string`?n.source:new TextDecoder().decode(n.source);try{let e=JSON.parse(r);return typeof e.version==`string`?e.version:void 0}catch{return}}function a(e){return async(t,n,r)=>{let i=(t.url??``).split(`?`)[0];if(e.matches(i,e.swUrl))try{let t=await e.buildWorker();n.setHeader(`Content-Type`,`application/javascript`),n.setHeader(`Service-Worker-Allowed`,`/`),n.setHeader(`Cache-Control`,`no-cache`),n.end(t)}catch(e){n.statusCode=500,n.end(`// SW dev build failed:\n// ${String(e)}`)}else e.matches(i,e.manifestUrl)?(n.setHeader(`Content-Type`,`application/json`),n.setHeader(`Cache-Control`,`no-cache`),n.end(JSON.stringify({version:`dev`,urls:[]}))):r()}}function o(o={}){let{swSrc:s=`src/sw.ts`,swUrl:c=`/sw.js`,manifestUrl:l=`/precache-manifest.json`,enabled:u=!0,build:d=!0}=o,f=process.cwd(),p=`/`,m=!1,h=`production`,g,_,v=!0,y=`warn`;function b(t,n){if(t===n)return!0;let r=e.basePrefix(p);return r!==`/`&&t===`${r}${n.replace(/^\//,``)}`}let x=null,S=null;async function C(){return x||(x=await(await import(`esbuild`)).context({entryPoints:[(0,t.resolve)(f,s)],bundle:!0,format:`iife`,platform:`browser`,target:`es2020`,write:!1,absWorkingDir:f,logLevel:`silent`}),x)}function w(){if(S)return S;let e=(async()=>{let e=(await(await C()).rebuild()).outputFiles?.[0]?.text;if(e===void 0)throw Error(`esbuild produced no service-worker output`);return e})();S=e;let t=()=>{S=null};return e.then(t,t),e}async function T(){let e=x;x=null,S=null,await e?.dispose()}async function E(){let e=await(await import(`vite`)).build({configFile:!1,root:f,mode:h,envDir:g,envPrefix:_,publicDir:!1,logLevel:y,build:{write:!1,emptyOutDir:!1,copyPublicDir:!1,sourcemap:!1,minify:v,lib:{entry:(0,t.resolve)(f,s),formats:[`iife`],name:`sw`,fileName:()=>`sw.js`}}}),n=Array.isArray(e)?e:[e];for(let{output:e=[]}of n){let t=e.find(e=>e.type===`chunk`&&e.isEntry);if(t?.code!==void 0)return t.code}throw Error(`the build produced no service-worker chunk`)}return{name:n,api:{swUrl:c,build:d},async buildEnd(){await T()},configResolved(e){f=e.root??process.cwd(),p=e.base??`/`,m=e.command===`build`&&!e.build?.ssr,h=e.mode??`production`,g=e.envDir,_=e.envPrefix,v=e.build?.minify??!0,y=e.logLevel===`silent`||e.logLevel===`error`?e.logLevel:`warn`},configureServer(e){u&&(e.httpServer?.once(`close`,()=>void T()),e.middlewares.use(a({swUrl:c,manifestUrl:l,matches:b,buildWorker:w})))},generateBundle:{order:`post`,async handler(t,n){if(!m||!d)return;let a;try{a=await E()}catch(e){this.error(`[tempest-pwa-dev-sw] could not bundle the service worker "${s}" into ${c}: ${e instanceof Error?e.message:String(e)}. Fix the entry, point \`swSrc\` at it, or pass \`build: false\` if another step bundles the worker.`)}let o=i(n,e.toOutputFile(p,l));this.emitFile({type:`asset`,fileName:e.toOutputFile(p,c),source:o===void 0?a:r(o)+a})}}}}exports.PWA_WORKER_PLUGIN_NAME=n,exports.precacheVersionBanner=r,exports.tempestPwaDevSw=o;
|
|
2
2
|
//# sourceMappingURL=tempest-pwa-dev-sw.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tempest-pwa-dev-sw.cjs","names":["basePrefix","resolve","require_base_url","node_path","createDevSwMiddleware","url","req","deps","source","res","error","next","tempestPwaDevSw","options","root","base","matches","target","prefix","context","pending","ensureContext","swSrc","buildWorker","run","text","clear","disposeContext","current","config","enabled","server"],"sources":["../../src/vite/tempest-pwa-dev-sw.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { basePrefix } from \"./base-url\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaDevSw}. */\nexport interface TempestPwaDevSwOptions {\n /** Service-worker entry, relative to the project root. Default `src/sw.ts`. */\n swSrc?: string;\n /**\n * URL the worker is served at (must match `registerServiceWorker`).\n * Default `/sw.js`.\n *\n * Write it relative to the site root; requests are matched against both the\n * bare path and the path prefixed with the resolved Vite `base`, so a\n * project served from a subpath is handled without extra configuration.\n */\n swUrl?: string;\n /**\n * Dev URL of the precache manifest. Default `/precache-manifest.json`.\n * Matched the same way as {@link TempestPwaDevSwOptions.swUrl}.\n */\n manifestUrl?: string;\n /** Serve the worker in dev. Default `true`; set `false` to opt out. */\n enabled?: boolean;\n}\n\n/**\n * The dev middleware: serve the compiled worker, and a placeholder manifest.\n *\n * Extracted from the plugin so the request handling reads on its own — the\n * plugin body is then only wiring. The manifest is deliberately empty in dev:\n * precaching a set of URLs Vite rewrites on every change would serve stale\n * modules, and the worker's runtime routes still work without it.\n *\n * @param deps - The two URLs, the path matcher and the incremental build.\n * @returns A connect-style middleware.\n */\nfunction createDevSwMiddleware(deps: {\n swUrl: string;\n manifestUrl: string;\n matches: (url: string, target: string) => boolean;\n buildWorker: () => Promise<string>;\n}): (\n req: { url?: string },\n res: {\n statusCode?: number;\n setHeader: (name: string, value: string) => void;\n end: (body: string) => void;\n },\n next: () => void,\n) => Promise<void> {\n return async (req, res, next) => {\n const url = (req.url ?? \"\").split(\"?\")[0];\n\n if (deps.matches(url, deps.swUrl)) {\n try {\n const source = await deps.buildWorker();\n res.setHeader(\"Content-Type\", \"application/javascript\");\n res.setHeader(\"Service-Worker-Allowed\", \"/\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(source);\n } catch (error) {\n res.statusCode = 500;\n res.end(`// SW dev build failed:\\n// ${String(error)}`);\n }\n return;\n }\n\n if (deps.matches(url, deps.manifestUrl)) {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(JSON.stringify({ version: \"dev\", urls: [] }));\n return;\n }\n\n next();\n };\n}\n\n/**\n * Dev-server plugin that makes the service worker available under `npm run dev`.\n *\n * The production worker is bundled at build time (`vite.sw.config.ts`), so in\n * dev there is no `/sw.js` to register. This plugin compiles `swSrc` on the fly\n * with esbuild — through one incremental context, not a cold build per request —\n * and serves it as a classic worker, plus an empty `precache-manifest.json` (there are no hashed build assets to precache in\n * dev — push and runtime caching still work). It closes the \"SW in dev\" gap\n * that otherwise only `vite-plugin-pwa`'s `devOptions` covered.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaDevSw } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestPwaDevSw()] });\n */\nexport function tempestPwaDevSw(options: TempestPwaDevSwOptions = {}): TempestVitePlugin {\n const {\n swSrc = \"src/sw.ts\",\n swUrl = \"/sw.js\",\n manifestUrl = \"/precache-manifest.json\",\n enabled = true,\n } = options;\n\n let root = process.cwd();\n let base = \"/\";\n\n /**\n * Whether a request path addresses `target`.\n *\n * Both the bare path and the base-prefixed one are accepted. Which of the\n * two arrives depends on where this middleware lands relative to Vite's own\n * base handling, and a project served from a subpath would otherwise never\n * match — the browser asks for `/app/sw.js` while the option says `/sw.js`.\n */\n function matches(url: string, target: string): boolean {\n if (url === target) return true;\n const prefix = basePrefix(base);\n if (prefix === \"/\") return false;\n return url === `${prefix}${target.replace(/^\\//, \"\")}`;\n }\n\n /**\n * The slice of esbuild's incremental API this plugin uses.\n *\n * Typed structurally rather than imported: `esbuild` is not a dependency of\n * this package. It is resolved at runtime from the app's own tree, where Vite\n * already brings it, so importing its types would add a build-time dependency\n * the runtime does not have.\n */\n interface BuildContext {\n rebuild(): Promise<{ outputFiles?: readonly { text: string }[] }>;\n dispose(): Promise<void>;\n }\n\n let context: BuildContext | null = null;\n let pending: Promise<string> | null = null;\n\n /**\n * The incremental build context, created on the first request that needs it.\n *\n * Lazy because a project that never registers a worker should not pay for an\n * esbuild child process, and because `apply: \"serve\"` still loads this module\n * in a build.\n *\n * @returns The context, reused across requests.\n */\n async function ensureContext(): Promise<BuildContext> {\n if (context) return context;\n const esbuild = (await import(\"esbuild\")) as unknown as {\n context(options: Record<string, unknown>): Promise<BuildContext>;\n };\n context = await esbuild.context({\n entryPoints: [resolve(root, swSrc)],\n bundle: true,\n format: \"iife\",\n platform: \"browser\",\n target: \"es2020\",\n write: false,\n absWorkingDir: root,\n logLevel: \"silent\",\n });\n return context;\n }\n\n /**\n * Bundle the worker, reusing the previous graph.\n *\n * `esbuild.build()` per request was a cold bundle every time, and the request\n * count is not one per session: `Cache-Control: no-cache` guarantees one per\n * page load, and Chrome re-fetches the worker script on every navigation and\n * on its own update checks. `rebuild()` reuses the graph and only redoes what\n * changed on disk.\n *\n * Single-flight, because those requests arrive in bursts and esbuild does not\n * promise anything about concurrent `rebuild()` calls on one context. Callers\n * that land while a bundle is in flight share its result, which is the right\n * answer anyway — they asked for the same file at the same moment.\n *\n * @returns The bundled worker source.\n */\n function buildWorker(): Promise<string> {\n if (pending) return pending;\n\n const run = (async (): Promise<string> => {\n const ctx = await ensureContext();\n const result = await ctx.rebuild();\n const text = result.outputFiles?.[0]?.text;\n if (text === undefined) throw new Error(\"esbuild produced no service-worker output\");\n return text;\n })();\n\n pending = run;\n const clear = (): void => {\n pending = null;\n };\n run.then(clear, clear);\n return run;\n }\n\n /**\n * Tear the context down, releasing esbuild's child process.\n *\n * Idempotent, because it is wired to both the dev server closing and the\n * plugin's `buildEnd`. Without it, restarting the dev server leaks one esbuild\n * process per restart.\n */\n async function disposeContext(): Promise<void> {\n const current = context;\n context = null;\n pending = null;\n await current?.dispose();\n }\n\n const plugin: Plugin = {\n name: \"tempest-pwa-dev-sw\",\n apply: \"serve\",\n async buildEnd() {\n await disposeContext();\n },\n configResolved(config) {\n root = config.root ?? process.cwd();\n base = config.base ?? \"/\";\n },\n configureServer(server) {\n if (!enabled) return;\n\n server.httpServer?.once(\"close\", () => void disposeContext());\n\n server.middlewares.use(\n createDevSwMiddleware({ swUrl, manifestUrl, matches, buildWorker }),\n );\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n"],"mappings":"A,6DAsCA,SAASI,EAAsB,EAaZ,CACf,OAAO,MAAO,EAAK,EAAK,IAAS,CAC7B,IAAMC,GAAOC,EAAI,KAAO,GAAA,CAAI,MAAM,GAAG,CAAC,CAAC,GAEvC,GAAIC,EAAK,QAAQF,EAAKE,EAAK,KAAK,EAC5B,GAAI,CACA,IAAMC,EAAS,MAAMD,EAAK,YAAY,EACtCE,EAAI,UAAU,eAAgB,wBAAwB,EACtDA,EAAI,UAAU,yBAA0B,GAAG,EAC3CA,EAAI,UAAU,gBAAiB,UAAU,EACzCA,EAAI,IAAID,CAAM,CAClB,OAASE,EAAO,CACZ,EAAI,WAAa,IACjBD,EAAI,IAAI,+BAA+B,OAAOC,CAAK,GAAG,CAC1D,MAIAH,EAAK,QAAQF,EAAKE,EAAK,WAAW,GAClCE,EAAI,UAAU,eAAgB,kBAAkB,EAChDA,EAAI,UAAU,gBAAiB,UAAU,EACzCA,EAAI,IAAI,KAAK,UAAU,CAAE,QAAS,MAAO,KAAM,CAAC,CAAE,CAAC,CAAC,GAIxDE,EAAK,CACT,CACJ,CAkBA,SAAgBC,EAAgB,EAAkC,CAAC,EAAsB,CACrF,GAAM,CACF,QAAQ,YACR,QAAQ,SACR,cAAc,0BACd,UAAU,IACVC,EAEAC,EAAO,QAAQ,IAAI,EACnBC,EAAO,IAUX,SAASC,EAAQ,EAAa,EAAyB,CACnD,GAAIX,IAAQY,EAAQ,MAAO,GAC3B,IAAMC,EAASlB,EAAAA,WAAWe,CAAI,EAE9B,OADIG,IAAW,KACRb,IAAQ,GAAGa,IAASD,EAAO,QAAQ,MAAO,EAAE,GACvD,CAeA,IAAIE,EAA+B,KAC/BC,EAAkC,KAWtC,eAAeC,GAAuC,CAelD,OAdIF,IAIJ,EAAU,MAAM,MAHO,OAAO,WAAA,CAGN,QAAQ,CAC5B,YAAa,EAAA,EAAClB,EAAAA,QAAAA,CAAQa,EAAMQ,CAAK,CAAC,EAClC,OAAQ,GACR,OAAQ,OACR,SAAU,UACV,OAAQ,SACR,MAAO,GACP,cAAeR,EACf,SAAU,QACd,CAAC,EACMK,EACX,CAkBA,SAASI,GAA+B,CACpC,GAAIH,EAAS,OAAOA,EAEpB,IAAMI,GAAO,SAA6B,CAGtC,IAAMC,GAAO,MADQ,MADHJ,EAAc,EAAA,CACP,QAAQ,EAAA,CACb,cAAc,EAAE,EAAE,KACtC,GAAII,IAAS,IAAA,GAAW,MAAU,MAAM,2CAA2C,EACnF,OAAOA,CACX,EAAA,CAAG,EAEH,EAAUD,EACV,IAAME,MAAoB,CACtB,EAAU,IACd,EAEA,OADAF,EAAI,KAAKE,EAAOA,CAAK,EACdF,CACX,CASA,eAAeG,GAAgC,CAC3C,IAAMC,EAAUT,EAChB,EAAU,KACV,EAAU,KACV,MAAMS,GAAS,QAAQ,CAC3B,CAuBA,MAAO,CApBH,KAAM,qBACN,MAAO,QACP,MAAM,UAAW,CACb,MAAMD,EAAe,CACzB,EACA,eAAe,EAAQ,CACnB,EAAOE,EAAO,MAAQ,QAAQ,IAAI,EAClC,EAAOA,EAAO,MAAQ,GAC1B,EACA,gBAAgB,EAAQ,CACfC,IAELC,EAAO,YAAY,KAAK,YAAe,KAAKJ,EAAe,CAAC,EAE5DI,EAAO,YAAY,IACf3B,EAAsB,CAAE,QAAO,cAAa,UAAS,aAAY,CAAC,CACtE,EACJ,CAGG,CACX,C"}
|
|
1
|
+
{"version":3,"file":"tempest-pwa-dev-sw.cjs","names":["basePrefix","resolve","toOutputFile","require_base_url","node_path","PWA_WORKER_PLUGIN_NAME","precacheVersionBanner","version","readManifestVersion","asset","bundle","fileName","source","parsed","createDevSwMiddleware","url","req","deps","res","error","next","tempestPwaDevSw","options","root","base","isBuild","mode","envDir","envPrefix","minify","logLevel","matches","target","prefix","context","pending","ensureContext","swSrc","buildWorker","run","text","clear","disposeContext","current","bundleWorkerForProduction","result","outputs","entry","output","item","config","enabled","server","build","swUrl","manifestUrl"],"sources":["../../src/vite/tempest-pwa-dev-sw.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { basePrefix, toOutputFile } from \"./base-url\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaDevSw}. */\nexport interface TempestPwaDevSwOptions {\n /** Service-worker entry, relative to the project root. Default `src/sw.ts`. */\n swSrc?: string;\n /**\n * URL the worker is served at (must match `registerServiceWorker`).\n * Default `/sw.js`.\n *\n * Write it relative to the site root; requests are matched against both the\n * bare path and the path prefixed with the resolved Vite `base`, so a\n * project served from a subpath is handled without extra configuration.\n */\n swUrl?: string;\n /**\n * Dev URL of the precache manifest. Default `/precache-manifest.json`.\n * Matched the same way as {@link TempestPwaDevSwOptions.swUrl}.\n */\n manifestUrl?: string;\n /** Serve the worker in dev. Default `true`; set `false` to opt out. */\n enabled?: boolean;\n /**\n * Bundle `swSrc` into the production build as `swUrl`. Default `true`.\n *\n * Set `false` only when another step bundles the worker (a second\n * `vite build --config vite.sw.config.ts`, `vite-plugin-pwa`). That is also\n * what tells {@link tempestPwaManifest} the worker is external, so its\n * missing-worker check stands down.\n */\n build?: boolean;\n}\n\n/** Name of the plugin {@link tempestPwaDevSw} returns, looked up by the manifest plugin. */\nexport const PWA_WORKER_PLUGIN_NAME = \"tempest-pwa-dev-sw\";\n\n/**\n * What {@link tempestPwaDevSw} publishes on its plugin `api`, for sibling plugins.\n *\n * {@link tempestPwaManifest} reads it to know which file the worker lands at and\n * whether this build emits it at all.\n */\nexport interface TempestPwaWorkerApi {\n /** URL the worker is served and emitted at. */\n swUrl: string;\n /** Whether this plugin bundles the worker into the production build. */\n build: boolean;\n}\n\n/**\n * The comment prepended to the production worker, carrying the precache version.\n *\n * The browser installs a new service worker only when the bytes of `sw.js`\n * change. A deploy that touches the app but not `src/sw.ts` would otherwise ship\n * a byte-identical worker: it never reinstalls, `installPrecache` never re-runs,\n * and the offline shell keeps serving the previous deploy. The manifest\n * `version` is a hash of every precached URL, so it changes exactly when the\n * precache does.\n *\n * @param version - The `version` of the emitted precache manifest.\n * @returns A one-line block comment.\n */\nexport function precacheVersionBanner(version: string): string {\n return `/* tempest-precache-version: ${version} */\\n`;\n}\n\n/**\n * The `version` of the precache manifest this build emitted, when there is one.\n *\n * @param bundle - The output bundle, as Rollup/Rolldown hands it to `generateBundle`.\n * @param fileName - Where the manifest was emitted, relative to `outDir`.\n * @returns The version, or `undefined` when the manifest is absent or unreadable.\n */\nfunction readManifestVersion(\n bundle: Record<string, { type?: string; source?: unknown }>,\n fileName: string,\n): string | undefined {\n const asset = bundle[fileName];\n if (asset?.type !== \"asset\") return undefined;\n const source =\n typeof asset.source === \"string\"\n ? asset.source\n : new TextDecoder().decode(asset.source as Uint8Array);\n try {\n const parsed = JSON.parse(source) as { version?: unknown };\n return typeof parsed.version === \"string\" ? parsed.version : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * The dev middleware: serve the compiled worker, and a placeholder manifest.\n *\n * Extracted from the plugin so the request handling reads on its own — the\n * plugin body is then only wiring. The manifest is deliberately empty in dev:\n * precaching a set of URLs Vite rewrites on every change would serve stale\n * modules, and the worker's runtime routes still work without it.\n *\n * @param deps - The two URLs, the path matcher and the incremental build.\n * @returns A connect-style middleware.\n */\nfunction createDevSwMiddleware(deps: {\n swUrl: string;\n manifestUrl: string;\n matches: (url: string, target: string) => boolean;\n buildWorker: () => Promise<string>;\n}): (\n req: { url?: string },\n res: {\n statusCode?: number;\n setHeader: (name: string, value: string) => void;\n end: (body: string) => void;\n },\n next: () => void,\n) => Promise<void> {\n return async (req, res, next) => {\n const url = (req.url ?? \"\").split(\"?\")[0];\n\n if (deps.matches(url, deps.swUrl)) {\n try {\n const source = await deps.buildWorker();\n res.setHeader(\"Content-Type\", \"application/javascript\");\n res.setHeader(\"Service-Worker-Allowed\", \"/\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(source);\n } catch (error) {\n res.statusCode = 500;\n res.end(`// SW dev build failed:\\n// ${String(error)}`);\n }\n return;\n }\n\n if (deps.matches(url, deps.manifestUrl)) {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(JSON.stringify({ version: \"dev\", urls: [] }));\n return;\n }\n\n next();\n };\n}\n\n/**\n * Service-worker plugin: serves `swSrc` under `npm run dev` and bundles it into\n * the production build.\n *\n * In dev there is no `/sw.js` to register. This plugin compiles `swSrc` on the\n * fly with esbuild — through one incremental context, not a cold build per\n * request — and serves it as a classic worker, plus an empty\n * `precache-manifest.json` (there are no hashed build assets to precache in\n * dev — push and runtime caching still work).\n *\n * In `vite build` it bundles the same entry with Vite's own `build()` (IIFE, so\n * it registers as a classic worker, exactly what the old `vite.sw.config.ts`\n * produced) and emits it at `swUrl`. Before 0.83.0 it was serve-only: an app\n * with this plugin and {@link tempestPwaManifest} worked in dev and shipped a\n * production build with no `sw.js` and a zero exit code, so registration\n * 404'd and `navigator.serviceWorker.ready` never resolved. The emitted worker\n * starts with the precache `version` (see {@link precacheVersionBanner}), so\n * every deploy that changes the precache changes the worker bytes and the\n * browser installs it. Pass `build: false` when another step bundles the\n * worker.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaDevSw, tempestPwaManifest } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestPwaManifest(), tempestPwaDevSw()] });\n */\nexport function tempestPwaDevSw(options: TempestPwaDevSwOptions = {}): TempestVitePlugin {\n const {\n swSrc = \"src/sw.ts\",\n swUrl = \"/sw.js\",\n manifestUrl = \"/precache-manifest.json\",\n enabled = true,\n build = true,\n } = options;\n\n let root = process.cwd();\n let base = \"/\";\n let isBuild = false;\n let mode = \"production\";\n let envDir: string | false | undefined;\n let envPrefix: string | string[] | undefined;\n let minify: boolean | string = true;\n let logLevel: \"silent\" | \"error\" | \"warn\" = \"warn\";\n\n /**\n * Whether a request path addresses `target`.\n *\n * Both the bare path and the base-prefixed one are accepted. Which of the\n * two arrives depends on where this middleware lands relative to Vite's own\n * base handling, and a project served from a subpath would otherwise never\n * match — the browser asks for `/app/sw.js` while the option says `/sw.js`.\n */\n function matches(url: string, target: string): boolean {\n if (url === target) return true;\n const prefix = basePrefix(base);\n if (prefix === \"/\") return false;\n return url === `${prefix}${target.replace(/^\\//, \"\")}`;\n }\n\n /**\n * The slice of esbuild's incremental API this plugin uses.\n *\n * Typed structurally rather than imported: `esbuild` is not a dependency of\n * this package. It is resolved at runtime from the app's own tree, where Vite\n * already brings it, so importing its types would add a build-time dependency\n * the runtime does not have.\n */\n interface BuildContext {\n rebuild(): Promise<{ outputFiles?: readonly { text: string }[] }>;\n dispose(): Promise<void>;\n }\n\n let context: BuildContext | null = null;\n let pending: Promise<string> | null = null;\n\n /**\n * The incremental build context, created on the first request that needs it.\n *\n * Lazy because a project that never registers a worker should not pay for an\n * esbuild child process, and because `apply: \"serve\"` still loads this module\n * in a build.\n *\n * @returns The context, reused across requests.\n */\n async function ensureContext(): Promise<BuildContext> {\n if (context) return context;\n const esbuild = (await import(\"esbuild\")) as unknown as {\n context(options: Record<string, unknown>): Promise<BuildContext>;\n };\n context = await esbuild.context({\n entryPoints: [resolve(root, swSrc)],\n bundle: true,\n format: \"iife\",\n platform: \"browser\",\n target: \"es2020\",\n write: false,\n absWorkingDir: root,\n logLevel: \"silent\",\n });\n return context;\n }\n\n /**\n * Bundle the worker, reusing the previous graph.\n *\n * `esbuild.build()` per request was a cold bundle every time, and the request\n * count is not one per session: `Cache-Control: no-cache` guarantees one per\n * page load, and Chrome re-fetches the worker script on every navigation and\n * on its own update checks. `rebuild()` reuses the graph and only redoes what\n * changed on disk.\n *\n * Single-flight, because those requests arrive in bursts and esbuild does not\n * promise anything about concurrent `rebuild()` calls on one context. Callers\n * that land while a bundle is in flight share its result, which is the right\n * answer anyway — they asked for the same file at the same moment.\n *\n * @returns The bundled worker source.\n */\n function buildWorker(): Promise<string> {\n if (pending) return pending;\n\n const run = (async (): Promise<string> => {\n const ctx = await ensureContext();\n const result = await ctx.rebuild();\n const text = result.outputFiles?.[0]?.text;\n if (text === undefined) throw new Error(\"esbuild produced no service-worker output\");\n return text;\n })();\n\n pending = run;\n const clear = (): void => {\n pending = null;\n };\n run.then(clear, clear);\n return run;\n }\n\n /**\n * Tear the context down, releasing esbuild's child process.\n *\n * Idempotent, because it is wired to both the dev server closing and the\n * plugin's `buildEnd`. Without it, restarting the dev server leaks one esbuild\n * process per restart.\n */\n async function disposeContext(): Promise<void> {\n const current = context;\n context = null;\n pending = null;\n await current?.dispose();\n }\n\n /**\n * Bundle `swSrc` for production with the host's own Vite.\n *\n * Vite's `build()` rather than esbuild: Vite 8 lists esbuild only as an\n * optional peer, so a fresh app may not have it, while the `vite` running\n * this plugin is always there. The config mirrors the `vite.sw.config.ts`\n * the PWA template used to ship — IIFE lib build, one entry, no app plugins —\n * plus the app's `mode`, `envDir` and `envPrefix`, so `import.meta.env`\n * inside the worker sees the same `VITE_*` values as the page. Its log level\n * is the app's, capped at `warn`, so the nested build adds no size table to\n * the output but a `silent` build stays silent.\n *\n * @returns The bundled worker source.\n */\n async function bundleWorkerForProduction(): Promise<string> {\n const vite = (await import(\"vite\")) as unknown as {\n build(config: Record<string, unknown>): Promise<unknown>;\n };\n const result = await vite.build({\n configFile: false,\n root,\n mode,\n envDir,\n envPrefix,\n publicDir: false,\n logLevel,\n build: {\n write: false,\n emptyOutDir: false,\n copyPublicDir: false,\n sourcemap: false,\n minify,\n lib: {\n entry: resolve(root, swSrc),\n formats: [\"iife\"],\n name: \"sw\",\n fileName: () => \"sw.js\",\n },\n },\n });\n const outputs = (Array.isArray(result) ? result : [result]) as {\n output?: { type: string; isEntry?: boolean; code?: string }[];\n }[];\n for (const { output = [] } of outputs) {\n const entry = output.find((item) => item.type === \"chunk\" && item.isEntry);\n if (entry?.code !== undefined) return entry.code;\n }\n throw new Error(\"the build produced no service-worker chunk\");\n }\n\n const api: TempestPwaWorkerApi = { swUrl, build };\n\n const plugin: Plugin = {\n name: PWA_WORKER_PLUGIN_NAME,\n api,\n async buildEnd() {\n await disposeContext();\n },\n configResolved(config) {\n root = config.root ?? process.cwd();\n base = config.base ?? \"/\";\n isBuild = config.command === \"build\" && !config.build?.ssr;\n mode = config.mode ?? \"production\";\n envDir = config.envDir;\n envPrefix = config.envPrefix;\n minify = config.build?.minify ?? true;\n logLevel =\n config.logLevel === \"silent\" || config.logLevel === \"error\"\n ? config.logLevel\n : \"warn\";\n },\n configureServer(server) {\n if (!enabled) return;\n\n server.httpServer?.once(\"close\", () => void disposeContext());\n\n server.middlewares.use(\n createDevSwMiddleware({ swUrl, manifestUrl, matches, buildWorker }),\n );\n },\n generateBundle: {\n order: \"post\",\n async handler(_outputOptions, bundle) {\n if (!isBuild || !build) return;\n let source: string;\n try {\n source = await bundleWorkerForProduction();\n } catch (error) {\n this.error(\n `[tempest-pwa-dev-sw] could not bundle the service worker \"${swSrc}\" ` +\n `into ${swUrl}: ${error instanceof Error ? error.message : String(error)}. ` +\n \"Fix the entry, point `swSrc` at it, or pass `build: false` if another \" +\n \"step bundles the worker.\",\n );\n }\n const version = readManifestVersion(\n bundle as Record<string, { type?: string; source?: unknown }>,\n toOutputFile(base, manifestUrl),\n );\n this.emitFile({\n type: \"asset\",\n fileName: toOutputFile(base, swUrl),\n source:\n version === undefined ? source : precacheVersionBanner(version) + source,\n });\n },\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n"],"mappings":"A,6DAqCA,IAAaK,EAAyB,qBA4BtC,SAAgBC,EAAsB,EAAyB,CAC3D,MAAO,gCAAgCC,EAAQ,MACnD,CASA,SAASC,EACL,EACA,EACkB,CAClB,IAAMC,EAAQC,EAAOC,GACrB,GAAIF,GAAO,OAAS,QAAS,OAC7B,IAAMG,EACF,OAAOH,EAAM,QAAW,SAClBA,EAAM,OACN,IAAI,YAAY,CAAC,CAAC,OAAOA,EAAM,MAAoB,EAC7D,GAAI,CACA,IAAMI,EAAS,KAAK,MAAMD,CAAM,EAChC,OAAO,OAAOC,EAAO,SAAY,SAAWA,EAAO,QAAU,IAAA,EACjE,MAAQ,CACJ,MACJ,CACJ,CAaA,SAASC,EAAsB,EAaZ,CACf,OAAO,MAAO,EAAK,EAAK,IAAS,CAC7B,IAAMC,GAAOC,EAAI,KAAO,GAAA,CAAI,MAAM,GAAG,CAAC,CAAC,GAEvC,GAAIC,EAAK,QAAQF,EAAKE,EAAK,KAAK,EAC5B,GAAI,CACA,IAAML,EAAS,MAAMK,EAAK,YAAY,EACtCC,EAAI,UAAU,eAAgB,wBAAwB,EACtDA,EAAI,UAAU,yBAA0B,GAAG,EAC3CA,EAAI,UAAU,gBAAiB,UAAU,EACzCA,EAAI,IAAIN,CAAM,CAClB,OAASO,EAAO,CACZ,EAAI,WAAa,IACjBD,EAAI,IAAI,+BAA+B,OAAOC,CAAK,GAAG,CAC1D,MAIAF,EAAK,QAAQF,EAAKE,EAAK,WAAW,GAClCC,EAAI,UAAU,eAAgB,kBAAkB,EAChDA,EAAI,UAAU,gBAAiB,UAAU,EACzCA,EAAI,IAAI,KAAK,UAAU,CAAE,QAAS,MAAO,KAAM,CAAC,CAAE,CAAC,CAAC,GAIxDE,EAAK,CACT,CACJ,CA6BA,SAAgBC,EAAgB,EAAkC,CAAC,EAAsB,CACrF,GAAM,CACF,QAAQ,YACR,QAAQ,SACR,cAAc,0BACd,UAAU,GACV,QAAQ,IACRC,EAEAC,EAAO,QAAQ,IAAI,EACnBC,EAAO,IACPC,EAAU,GACVC,EAAO,aACPC,EACAC,EACAC,EAA2B,GAC3BC,EAAwC,OAU5C,SAASC,EAAQ,EAAa,EAAyB,CACnD,GAAIhB,IAAQiB,EAAQ,MAAO,GAC3B,IAAMC,EAASjC,EAAAA,WAAWwB,CAAI,EAE9B,OADIS,IAAW,KACRlB,IAAQ,GAAGkB,IAASD,EAAO,QAAQ,MAAO,EAAE,GACvD,CAeA,IAAIE,EAA+B,KAC/BC,EAAkC,KAWtC,eAAeC,GAAuC,CAelD,OAdIF,IAIJ,EAAU,MAAM,MAHO,OAAO,WAAA,CAGN,QAAQ,CAC5B,YAAa,EAAA,EAACjC,EAAAA,QAAAA,CAAQsB,EAAMc,CAAK,CAAC,EAClC,OAAQ,GACR,OAAQ,OACR,SAAU,UACV,OAAQ,SACR,MAAO,GACP,cAAed,EACf,SAAU,QACd,CAAC,EACMW,EACX,CAkBA,SAASI,GAA+B,CACpC,GAAIH,EAAS,OAAOA,EAEpB,IAAMI,GAAO,SAA6B,CAGtC,IAAMC,GAAO,MADQ,MADHJ,EAAc,EAAA,CACP,QAAQ,EAAA,CACb,cAAc,EAAE,EAAE,KACtC,GAAII,IAAS,IAAA,GAAW,MAAU,MAAM,2CAA2C,EACnF,OAAOA,CACX,EAAA,CAAG,EAEH,EAAUD,EACV,IAAME,MAAoB,CACtB,EAAU,IACd,EAEA,OADAF,EAAI,KAAKE,EAAOA,CAAK,EACdF,CACX,CASA,eAAeG,GAAgC,CAC3C,IAAMC,EAAUT,EAChB,EAAU,KACV,EAAU,KACV,MAAMS,GAAS,QAAQ,CAC3B,CAgBA,eAAeC,GAA6C,CAIxD,IAAMC,EAAS,MAAM,MAHD,OAAO,QAAA,CAGD,MAAM,CAC5B,WAAY,GACZ,OACA,OACA,SACA,YACA,UAAW,GACX,WACA,MAAO,CACH,MAAO,GACP,YAAa,GACb,cAAe,GACf,UAAW,GACX,SACA,IAAK,CACD,OAAA,EAAO5C,EAAAA,QAAAA,CAAQsB,EAAMc,CAAK,EAC1B,QAAS,CAAC,MAAM,EAChB,KAAM,KACN,aAAgB,OACpB,CACJ,CACJ,CAAC,EACKS,EAAW,MAAM,QAAQD,CAAM,EAAIA,EAAS,CAACA,CAAM,EAGzD,IAAK,GAAM,CAAE,SAAS,CAAC,KAAOC,EAAS,CACnC,IAAMC,EAAQC,EAAO,KAAM,GAASC,EAAK,OAAS,SAAWA,EAAK,OAAO,EACzE,GAAIF,GAAO,OAAS,IAAA,GAAW,OAAOA,EAAM,IAChD,CACA,MAAU,MAAM,4CAA4C,CAChE,CA6DA,MAAO,CAxDH,KAAM1C,EACN,IAAA,CAJ+B,QAAO,OAItC,EACA,MAAM,UAAW,CACb,MAAMqC,EAAe,CACzB,EACA,eAAe,EAAQ,CACnB,EAAOQ,EAAO,MAAQ,QAAQ,IAAI,EAClC,EAAOA,EAAO,MAAQ,IACtB,EAAUA,EAAO,UAAY,SAAW,CAACA,EAAO,OAAO,IACvD,EAAOA,EAAO,MAAQ,aACtB,EAASA,EAAO,OAChB,EAAYA,EAAO,UACnB,EAASA,EAAO,OAAO,QAAU,GACjC,EACIA,EAAO,WAAa,UAAYA,EAAO,WAAa,QAC9CA,EAAO,SACP,MACd,EACA,gBAAgB,EAAQ,CACfC,IAELC,EAAO,YAAY,KAAK,YAAe,KAAKV,EAAe,CAAC,EAE5DU,EAAO,YAAY,IACftC,EAAsB,CAAE,QAAO,cAAa,UAAS,aAAY,CAAC,CACtE,EACJ,EACA,eAAgB,CACZ,MAAO,OACP,MAAM,QAAQ,EAAgB,EAAQ,CAClC,GAAI,CAACW,GAAW,CAAC4B,EAAO,OACxB,IAAIzC,EACJ,GAAI,CACA,EAAS,MAAMgC,EAA0B,CAC7C,OAASzB,EAAO,CACZ,KAAK,MACD,6DAA6DkB,EAAM,SACvDiB,EAAM,IAAInC,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,EAAE,qGAGjF,CACJ,CACA,IAAMZ,EAAUC,EACZE,EACAR,EAAAA,aAAasB,EAAM+B,CAAW,CAClC,EACA,KAAK,SAAS,CACV,KAAM,QACN,SAAUrD,EAAAA,aAAasB,EAAM8B,CAAK,EAClC,OACI/C,IAAY,IAAA,GAAYK,EAASN,EAAsBC,CAAO,EAAIK,CAC1E,CAAC,CACL,CACJ,CAGG,CACX,C"}
|