@canmi/web 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2025 Canmi
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,8 @@
1
+ //#region compat/src/build.d.ts
2
+ /**
3
+ * Reads an app's `browserslist` floors into esbuild's `build.target`.
4
+ *
5
+ * See spec/compat.md, "The syntax floor is set to the same line, deliberately".
6
+ */
7
+ export declare function esbuildTarget(browserslist: string[]): string[];
8
+ //#endregion
@@ -0,0 +1,15 @@
1
+ //#region compat/src/build.ts
2
+ /**
3
+ * Reads an app's `browserslist` floors into esbuild's `build.target`.
4
+ *
5
+ * See spec/compat.md, "The syntax floor is set to the same line, deliberately".
6
+ */
7
+ function esbuildTarget(browserslist) {
8
+ return browserslist.map((query) => {
9
+ const floor = /^([a-z]+) >= ([\d.]+)$/.exec(query);
10
+ if (!floor) throw new Error(`browserslist entry is not a floor, so esbuild cannot take it: ${query}`);
11
+ return `${floor[1]}${floor[2]}`;
12
+ });
13
+ }
14
+ //#endregion
15
+ export { esbuildTarget };
@@ -0,0 +1,14 @@
1
+ //#region compat/src/index.d.ts
2
+ /**
3
+ * Browser features missing from a browser that once crashed a reader; any one absent loads
4
+ * core-js. A new one met in production is one more line here and one in canaries.test.ts.
5
+ *
6
+ * Why a list of met cases and not a complete one, why `stable` and not `es` or `actual` -- see
7
+ * spec/compat.md, "The API floor".
8
+ */
9
+ export declare const CANARIES: {
10
+ name: string;
11
+ present: () => boolean;
12
+ }[];
13
+ export declare function prepareBrowserRuntime(): Promise<void>;
14
+ //#endregion
@@ -0,0 +1,21 @@
1
+ //#region compat/src/index.ts
2
+ /**
3
+ * Browser features missing from a browser that once crashed a reader; any one absent loads
4
+ * core-js. A new one met in production is one more line here and one in canaries.test.ts.
5
+ *
6
+ * Why a list of met cases and not a complete one, why `stable` and not `es` or `actual` -- see
7
+ * spec/compat.md, "The API floor".
8
+ */
9
+ const CANARIES = [{
10
+ name: "Array.prototype.toSorted",
11
+ present: () => typeof Array.prototype.toSorted === "function"
12
+ }, {
13
+ name: "URL.canParse",
14
+ present: () => typeof URL.canParse === "function"
15
+ }];
16
+ async function prepareBrowserRuntime() {
17
+ if (CANARIES.every((canary) => canary.present())) return;
18
+ await import("core-js/stable");
19
+ }
20
+ //#endregion
21
+ export { CANARIES, prepareBrowserRuntime };
@@ -0,0 +1,20 @@
1
+ //#region referer/src/index.d.ts
2
+ /**
3
+ * Where a reader came from, taken out of the address bar. See spec/architecture/referer.md.
4
+ */
5
+ /** Parameters a link may carry into any page, taken out once the page is running. */
6
+ export declare const ARRIVAL_PARAMETERS: readonly ['ref'];
7
+ /**
8
+ * `url`'s path, query and hash with every `name` pair taken out and every other pair left as it
9
+ * arrived, spelling and order included; undefined when there is no `name` to take.
10
+ */
11
+ export declare function withoutParameter(url: URL, name: string): string | undefined;
12
+ /**
13
+ * Take `name` out of the address bar, in place: no history entry, and no page view either. A
14
+ * tracker counts views by wrapping `history.replaceState` on the instance, so the prototype's is
15
+ * the browser's own. Returns the value taken, or null when there was none.
16
+ */
17
+ export declare function takeParameter(name: string): string | null;
18
+ /** Take every arrival parameter. Called once a page has hydrated; nothing reads them yet. */
19
+ export declare function takeArrivalParameters(): void;
20
+ //#endregion
@@ -0,0 +1,45 @@
1
+ //#region referer/src/index.ts
2
+ /**
3
+ * Where a reader came from, taken out of the address bar. See spec/architecture/referer.md.
4
+ */
5
+ /** Parameters a link may carry into any page, taken out once the page is running. */
6
+ const ARRIVAL_PARAMETERS = ["ref"];
7
+ /** A query pair's name, decoded; a name that does not decode is compared as written. */
8
+ function nameOf(pair) {
9
+ const name = pair.split("=", 1)[0].replaceAll("+", " ");
10
+ try {
11
+ return decodeURIComponent(name);
12
+ } catch {
13
+ return name;
14
+ }
15
+ }
16
+ /**
17
+ * `url`'s path, query and hash with every `name` pair taken out and every other pair left as it
18
+ * arrived, spelling and order included; undefined when there is no `name` to take.
19
+ */
20
+ function withoutParameter(url, name) {
21
+ if (!url.search) return void 0;
22
+ const pairs = url.search.slice(1).split("&");
23
+ const kept = pairs.filter((pair) => nameOf(pair) !== name);
24
+ if (kept.length === pairs.length) return void 0;
25
+ const search = kept.length > 0 ? `?${kept.join("&")}` : "";
26
+ return `${url.pathname}${search}${url.hash}`;
27
+ }
28
+ /**
29
+ * Take `name` out of the address bar, in place: no history entry, and no page view either. A
30
+ * tracker counts views by wrapping `history.replaceState` on the instance, so the prototype's is
31
+ * the browser's own. Returns the value taken, or null when there was none.
32
+ */
33
+ function takeParameter(name) {
34
+ const url = new URL(window.location.href);
35
+ const value = url.searchParams.get(name);
36
+ const replacement = withoutParameter(url, name);
37
+ if (replacement !== void 0) History.prototype.replaceState.call(history, history.state, "", replacement);
38
+ return value;
39
+ }
40
+ /** Take every arrival parameter. Called once a page has hydrated; nothing reads them yet. */
41
+ function takeArrivalParameters() {
42
+ for (const name of ARRIVAL_PARAMETERS) takeParameter(name);
43
+ }
44
+ //#endregion
45
+ export { ARRIVAL_PARAMETERS, takeArrivalParameters, takeParameter, withoutParameter };
@@ -0,0 +1,50 @@
1
+ import { sentrySvelteKit } from "@sentry/sveltekit/vite";
2
+ //#region sentry/src/build.d.ts
3
+ /** What `sentrySvelteKit` takes. Its own name for this is not part of the published surface. */
4
+ export type SentryPluginOptions = NonNullable<Parameters<typeof sentrySvelteKit>[0]>;
5
+ /** The environment a build reads, passed in so a test can hand it one. */
6
+ export type BuildEnv = Readonly<Record<string, string | undefined>>;
7
+ /** The Sentry organization every app's project lives in. */
8
+ export declare const SENTRY_ORG = "canmi";
9
+ export interface UploadPolicy {
10
+ /**
11
+ * Throw when CI builds without `SENTRY_AUTH_TOKEN`, for an app whose CI build is the one
12
+ * deployed. The site sets it; the status page, built on Vercel, does not.
13
+ */
14
+ requireTokenInCi?: boolean;
15
+ }
16
+ /**
17
+ * Whether this build sends its source maps to Sentry: only when `SENTRY_AUTH_TOKEN` is set.
18
+ *
19
+ * `SENTRY_SKIP_UPLOAD`, set in `mise.toml`, turns it off whatever the token says -- see
20
+ * spec/architecture/data.md, "A CI build compiles the site, and no longer compiles the corpus".
21
+ * Any non-empty value skips, so `SENTRY_SKIP_UPLOAD= pnpm run build` is how one local build
22
+ * uploads after all; `0` and `false` skip too, since this is a switch and parses no words.
23
+ */
24
+ export declare function uploadsSourceMaps(env: BuildEnv, policy?: UploadPolicy): boolean;
25
+ export interface PluginRequest {
26
+ /** The project's slug in {@link SENTRY_ORG}. */
27
+ project: string;
28
+ /** The answer of {@link uploadsSourceMaps}, asked once by the caller. */
29
+ upload: boolean;
30
+ env: BuildEnv;
31
+ /** Globs of the maps the adapter wrote, deleted once they are uploaded. */
32
+ mapsToDelete: string[];
33
+ }
34
+ /**
35
+ * The options every app hands `sentrySvelteKit`.
36
+ *
37
+ * The skip drives `autoUploadSourceMaps` rather than only withholding the token, because the
38
+ * plugin reads `SENTRY_AUTH_TOKEN` from the environment itself. Maps are deleted after the upload,
39
+ * so the deployed output carries none.
40
+ */
41
+ export declare function pluginOptions({ project, upload, env, mapsToDelete }: PluginRequest): SentryPluginOptions;
42
+ /**
43
+ * Vite's `build.sourcemap` for a build that does or does not upload.
44
+ *
45
+ * `hidden` emits maps without the `sourceMappingURL` comment, so no browser asks for a file that
46
+ * was deleted. A build that skips emits none, since deletion only follows an upload and a map
47
+ * left behind would ship the app's source as a static asset.
48
+ */
49
+ export declare function sourcemapSetting(upload: boolean): 'hidden' | false;
50
+ //#endregion
@@ -0,0 +1,46 @@
1
+ //#region sentry/src/build.ts
2
+ /** The Sentry organization every app's project lives in. */
3
+ const SENTRY_ORG = "canmi";
4
+ /**
5
+ * Whether this build sends its source maps to Sentry: only when `SENTRY_AUTH_TOKEN` is set.
6
+ *
7
+ * `SENTRY_SKIP_UPLOAD`, set in `mise.toml`, turns it off whatever the token says -- see
8
+ * spec/architecture/data.md, "A CI build compiles the site, and no longer compiles the corpus".
9
+ * Any non-empty value skips, so `SENTRY_SKIP_UPLOAD= pnpm run build` is how one local build
10
+ * uploads after all; `0` and `false` skip too, since this is a switch and parses no words.
11
+ */
12
+ function uploadsSourceMaps(env, policy = {}) {
13
+ if (env.SENTRY_SKIP_UPLOAD) return false;
14
+ const token = env.SENTRY_AUTH_TOKEN;
15
+ if (!token && env.CI && policy.requireTokenInCi) throw new Error("SENTRY_AUTH_TOKEN is unset in CI. Add it as an encrypted build variable, or the deployed worker will report every error without a usable stack trace.");
16
+ return Boolean(token);
17
+ }
18
+ /**
19
+ * The options every app hands `sentrySvelteKit`.
20
+ *
21
+ * The skip drives `autoUploadSourceMaps` rather than only withholding the token, because the
22
+ * plugin reads `SENTRY_AUTH_TOKEN` from the environment itself. Maps are deleted after the upload,
23
+ * so the deployed output carries none.
24
+ */
25
+ function pluginOptions({ project, upload, env, mapsToDelete }) {
26
+ return {
27
+ org: SENTRY_ORG,
28
+ project,
29
+ autoUploadSourceMaps: upload,
30
+ authToken: upload ? env.SENTRY_AUTH_TOKEN : void 0,
31
+ telemetry: false,
32
+ sourcemaps: { filesToDeleteAfterUpload: mapsToDelete }
33
+ };
34
+ }
35
+ /**
36
+ * Vite's `build.sourcemap` for a build that does or does not upload.
37
+ *
38
+ * `hidden` emits maps without the `sourceMappingURL` comment, so no browser asks for a file that
39
+ * was deleted. A build that skips emits none, since deletion only follows an upload and a map
40
+ * left behind would ship the app's source as a static asset.
41
+ */
42
+ function sourcemapSetting(upload) {
43
+ return upload ? "hidden" : false;
44
+ }
45
+ //#endregion
46
+ export { SENTRY_ORG, pluginOptions, sourcemapSetting, uploadsSourceMaps };
@@ -0,0 +1,6 @@
1
+ import { SentryApp } from "./options.js";
2
+ //#region sentry/src/client.d.ts
3
+ /** Initialize the browser SDK, or do nothing when the app has no DSN. */
4
+ export declare function initClient({ dsn, dev }: SentryApp): void;
5
+ //#endregion
6
+ export type { SentryApp };
@@ -0,0 +1,10 @@
1
+ import { initOptions } from "./options.js";
2
+ import { init } from "@sentry/sveltekit";
3
+ //#region sentry/src/client.ts
4
+ /** Initialize the browser SDK, or do nothing when the app has no DSN. */
5
+ function initClient({ dsn, dev }) {
6
+ if (!dsn) return;
7
+ init(initOptions(dsn, dev));
8
+ }
9
+ //#endregion
10
+ export { initClient };
@@ -0,0 +1,11 @@
1
+ //#region sentry/src/options.d.ts
2
+ /**
3
+ * What an app says about itself to the SDK, on either side.
4
+ *
5
+ * `dsn` absent means the app has no Sentry project: every helper then does nothing.
6
+ */
7
+ export interface SentryApp {
8
+ dsn: string | undefined;
9
+ dev: boolean;
10
+ }
11
+ //#endregion
@@ -0,0 +1,22 @@
1
+ //#region sentry/src/options.ts
2
+ /** A transport that accepts every envelope and sends none of them. */
3
+ const silent = () => ({
4
+ send: () => Promise.resolve({}),
5
+ flush: () => Promise.resolve(true)
6
+ });
7
+ /**
8
+ * The init options both sides share.
9
+ *
10
+ * Development initializes the SDK with every integration installed, so capture is exercised, and
11
+ * hands it a transport that sends nothing. `enabled: false` would install no integrations at all.
12
+ * See spec/analytics.md, "Development loads the client and reports nothing".
13
+ */
14
+ function initOptions(dsn, dev) {
15
+ return {
16
+ dsn,
17
+ environment: dev ? "development" : "production",
18
+ ...dev ? { transport: silent } : {}
19
+ };
20
+ }
21
+ //#endregion
22
+ export { initOptions };
@@ -0,0 +1,13 @@
1
+ import { SentryApp } from "./options.js";
2
+ import { Handle } from "@sveltejs/kit/hooks";
3
+ //#region sentry/src/server.d.ts
4
+ /**
5
+ * The handles that initialize and instrument the server SDK, first in `sequence`; none without a
6
+ * DSN.
7
+ *
8
+ * `initCloudflareSentryHandle` serves both adapters: the `worker` export condition wraps the
9
+ * request in Cloudflare's context, and the `node` one initializes on the first request.
10
+ */
11
+ export declare function serverHandles({ dsn, dev }: SentryApp): Handle[];
12
+ //#endregion
13
+ export type { SentryApp };
@@ -0,0 +1,16 @@
1
+ import { initOptions } from "./options.js";
2
+ import { initCloudflareSentryHandle, sentryHandle } from "@sentry/sveltekit";
3
+ //#region sentry/src/server.ts
4
+ /**
5
+ * The handles that initialize and instrument the server SDK, first in `sequence`; none without a
6
+ * DSN.
7
+ *
8
+ * `initCloudflareSentryHandle` serves both adapters: the `worker` export condition wraps the
9
+ * request in Cloudflare's context, and the `node` one initializes on the first request.
10
+ */
11
+ function serverHandles({ dsn, dev }) {
12
+ if (!dsn) return [];
13
+ return [initCloudflareSentryHandle(initOptions(dsn, dev)), sentryHandle()];
14
+ }
15
+ //#endregion
16
+ export { serverHandles };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@canmi/web",
3
+ "version": "0.0.0",
4
+ "description": "Running a SvelteKit app in public: browser compatibility, the referrer and Sentry",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/canmi21/lib.git",
9
+ "directory": "pkgs/web"
10
+ },
11
+ "type": "module",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "exports": {
16
+ "./compat": {
17
+ "types": "./dist/compat/src/index.d.ts",
18
+ "default": "./dist/compat/src/index.js"
19
+ },
20
+ "./compat/build": {
21
+ "types": "./dist/compat/src/build.d.ts",
22
+ "default": "./dist/compat/src/build.js"
23
+ },
24
+ "./referer": {
25
+ "types": "./dist/referer/src/index.d.ts",
26
+ "default": "./dist/referer/src/index.js"
27
+ },
28
+ "./sentry/build": {
29
+ "types": "./dist/sentry/src/build.d.ts",
30
+ "default": "./dist/sentry/src/build.js"
31
+ },
32
+ "./sentry/client": {
33
+ "types": "./dist/sentry/src/client.d.ts",
34
+ "default": "./dist/sentry/src/client.js"
35
+ },
36
+ "./sentry/server": {
37
+ "types": "./dist/sentry/src/server.d.ts",
38
+ "default": "./dist/sentry/src/server.js"
39
+ }
40
+ },
41
+ "dependencies": {
42
+ "@sentry/sveltekit": "^11.4.0",
43
+ "@sveltejs/kit": "^3.0.0",
44
+ "core-js": "^3.50.0"
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown"
48
+ }
49
+ }