@sjohansson/astro-plausible 0.0.0-stage → 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Stefan Johansson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,203 @@
1
- # Temporary Holding Version
1
+ # Astro Plausible
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ First-party [Plausible](https://plausible.io) analytics for Astro sites.
4
+
5
+ - The official Plausible tracker (the `@plausible-analytics/tracker` package) is bundled into your site's own
6
+ JavaScript. There is no third-party script tag for a blocker to recognise.
7
+ - Events go to `/api/event` on your own domain. A small relay there forwards them to Plausible with the visitor's
8
+ IP and user agent. Plausible drops events that arrive without a real visitor IP, so the relay is not optional.
9
+ - `astro-plausible init` writes that relay for Azure Static Web Apps. On other hosts it is a few lines around
10
+ `createRelay()`.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pnpm add @sjohansson/astro-plausible
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### 1. Add the integration
21
+
22
+ ```js
23
+ // astro.config.mjs
24
+ import { defineConfig } from "astro/config";
25
+ import plausible from "@sjohansson/astro-plausible/integration";
26
+
27
+ export default defineConfig({
28
+ integrations: [plausible({ domain: "example.com" })],
29
+ });
30
+ ```
31
+
32
+ It adds one script to every page that starts the tracker. Options:
33
+
34
+ | Option | Default | Notes |
35
+ | ---------------------- | -------------- | ------------------------------------------------------------------------- |
36
+ | `domain` | | Required. The site domain as entered in Plausible. |
37
+ | `endpoint` | `"/api/event"` | Where events go. Set `https://plausible.io/api/event` to skip the relay. |
38
+ | `enabled` | | Defaults to on for `astro build` and off for `astro dev`. |
39
+ | `autoCapturePageviews` | `true` | Send a pageview on load and on history navigation. |
40
+ | `hashBasedRouting` | `false` | Track `#` paths as separate pages. |
41
+ | `outboundLinks` | `false` | Track clicks on links to other sites. |
42
+ | `fileDownloads` | `false` | `true` for Plausible's default file types, or `{ fileExtensions: [...] }`. |
43
+ | `formSubmissions` | `false` | Track form submissions. |
44
+ | `captureOnLocalhost` | `false` | Also track on localhost. |
45
+ | `logging` | `true` | Log ignored events to the console. |
46
+ | `customProperties` | | Custom properties added to every event. |
47
+ | `bindToWindow` | `true` | Expose `window.plausible`. Plausible's installation check looks for it. |
48
+
49
+ ### 2. Add the relay
50
+
51
+ #### Azure Static Web Apps
52
+
53
+ ```bash
54
+ pnpm exec astro-plausible init --domain example.com
55
+ ```
56
+
57
+ This writes `api/event/index.mjs` and `api/event/function.json`, plus `api/host.json` and `api/package.json` when
58
+ they are missing. The function is one file with no imports, so the `api/` folder has no dependencies and the deploy
59
+ can skip the API build. Commit the files. Re-run the command after upgrading the package; it rewrites the function
60
+ and leaves the other files alone.
61
+
62
+ Deploy settings:
63
+
64
+ ```json
65
+ // public/staticwebapp.config.json
66
+ { "platform": { "apiRuntime": "node:22" } }
67
+ ```
68
+
69
+ ```yaml
70
+ # The deploy step
71
+ - uses: Azure/static-web-apps-deploy@v1
72
+ with:
73
+ app_location: "dist"
74
+ api_location: "api"
75
+ skip_app_build: true
76
+ skip_api_build: true
77
+ ```
78
+
79
+ CLI options:
80
+
81
+ ```text
82
+ --target <name> Hosting target. "swa" is the only one today.
83
+ --domain <domain> Site domain as entered in Plausible (or PLAUSIBLE_DOMAIN). Repeat it for several sites.
84
+ --upstream <url> Plausible host, default https://plausible.io. Self-hosted: https://plausible.example.com
85
+ --dir <path> API folder, default "api"
86
+ --rule <spec> Validate a custom event: <event>:<property>=<regex>. Repeatable.
87
+ --max-body <bytes> Reject bodies larger than this, default 16384
88
+ --dry-run Print the files instead of writing them
89
+ ```
90
+
91
+ #### Other hosts
92
+
93
+ The relay works on the web standard `Request` and `Response`. Give it the visitor IP when the platform provides one;
94
+ otherwise it reads the usual headers (`x-azure-clientip`, `cf-connecting-ip`, `x-real-ip`, `x-forwarded-for`, and
95
+ so on).
96
+
97
+ ```ts
98
+ // src/pages/api/event.ts, an Astro server endpoint
99
+ import { createRelay } from "@sjohansson/astro-plausible/relay";
100
+ import type { APIRoute } from "astro";
101
+
102
+ export const prerender = false;
103
+
104
+ const relay = createRelay({ domain: "example.com" });
105
+
106
+ export const POST: APIRoute = ({ request, clientAddress }) => relay.handle(request, { clientIp: clientAddress });
107
+ ```
108
+
109
+ ```ts
110
+ // functions/api/event.ts, a Cloudflare Pages function
111
+ import { createRelay } from "@sjohansson/astro-plausible/relay";
112
+
113
+ const relay = createRelay({ domain: "example.com" });
114
+
115
+ export const onRequestPost = ({ request }) => relay.handle(request);
116
+ ```
117
+
118
+ Relay options:
119
+
120
+ | Option | Default | Notes |
121
+ | --------------- | ------------------------ | ------------------------------------------------------------------------- |
122
+ | `domain` | | Required. One domain or several. Events for other domains get a 400. |
123
+ | `upstream` | `"https://plausible.io"` | Events are posted to `<upstream>/api/event`. |
124
+ | `maxBodyLength` | `16384` | Largest request body accepted, in bytes. |
125
+ | `events` | | Validation for custom events, see below. |
126
+ | `onError` | | Called when Plausible cannot be reached. The response is a 502 either way. |
127
+
128
+ The relay answers 405 for anything but POST, 413 for a body over the limit, 400 for anything it will not forward,
129
+ and otherwise passes Plausible's status and body through. The visitor IP goes in `X-Forwarded-For`.
130
+
131
+ ### Validating custom events
132
+
133
+ If you build anything on custom events, say vote counts, the relay can make sure only well-formed ones get through.
134
+ A rule lists the properties an event may carry and a pattern for each. Events with a rule are forwarded only with
135
+ those properties, each matching its pattern. Events without a rule pass through untouched.
136
+
137
+ ```ts
138
+ import { voteRelayRule } from "@sjohansson/astro-votes";
139
+
140
+ const relay = createRelay({
141
+ domain: "example.com",
142
+ events: {
143
+ ...voteRelayRule(),
144
+ signup: { plan: /^(free|pro)$/ },
145
+ },
146
+ });
147
+ ```
148
+
149
+ The same for the CLI:
150
+
151
+ ```bash
152
+ pnpm exec astro-plausible init --domain example.com \
153
+ --rule "vote:vote=^[A-Za-z0-9._/-]{1,200}:(?:up|down)(?:-undo)?$" \
154
+ --rule "signup:plan=^(free|pro)$"
155
+ ```
156
+
157
+ ### Tracking events
158
+
159
+ `window.plausible("signup")` works as in any Plausible setup. The typed way is the tracker's own `track`:
160
+
161
+ ```ts
162
+ import { track } from "@sjohansson/astro-plausible/client";
163
+
164
+ track("signup", { props: { plan: "pro" } });
165
+ ```
166
+
167
+ ### Without the integration
168
+
169
+ Start the tracker yourself when you want a function for `customProperties`, a `transformRequest` hook, or control
170
+ over where it runs.
171
+
172
+ ```ts
173
+ import { startPlausible } from "@sjohansson/astro-plausible/client";
174
+
175
+ startPlausible({
176
+ domain: "example.com",
177
+ transformRequest: (payload) => (payload.u.includes("/admin") ? null : payload),
178
+ });
179
+ ```
180
+
181
+ The first call wins; later calls do nothing. The tracker itself allows a single `init()`.
182
+
183
+ ### A path other than `/api/event`
184
+
185
+ Set `endpoint` to wherever the relay answers, for example `/stats/api/event`. Static Web Apps managed functions
186
+ always live under `/api`; a rewrite from another path to `/api/event` in `staticwebapp.config.json` may do it, but
187
+ that is untested and the Static Web Apps documentation says route rules for the API only support redirects and
188
+ roles.
189
+
190
+ ### Local development
191
+
192
+ The integration leaves the tracker out of `astro dev`, and the tracker ignores localhost unless
193
+ `captureOnLocalhost` is set. To test the whole chain locally, set both and run the site through something that
194
+ serves the relay too, such as the Static Web Apps CLI.
195
+
196
+ ## Keeping the tracker current
197
+
198
+ The tracker is a normal dependency, so Renovate or a routine upgrade brings new Plausible releases to your site.
199
+ Nothing is vendored.
200
+
201
+ ## License
202
+
203
+ MIT
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/bin.js ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ import { DEFAULT_MAX_BODY_LENGTH, DEFAULT_UPSTREAM } from "./relay.js";
3
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ //#region src/cli.ts
7
+ /**
8
+ * astro-plausible - writes the relay that forwards tracker events to Plausible.
9
+ *
10
+ * Static Web Apps managed functions must be real files in the repo, and nothing in node_modules is picked up, so
11
+ * the relay is written out as one self-contained file. Re-run the command to update it after a package upgrade.
12
+ */
13
+ const HELP = `Usage: astro-plausible init [options]
14
+
15
+ Writes a relay function that forwards Plausible tracker events from your domain to Plausible.
16
+
17
+ Options:
18
+ --target <name> Hosting target. "swa" (Azure Static Web Apps managed functions) is the only one today.
19
+ --domain <domain> Site domain as entered in Plausible (or PLAUSIBLE_DOMAIN). Repeat it for several sites.
20
+ --upstream <url> Plausible host, default ${DEFAULT_UPSTREAM}. Self-hosted: https://plausible.example.com
21
+ --dir <path> API folder, default "api"
22
+ --rule <spec> Validate a custom event: <event>:<property>=<regex>. Repeatable. Events with a rule are
23
+ forwarded only with the listed properties, each matching its pattern.
24
+ --max-body <bytes> Reject bodies larger than this, default ${DEFAULT_MAX_BODY_LENGTH}
25
+ --dry-run Print the files instead of writing them
26
+ -h, --help Show this help
27
+
28
+ Writes <dir>/event/index.mjs and <dir>/event/function.json every time, and <dir>/host.json and <dir>/package.json
29
+ when they are missing. Deploy with api_location set to <dir>.
30
+ `;
31
+ const CONFIG_PLACEHOLDER = "__ASTRO_PLAUSIBLE_RELAY_CONFIG__";
32
+ const RULE_PATTERN = /^([\w.-]+):([\w.-]+)=(.+)$/s;
33
+ /** Parses `<event>:<property>=<regex>` specs into relay rules. Throws on a bad spec or pattern. */
34
+ function parseRules(specs) {
35
+ const events = {};
36
+ for (const spec of specs) {
37
+ const match = spec.match(RULE_PATTERN);
38
+ if (!match) throw new Error(`--rule must look like <event>:<property>=<regex>, got "${spec}"`);
39
+ const [, event = "", property = "", pattern = ""] = match;
40
+ try {
41
+ new RegExp(pattern);
42
+ } catch (error) {
43
+ throw new Error(`--rule pattern for ${event}:${property} is not a valid regex: ${error}`);
44
+ }
45
+ events[event] = {
46
+ ...events[event],
47
+ [property]: pattern
48
+ };
49
+ }
50
+ return events;
51
+ }
52
+ /** The files `init --target swa` produces for a relay config. */
53
+ function swaFiles(template, config, dir, context) {
54
+ if (!template.includes(CONFIG_PLACEHOLDER)) throw new Error("The relay template is missing its config placeholder.");
55
+ const banner = [
56
+ `// Written by astro-plausible init (@sjohansson/astro-plausible ${context.version}) on ${context.now.toISOString().slice(0, 10)}.`,
57
+ "// Forwards Plausible tracker events to Plausible so the tracker runs first party and is not dropped by ad blockers.",
58
+ "// Do not edit: re-run the command to change the config or pick up a new package version.",
59
+ ""
60
+ ].join("\n");
61
+ const code = template.replace(/^\/\/# sourceMappingURL=.*$/m, "").replaceAll(CONFIG_PLACEHOLDER, JSON.stringify(config, null, 2)).trimEnd();
62
+ return [
63
+ {
64
+ path: join(dir, "event", "index.mjs"),
65
+ content: `${banner}${code}\n`,
66
+ keepExisting: false
67
+ },
68
+ {
69
+ path: join(dir, "event", "function.json"),
70
+ content: `${JSON.stringify({
71
+ scriptFile: "index.mjs",
72
+ entryPoint: "default",
73
+ bindings: [{
74
+ authLevel: "anonymous",
75
+ type: "httpTrigger",
76
+ direction: "in",
77
+ name: "req",
78
+ methods: ["post"]
79
+ }, {
80
+ type: "http",
81
+ direction: "out",
82
+ name: "res"
83
+ }]
84
+ }, null, 2)}\n`,
85
+ keepExisting: false
86
+ },
87
+ {
88
+ path: join(dir, "host.json"),
89
+ content: "{\n \"version\": \"2.0\"\n}\n",
90
+ keepExisting: true
91
+ },
92
+ {
93
+ path: join(dir, "package.json"),
94
+ content: `${JSON.stringify({
95
+ name: "site-api",
96
+ private: true,
97
+ description: "Static Web Apps managed functions. No dependencies on purpose, so the deploy skips the API build."
98
+ }, null, 2)}\n`,
99
+ keepExisting: true
100
+ }
101
+ ];
102
+ }
103
+ async function exists(path) {
104
+ try {
105
+ await access(path);
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+ async function packageVersion() {
112
+ try {
113
+ const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
114
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
115
+ } catch {
116
+ return "unknown";
117
+ }
118
+ }
119
+ async function main(argv, env, context = {}) {
120
+ const { values, positionals } = parseArgs({
121
+ args: argv,
122
+ allowPositionals: true,
123
+ options: {
124
+ target: { type: "string" },
125
+ domain: {
126
+ type: "string",
127
+ multiple: true
128
+ },
129
+ upstream: { type: "string" },
130
+ dir: { type: "string" },
131
+ rule: {
132
+ type: "string",
133
+ multiple: true
134
+ },
135
+ "max-body": { type: "string" },
136
+ "dry-run": { type: "boolean" },
137
+ help: {
138
+ type: "boolean",
139
+ short: "h"
140
+ }
141
+ }
142
+ });
143
+ if (values.help || positionals.length === 0) {
144
+ process.stdout.write(HELP);
145
+ return values.help ? 0 : 1;
146
+ }
147
+ if (positionals[0] !== "init") return fail(`Unknown command "${positionals[0]}". Only "init" exists.`);
148
+ const target = values.target ?? "swa";
149
+ if (target !== "swa") return fail(`Unknown target "${target}". Only "swa" exists.`);
150
+ const domains = values.domain ?? (env["PLAUSIBLE_DOMAIN"] ? [env["PLAUSIBLE_DOMAIN"]] : []);
151
+ if (domains.length === 0) return fail("No domain. Pass --domain or set PLAUSIBLE_DOMAIN.");
152
+ const maxBodyLength = values["max-body"] === void 0 ? void 0 : Number(values["max-body"]);
153
+ if (maxBodyLength !== void 0 && !(Number.isInteger(maxBodyLength) && maxBodyLength > 0)) return fail(`--max-body must be a positive whole number, got "${values["max-body"]}".`);
154
+ let events;
155
+ try {
156
+ events = parseRules(values.rule ?? []);
157
+ } catch (error) {
158
+ return fail(error instanceof Error ? error.message : String(error));
159
+ }
160
+ const config = {
161
+ domain: domains.length === 1 ? domains[0] : domains,
162
+ ...values.upstream ? { upstream: values.upstream } : {},
163
+ ...maxBodyLength ? { maxBodyLength } : {},
164
+ ...Object.keys(events).length > 0 ? { events } : {}
165
+ };
166
+ const templateDir = context.templateDir ?? new URL("./templates/", import.meta.url);
167
+ const files = swaFiles(await readFile(new URL("swa.mjs", templateDir), "utf8"), config, values.dir ?? "api", {
168
+ version: context.version ?? await packageVersion(),
169
+ now: context.now ?? /* @__PURE__ */ new Date()
170
+ });
171
+ for (const file of files) {
172
+ if (values["dry-run"]) {
173
+ process.stdout.write(`--- ${file.path}\n${file.content}\n`);
174
+ continue;
175
+ }
176
+ const present = await exists(file.path);
177
+ if (present && file.keepExisting) {
178
+ process.stdout.write(`kept ${file.path}\n`);
179
+ continue;
180
+ }
181
+ await mkdir(join(file.path, ".."), { recursive: true });
182
+ await writeFile(file.path, file.content);
183
+ process.stdout.write(`${present ? "updated" : "created"} ${file.path}\n`);
184
+ }
185
+ return 0;
186
+ }
187
+ function fail(message) {
188
+ process.stderr.write(`astro-plausible: ${message}\n`);
189
+ return 1;
190
+ }
191
+ //#endregion
192
+ //#region src/bin.ts
193
+ main(process.argv.slice(2), process.env).then((code) => {
194
+ process.exitCode = code;
195
+ }, (error) => {
196
+ process.exitCode = fail(error instanceof Error ? error.message : String(error));
197
+ });
198
+ //#endregion
199
+
200
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","names":[],"sources":["../src/cli.ts","../src/bin.ts"],"sourcesContent":["/**\n * astro-plausible - writes the relay that forwards tracker events to Plausible.\n *\n * Static Web Apps managed functions must be real files in the repo, and nothing in node_modules is picked up, so\n * the relay is written out as one self-contained file. Re-run the command to update it after a package upgrade.\n */\nimport { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { DEFAULT_MAX_BODY_LENGTH, DEFAULT_UPSTREAM, type RelayOptions, type RelayRule } from \"./relay\";\n\nconst HELP = `Usage: astro-plausible init [options]\n\nWrites a relay function that forwards Plausible tracker events from your domain to Plausible.\n\nOptions:\n --target <name> Hosting target. \"swa\" (Azure Static Web Apps managed functions) is the only one today.\n --domain <domain> Site domain as entered in Plausible (or PLAUSIBLE_DOMAIN). Repeat it for several sites.\n --upstream <url> Plausible host, default ${DEFAULT_UPSTREAM}. Self-hosted: https://plausible.example.com\n --dir <path> API folder, default \"api\"\n --rule <spec> Validate a custom event: <event>:<property>=<regex>. Repeatable. Events with a rule are\n forwarded only with the listed properties, each matching its pattern.\n --max-body <bytes> Reject bodies larger than this, default ${DEFAULT_MAX_BODY_LENGTH}\n --dry-run Print the files instead of writing them\n -h, --help Show this help\n\nWrites <dir>/event/index.mjs and <dir>/event/function.json every time, and <dir>/host.json and <dir>/package.json\nwhen they are missing. Deploy with api_location set to <dir>.\n`;\n\nconst CONFIG_PLACEHOLDER = \"__ASTRO_PLAUSIBLE_RELAY_CONFIG__\";\nconst RULE_PATTERN = /^([\\w.-]+):([\\w.-]+)=(.+)$/s;\n\nexport interface CliContext {\n /** Where the built templates live, as a directory URL. Defaults to the package's own dist/templates. */\n templateDir?: URL;\n version?: string;\n now?: Date;\n}\n\nexport interface ScaffoldFile {\n path: string;\n content: string;\n /** Written only when the file does not exist yet. */\n keepExisting: boolean;\n}\n\n/** Parses `<event>:<property>=<regex>` specs into relay rules. Throws on a bad spec or pattern. */\nexport function parseRules(specs: string[]): Record<string, RelayRule> {\n const events: Record<string, RelayRule> = {};\n for (const spec of specs) {\n const match = spec.match(RULE_PATTERN);\n if (!match) throw new Error(`--rule must look like <event>:<property>=<regex>, got \"${spec}\"`);\n const [, event = \"\", property = \"\", pattern = \"\"] = match;\n try {\n new RegExp(pattern);\n } catch (error) {\n throw new Error(`--rule pattern for ${event}:${property} is not a valid regex: ${error}`);\n }\n events[event] = { ...events[event], [property]: pattern };\n }\n return events;\n}\n\n/** The files `init --target swa` produces for a relay config. */\nexport function swaFiles(\n template: string,\n config: RelayOptions,\n dir: string,\n context: Required<Pick<CliContext, \"version\" | \"now\">>,\n): ScaffoldFile[] {\n if (!template.includes(CONFIG_PLACEHOLDER)) throw new Error(\"The relay template is missing its config placeholder.\");\n const banner = [\n `// Written by astro-plausible init (@sjohansson/astro-plausible ${context.version}) on ${context.now.toISOString().slice(0, 10)}.`,\n \"// Forwards Plausible tracker events to Plausible so the tracker runs first party and is not dropped by ad blockers.\",\n \"// Do not edit: re-run the command to change the config or pick up a new package version.\",\n \"\",\n ].join(\"\\n\");\n const code = template\n .replace(/^\\/\\/# sourceMappingURL=.*$/m, \"\")\n .replaceAll(CONFIG_PLACEHOLDER, JSON.stringify(config, null, 2))\n .trimEnd();\n\n const functionJson = {\n scriptFile: \"index.mjs\",\n entryPoint: \"default\",\n bindings: [\n { authLevel: \"anonymous\", type: \"httpTrigger\", direction: \"in\", name: \"req\", methods: [\"post\"] },\n { type: \"http\", direction: \"out\", name: \"res\" },\n ],\n };\n const packageJson = {\n name: \"site-api\",\n private: true,\n description: \"Static Web Apps managed functions. No dependencies on purpose, so the deploy skips the API build.\",\n };\n\n return [\n { path: join(dir, \"event\", \"index.mjs\"), content: `${banner}${code}\\n`, keepExisting: false },\n {\n path: join(dir, \"event\", \"function.json\"),\n content: `${JSON.stringify(functionJson, null, 2)}\\n`,\n keepExisting: false,\n },\n { path: join(dir, \"host.json\"), content: '{\\n \"version\": \"2.0\"\\n}\\n', keepExisting: true },\n { path: join(dir, \"package.json\"), content: `${JSON.stringify(packageJson, null, 2)}\\n`, keepExisting: true },\n ];\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function packageVersion(): Promise<string> {\n try {\n const pkg = JSON.parse(await readFile(new URL(\"../package.json\", import.meta.url), \"utf8\"));\n return typeof pkg.version === \"string\" ? pkg.version : \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\nexport async function main(\n argv: string[],\n env: Record<string, string | undefined>,\n context: CliContext = {},\n): Promise<number> {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n target: { type: \"string\" },\n domain: { type: \"string\", multiple: true },\n upstream: { type: \"string\" },\n dir: { type: \"string\" },\n rule: { type: \"string\", multiple: true },\n \"max-body\": { type: \"string\" },\n \"dry-run\": { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n });\n\n if (values.help || positionals.length === 0) {\n process.stdout.write(HELP);\n return values.help ? 0 : 1;\n }\n if (positionals[0] !== \"init\") return fail(`Unknown command \"${positionals[0]}\". Only \"init\" exists.`);\n\n const target = values.target ?? \"swa\";\n if (target !== \"swa\") return fail(`Unknown target \"${target}\". Only \"swa\" exists.`);\n\n const domains = values.domain ?? (env[\"PLAUSIBLE_DOMAIN\"] ? [env[\"PLAUSIBLE_DOMAIN\"]] : []);\n if (domains.length === 0) return fail(\"No domain. Pass --domain or set PLAUSIBLE_DOMAIN.\");\n\n const maxBodyLength = values[\"max-body\"] === undefined ? undefined : Number(values[\"max-body\"]);\n if (maxBodyLength !== undefined && !(Number.isInteger(maxBodyLength) && maxBodyLength > 0)) {\n return fail(`--max-body must be a positive whole number, got \"${values[\"max-body\"]}\".`);\n }\n\n let events: Record<string, RelayRule>;\n try {\n events = parseRules(values.rule ?? []);\n } catch (error) {\n return fail(error instanceof Error ? error.message : String(error));\n }\n\n const config: RelayOptions = {\n domain: domains.length === 1 ? (domains[0] as string) : domains,\n ...(values.upstream ? { upstream: values.upstream } : {}),\n ...(maxBodyLength ? { maxBodyLength } : {}),\n ...(Object.keys(events).length > 0 ? { events } : {}),\n };\n\n const templateDir = context.templateDir ?? new URL(\"./templates/\", import.meta.url);\n const template = await readFile(new URL(\"swa.mjs\", templateDir), \"utf8\");\n const files = swaFiles(template, config, values.dir ?? \"api\", {\n version: context.version ?? (await packageVersion()),\n now: context.now ?? new Date(),\n });\n\n for (const file of files) {\n if (values[\"dry-run\"]) {\n process.stdout.write(`--- ${file.path}\\n${file.content}\\n`);\n continue;\n }\n const present = await exists(file.path);\n if (present && file.keepExisting) {\n process.stdout.write(`kept ${file.path}\\n`);\n continue;\n }\n await mkdir(join(file.path, \"..\"), { recursive: true });\n await writeFile(file.path, file.content);\n process.stdout.write(`${present ? \"updated\" : \"created\"} ${file.path}\\n`);\n }\n return 0;\n}\n\nexport function fail(message: string): number {\n process.stderr.write(`astro-plausible: ${message}\\n`);\n return 1;\n}\n","#!/usr/bin/env node\nimport { fail, main } from \"./cli\";\n\nmain(process.argv.slice(2), process.env).then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.exitCode = fail(error instanceof Error ? error.message : String(error));\n },\n);\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { n as PlausibleOptions } from "./options-WE-C48Ul.js";
2
+ import { CustomProperties, PlausibleConfig, PlausibleEventOptions, PlausibleRequestPayload, track } from "@plausible-analytics/tracker";
3
+ //#region src/client.d.ts
4
+ export interface PlausibleClientOptions extends Omit<PlausibleOptions, "customProperties"> {
5
+ /** Custom properties for every event, or a function of the event name that returns them. */
6
+ customProperties?: PlausibleConfig["customProperties"];
7
+ /** Change or drop a request before it is sent. Return null to drop it. */
8
+ transformRequest?: PlausibleConfig["transformRequest"];
9
+ }
10
+ /**
11
+ * Starts the tracker. The first call wins; later calls do nothing, so it is safe in a script that runs on every
12
+ * page. The tracker itself allows a single `init()`.
13
+ */
14
+ export declare function startPlausible(options: PlausibleClientOptions): void;
15
+ //#endregion
16
+ export { type CustomProperties, type PlausibleEventOptions, type PlausibleRequestPayload, track };
17
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;iBAUiB,+BAA+B,KAAK;;EAEnD,mBAAmB;;EAEnB,mBAAmB;;;;;;wBASL,eAAe,SAAS"}
package/dist/client.js ADDED
@@ -0,0 +1,28 @@
1
+ import { n as plausibleConfig } from "./options-CZaM-Wb7.js";
2
+ import { init, track } from "@plausible-analytics/tracker";
3
+ //#region src/client.ts
4
+ /**
5
+ * Browser side. Starts the official Plausible tracker (the `@plausible-analytics/tracker` package, bundled into the
6
+ * site) pointed at the relay on your own domain.
7
+ */
8
+ let started = false;
9
+ /**
10
+ * Starts the tracker. The first call wins; later calls do nothing, so it is safe in a script that runs on every
11
+ * page. The tracker itself allows a single `init()`.
12
+ */
13
+ function startPlausible(options) {
14
+ if (started || typeof window === "undefined") return;
15
+ started = true;
16
+ const { customProperties, transformRequest, ...rest } = options;
17
+ const config = plausibleConfig(typeof customProperties === "function" || customProperties === void 0 ? rest : {
18
+ ...rest,
19
+ customProperties
20
+ });
21
+ if (typeof customProperties === "function") config.customProperties = customProperties;
22
+ if (transformRequest) config.transformRequest = transformRequest;
23
+ init(config);
24
+ }
25
+ //#endregion
26
+ export { startPlausible, track };
27
+
28
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * Browser side. Starts the official Plausible tracker (the `@plausible-analytics/tracker` package, bundled into the\n * site) pointed at the relay on your own domain.\n */\nimport { init, type PlausibleConfig, track } from \"@plausible-analytics/tracker\";\nimport { type PlausibleOptions, plausibleConfig } from \"./options\";\n\nexport type { CustomProperties, PlausibleEventOptions, PlausibleRequestPayload } from \"@plausible-analytics/tracker\";\nexport { track };\n\nexport interface PlausibleClientOptions extends Omit<PlausibleOptions, \"customProperties\"> {\n /** Custom properties for every event, or a function of the event name that returns them. */\n customProperties?: PlausibleConfig[\"customProperties\"];\n /** Change or drop a request before it is sent. Return null to drop it. */\n transformRequest?: PlausibleConfig[\"transformRequest\"];\n}\n\nlet started = false;\n\n/**\n * Starts the tracker. The first call wins; later calls do nothing, so it is safe in a script that runs on every\n * page. The tracker itself allows a single `init()`.\n */\nexport function startPlausible(options: PlausibleClientOptions): void {\n if (started || typeof window === \"undefined\") return;\n started = true;\n\n const { customProperties, transformRequest, ...rest } = options;\n const config = plausibleConfig(\n typeof customProperties === \"function\" || customProperties === undefined ? rest : { ...rest, customProperties },\n );\n if (typeof customProperties === \"function\") config.customProperties = customProperties;\n if (transformRequest) config.transformRequest = transformRequest;\n init(config);\n}\n"],"mappings":";;;;;;;AAiBA,IAAI,UAAU;;;;;AAMd,SAAgB,eAAe,SAAuC;CACpE,IAAI,WAAW,OAAO,WAAW,aAAa;CAC9C,UAAU;CAEV,MAAM,EAAE,kBAAkB,kBAAkB,GAAG,SAAS;CACxD,MAAM,SAAS,gBACb,OAAO,qBAAqB,cAAc,qBAAqB,KAAA,IAAY,OAAO;EAAE,GAAG;EAAM;CAAiB,CAChH;CACA,IAAI,OAAO,qBAAqB,YAAY,OAAO,mBAAmB;CACtE,IAAI,kBAAkB,OAAO,mBAAmB;CAChD,KAAK,MAAM;AACb"}
@@ -0,0 +1,3 @@
1
+ import { n as PlausibleOptions, r as plausibleConfig, t as DEFAULT_ENDPOINT } from "./options-WE-C48Ul.js";
2
+ import { PlausibleIntegrationOptions, clientScript } from "./integration.js";
3
+ export { DEFAULT_ENDPOINT, type PlausibleIntegrationOptions, type PlausibleOptions, clientScript, plausibleConfig };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { n as plausibleConfig, t as DEFAULT_ENDPOINT } from "./options-CZaM-Wb7.js";
2
+ import { clientScript } from "./integration.js";
3
+ export { DEFAULT_ENDPOINT, clientScript, plausibleConfig };
@@ -0,0 +1,31 @@
1
+ import { n as PlausibleOptions } from "./options-WE-C48Ul.js";
2
+ import { AstroIntegration } from "astro";
3
+ //#region src/integration.d.ts
4
+ export interface PlausibleIntegrationOptions extends PlausibleOptions {
5
+ /**
6
+ * Whether to add the tracker to pages. Default is to add it for `astro build` and leave it out of `astro dev`,
7
+ * so local browsing does not show up in the dashboard. Set `true` to track in dev too (with
8
+ * `captureOnLocalhost: true`, or the tracker ignores localhost anyway).
9
+ */
10
+ enabled?: boolean;
11
+ }
12
+ /** The client script injected on every page. Exported for tests. */
13
+ export declare function clientScript(options: PlausibleOptions): string;
14
+ /**
15
+ * Astro integration. Bundles the Plausible tracker into every page and points it at `endpoint`, which defaults to
16
+ * `/api/event` on the site itself. Pair it with the relay from `astro-plausible init` so events reach Plausible
17
+ * from your own domain.
18
+ *
19
+ * @example
20
+ * ```js
21
+ * // astro.config.mjs
22
+ * import plausible from "@sjohansson/astro-plausible/integration";
23
+ *
24
+ * export default defineConfig({
25
+ * integrations: [plausible({ domain: "example.com" })],
26
+ * });
27
+ * ```
28
+ */
29
+ export default function plausibleIntegration(options: PlausibleIntegrationOptions): AstroIntegration;
30
+ //#endregion
31
+ //# sourceMappingURL=integration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.d.ts","names":[],"sources":["../src/integration.ts"],"mappings":";;;iBAGiB,oCAAoC;;;;;;EAMnD;;;wBAIc,aAAa,SAAS;;;;;;;;;;;;;;;;wBAsBd,qBAAqB,SAAS,8BAA8B"}
@@ -0,0 +1,41 @@
1
+ import { n as plausibleConfig } from "./options-CZaM-Wb7.js";
2
+ //#region src/integration.ts
3
+ /** The client script injected on every page. Exported for tests. */
4
+ function clientScript(options) {
5
+ return ["import { startPlausible } from \"@sjohansson/astro-plausible/client\";", `startPlausible(${JSON.stringify(plausibleConfig(options))});`].join("\n");
6
+ }
7
+ /**
8
+ * Astro integration. Bundles the Plausible tracker into every page and points it at `endpoint`, which defaults to
9
+ * `/api/event` on the site itself. Pair it with the relay from `astro-plausible init` so events reach Plausible
10
+ * from your own domain.
11
+ *
12
+ * @example
13
+ * ```js
14
+ * // astro.config.mjs
15
+ * import plausible from "@sjohansson/astro-plausible/integration";
16
+ *
17
+ * export default defineConfig({
18
+ * integrations: [plausible({ domain: "example.com" })],
19
+ * });
20
+ * ```
21
+ */
22
+ function plausibleIntegration(options) {
23
+ const { enabled, ...tracker } = options ?? {};
24
+ if (enabled !== void 0 && typeof enabled !== "boolean") throw new Error("[@sjohansson/astro-plausible] enabled must be a boolean.");
25
+ plausibleConfig(tracker);
26
+ return {
27
+ name: "@sjohansson/astro-plausible",
28
+ hooks: { "astro:config:setup": ({ command, injectScript, logger }) => {
29
+ if (!(enabled ?? command === "build")) {
30
+ logger.info(`Plausible tracker left out of this ${command}. Set enabled: true to include it.`);
31
+ return;
32
+ }
33
+ injectScript("page", clientScript(tracker));
34
+ logger.debug(`Plausible tracker added, sending to ${tracker.endpoint ?? "/api/event"}`);
35
+ } }
36
+ };
37
+ }
38
+ //#endregion
39
+ export { clientScript, plausibleIntegration as default };
40
+
41
+ //# sourceMappingURL=integration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.js","names":[],"sources":["../src/integration.ts"],"sourcesContent":["import type { AstroIntegration } from \"astro\";\nimport { type PlausibleOptions, plausibleConfig } from \"./options\";\n\nexport interface PlausibleIntegrationOptions extends PlausibleOptions {\n /**\n * Whether to add the tracker to pages. Default is to add it for `astro build` and leave it out of `astro dev`,\n * so local browsing does not show up in the dashboard. Set `true` to track in dev too (with\n * `captureOnLocalhost: true`, or the tracker ignores localhost anyway).\n */\n enabled?: boolean;\n}\n\n/** The client script injected on every page. Exported for tests. */\nexport function clientScript(options: PlausibleOptions): string {\n return [\n 'import { startPlausible } from \"@sjohansson/astro-plausible/client\";',\n `startPlausible(${JSON.stringify(plausibleConfig(options))});`,\n ].join(\"\\n\");\n}\n\n/**\n * Astro integration. Bundles the Plausible tracker into every page and points it at `endpoint`, which defaults to\n * `/api/event` on the site itself. Pair it with the relay from `astro-plausible init` so events reach Plausible\n * from your own domain.\n *\n * @example\n * ```js\n * // astro.config.mjs\n * import plausible from \"@sjohansson/astro-plausible/integration\";\n *\n * export default defineConfig({\n * integrations: [plausible({ domain: \"example.com\" })],\n * });\n * ```\n */\nexport default function plausibleIntegration(options: PlausibleIntegrationOptions): AstroIntegration {\n const { enabled, ...tracker } = options ?? {};\n if (enabled !== undefined && typeof enabled !== \"boolean\") {\n throw new Error(\"[@sjohansson/astro-plausible] enabled must be a boolean.\");\n }\n // Validate now so a bad config fails at startup, not on the first page.\n plausibleConfig(tracker);\n\n return {\n name: \"@sjohansson/astro-plausible\",\n hooks: {\n \"astro:config:setup\": ({ command, injectScript, logger }) => {\n const on = enabled ?? command === \"build\";\n if (!on) {\n logger.info(`Plausible tracker left out of this ${command}. Set enabled: true to include it.`);\n return;\n }\n injectScript(\"page\", clientScript(tracker));\n logger.debug(`Plausible tracker added, sending to ${tracker.endpoint ?? \"/api/event\"}`);\n },\n },\n };\n}\n"],"mappings":";;;AAaA,SAAgB,aAAa,SAAmC;CAC9D,OAAO,CACL,0EACA,kBAAkB,KAAK,UAAU,gBAAgB,OAAO,CAAC,EAAE,GAC7D,CAAC,CAAC,KAAK,IAAI;AACb;;;;;;;;;;;;;;;;AAiBA,SAAwB,qBAAqB,SAAwD;CACnG,MAAM,EAAE,SAAS,GAAG,YAAY,WAAW,CAAC;CAC5C,IAAI,YAAY,KAAA,KAAa,OAAO,YAAY,WAC9C,MAAM,IAAI,MAAM,0DAA0D;CAG5E,gBAAgB,OAAO;CAEvB,OAAO;EACL,MAAM;EACN,OAAO,EACL,uBAAuB,EAAE,SAAS,cAAc,aAAa;GAE3D,IAAI,EADO,WAAW,YAAY,UACzB;IACP,OAAO,KAAK,sCAAsC,QAAQ,mCAAmC;IAC7F;GACF;GACA,aAAa,QAAQ,aAAa,OAAO,CAAC;GAC1C,OAAO,MAAM,uCAAuC,QAAQ,YAAY,cAAc;EACxF,EACF;CACF;AACF"}
@@ -0,0 +1,51 @@
1
+ //#region src/options.ts
2
+ const DEFAULT_ENDPOINT = "/api/event";
3
+ const DOMAIN_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(\/[\w.-]+)*$/i;
4
+ const BOOLEAN_KEYS = [
5
+ "autoCapturePageviews",
6
+ "hashBasedRouting",
7
+ "outboundLinks",
8
+ "formSubmissions",
9
+ "captureOnLocalhost",
10
+ "logging",
11
+ "bindToWindow"
12
+ ];
13
+ function fail(message) {
14
+ throw new Error(`[@sjohansson/astro-plausible] ${message}`);
15
+ }
16
+ /** Validates the options and returns the config for the tracker's `init()`. Only keys that were set are included. */
17
+ function plausibleConfig(options) {
18
+ if (typeof options?.domain !== "string" || options.domain.length > 253 || !DOMAIN_PATTERN.test(options.domain)) fail("domain must be the site domain as entered in Plausible, like example.com.");
19
+ const endpoint = options.endpoint ?? "/api/event";
20
+ if (typeof endpoint !== "string" || endpoint.length === 0) fail("endpoint must be a non-empty string.");
21
+ const config = {
22
+ domain: options.domain,
23
+ endpoint
24
+ };
25
+ for (const key of BOOLEAN_KEYS) {
26
+ const value = options[key];
27
+ if (value === void 0) continue;
28
+ if (typeof value !== "boolean") fail(`${key} must be a boolean.`);
29
+ config[key] = value;
30
+ }
31
+ if (options.fileDownloads !== void 0) {
32
+ const value = options.fileDownloads;
33
+ const custom = typeof value === "object" && value !== null && Array.isArray(value.fileExtensions);
34
+ if (typeof value !== "boolean" && !custom) fail("fileDownloads must be a boolean or { fileExtensions: [...] }.");
35
+ config.fileDownloads = value;
36
+ }
37
+ if (options.customProperties !== void 0) {
38
+ const value = options.customProperties;
39
+ if (!(typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => [
40
+ "string",
41
+ "number",
42
+ "boolean"
43
+ ].includes(typeof v)))) fail("customProperties must be an object of strings, numbers and booleans.");
44
+ config.customProperties = value;
45
+ }
46
+ return config;
47
+ }
48
+ //#endregion
49
+ export { plausibleConfig as n, DEFAULT_ENDPOINT as t };
50
+
51
+ //# sourceMappingURL=options-CZaM-Wb7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options-CZaM-Wb7.js","names":[],"sources":["../src/options.ts"],"sourcesContent":["import type { PlausibleConfig } from \"@plausible-analytics/tracker\";\n\n/** Tracker settings that can be written down as JSON. These go straight to the tracker's `init()`. */\nexport interface PlausibleOptions {\n /** Your site's domain, as entered in Plausible. */\n domain: string;\n\n /**\n * Where events go. `/api/event` is the relay from `astro-plausible init` on your own domain. Set the full\n * Plausible URL, `https://plausible.io/api/event`, to skip the relay.\n * @default \"/api/event\"\n */\n endpoint?: string;\n\n /**\n * Send a pageview on load and on history navigation.\n * @default true\n */\n autoCapturePageviews?: boolean;\n\n /**\n * Track `#` paths as separate pages.\n * @default false\n */\n hashBasedRouting?: boolean;\n\n /**\n * Track clicks on links to other sites.\n * @default false\n */\n outboundLinks?: boolean;\n\n /**\n * Track file downloads, with Plausible's default file types or the ones given.\n * @default false\n */\n fileDownloads?: boolean | { fileExtensions: string[] };\n\n /**\n * Track form submissions.\n * @default false\n */\n formSubmissions?: boolean;\n\n /**\n * Also track on localhost.\n * @default false\n */\n captureOnLocalhost?: boolean;\n\n /**\n * Log ignored events to the console.\n * @default true\n */\n logging?: boolean;\n\n /** Custom properties added to every event. */\n customProperties?: Record<string, string | number | boolean>;\n\n /**\n * Expose the tracker as `window.plausible`. Plausible's installation check looks for it.\n * @default true\n */\n bindToWindow?: boolean;\n}\n\nexport const DEFAULT_ENDPOINT = \"/api/event\";\n\nconst DOMAIN_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(\\/[\\w.-]+)*$/i;\nconst BOOLEAN_KEYS = [\n \"autoCapturePageviews\",\n \"hashBasedRouting\",\n \"outboundLinks\",\n \"formSubmissions\",\n \"captureOnLocalhost\",\n \"logging\",\n \"bindToWindow\",\n] as const;\n\nfunction fail(message: string): never {\n throw new Error(`[@sjohansson/astro-plausible] ${message}`);\n}\n\n/** Validates the options and returns the config for the tracker's `init()`. Only keys that were set are included. */\nexport function plausibleConfig(options: PlausibleOptions): PlausibleConfig {\n if (typeof options?.domain !== \"string\" || options.domain.length > 253 || !DOMAIN_PATTERN.test(options.domain)) {\n fail(\"domain must be the site domain as entered in Plausible, like example.com.\");\n }\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n if (typeof endpoint !== \"string\" || endpoint.length === 0) fail(\"endpoint must be a non-empty string.\");\n\n const config: PlausibleConfig = { domain: options.domain, endpoint };\n\n for (const key of BOOLEAN_KEYS) {\n const value = options[key];\n if (value === undefined) continue;\n if (typeof value !== \"boolean\") fail(`${key} must be a boolean.`);\n config[key] = value;\n }\n if (options.fileDownloads !== undefined) {\n const value = options.fileDownloads;\n const custom = typeof value === \"object\" && value !== null && Array.isArray(value.fileExtensions);\n if (typeof value !== \"boolean\" && !custom) fail(\"fileDownloads must be a boolean or { fileExtensions: [...] }.\");\n config.fileDownloads = value;\n }\n if (options.customProperties !== undefined) {\n const value = options.customProperties;\n const plain =\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.values(value).every((v) => [\"string\", \"number\", \"boolean\"].includes(typeof v));\n if (!plain) fail(\"customProperties must be an object of strings, numbers and booleans.\");\n config.customProperties = value;\n }\n return config;\n}\n"],"mappings":";AAkEA,MAAa,mBAAmB;AAEhC,MAAM,iBAAiB;AACvB,MAAM,eAAe;CACnB;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAS,KAAK,SAAwB;CACpC,MAAM,IAAI,MAAM,iCAAiC,SAAS;AAC5D;;AAGA,SAAgB,gBAAgB,SAA4C;CAC1E,IAAI,OAAO,SAAS,WAAW,YAAY,QAAQ,OAAO,SAAS,OAAO,CAAC,eAAe,KAAK,QAAQ,MAAM,GAC3G,KAAK,2EAA2E;CAElF,MAAM,WAAW,QAAQ,YAAA;CACzB,IAAI,OAAO,aAAa,YAAY,SAAS,WAAW,GAAG,KAAK,sCAAsC;CAEtG,MAAM,SAA0B;EAAE,QAAQ,QAAQ;EAAQ;CAAS;CAEnE,KAAK,MAAM,OAAO,cAAc;EAC9B,MAAM,QAAQ,QAAQ;EACtB,IAAI,UAAU,KAAA,GAAW;EACzB,IAAI,OAAO,UAAU,WAAW,KAAK,GAAG,IAAI,oBAAoB;EAChE,OAAO,OAAO;CAChB;CACA,IAAI,QAAQ,kBAAkB,KAAA,GAAW;EACvC,MAAM,QAAQ,QAAQ;EACtB,MAAM,SAAS,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,MAAM,cAAc;EAChG,IAAI,OAAO,UAAU,aAAa,CAAC,QAAQ,KAAK,+DAA+D;EAC/G,OAAO,gBAAgB;CACzB;CACA,IAAI,QAAQ,qBAAqB,KAAA,GAAW;EAC1C,MAAM,QAAQ,QAAQ;EAMtB,IAAI,EAJF,OAAO,UAAU,YACjB,UAAU,QACV,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,OAAO,KAAK,CAAC,CAAC,OAAO,MAAM;GAAC;GAAU;GAAU;EAAS,CAAC,CAAC,SAAS,OAAO,CAAC,CAAC,IAC1E,KAAK,sEAAsE;EACvF,OAAO,mBAAmB;CAC5B;CACA,OAAO;AACT"}
@@ -0,0 +1,63 @@
1
+ import { PlausibleConfig } from "@plausible-analytics/tracker";
2
+ //#region src/options.d.ts
3
+ /** Tracker settings that can be written down as JSON. These go straight to the tracker's `init()`. */
4
+ interface PlausibleOptions {
5
+ /** Your site's domain, as entered in Plausible. */
6
+ domain: string;
7
+ /**
8
+ * Where events go. `/api/event` is the relay from `astro-plausible init` on your own domain. Set the full
9
+ * Plausible URL, `https://plausible.io/api/event`, to skip the relay.
10
+ * @default "/api/event"
11
+ */
12
+ endpoint?: string;
13
+ /**
14
+ * Send a pageview on load and on history navigation.
15
+ * @default true
16
+ */
17
+ autoCapturePageviews?: boolean;
18
+ /**
19
+ * Track `#` paths as separate pages.
20
+ * @default false
21
+ */
22
+ hashBasedRouting?: boolean;
23
+ /**
24
+ * Track clicks on links to other sites.
25
+ * @default false
26
+ */
27
+ outboundLinks?: boolean;
28
+ /**
29
+ * Track file downloads, with Plausible's default file types or the ones given.
30
+ * @default false
31
+ */
32
+ fileDownloads?: boolean | {
33
+ fileExtensions: string[];
34
+ };
35
+ /**
36
+ * Track form submissions.
37
+ * @default false
38
+ */
39
+ formSubmissions?: boolean;
40
+ /**
41
+ * Also track on localhost.
42
+ * @default false
43
+ */
44
+ captureOnLocalhost?: boolean;
45
+ /**
46
+ * Log ignored events to the console.
47
+ * @default true
48
+ */
49
+ logging?: boolean;
50
+ /** Custom properties added to every event. */
51
+ customProperties?: Record<string, string | number | boolean>;
52
+ /**
53
+ * Expose the tracker as `window.plausible`. Plausible's installation check looks for it.
54
+ * @default true
55
+ */
56
+ bindToWindow?: boolean;
57
+ }
58
+ declare const DEFAULT_ENDPOINT = "/api/event";
59
+ /** Validates the options and returns the config for the tracker's `init()`. Only keys that were set are included. */
60
+ declare function plausibleConfig(options: PlausibleOptions): PlausibleConfig;
61
+ //#endregion
62
+ export { PlausibleOptions as n, plausibleConfig as r, DEFAULT_ENDPOINT as t };
63
+ //# sourceMappingURL=options-WE-C48Ul.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options-WE-C48Ul.d.ts","names":[],"sources":["../src/options.ts"],"mappings":";;;UAGiB;;EAEf;;;;;;EAOA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;EAMA;IAA4B;;;;;;EAM5B;;;;;EAMA;;;;;EAMA;;EAGA,mBAAmB;;;;;EAMnB;;cAGW;;iBAkBG,gBAAgB,SAAS,mBAAmB"}
@@ -0,0 +1,82 @@
1
+ //#region src/relay.d.ts
2
+ /**
3
+ * The relay forwards tracker events from your domain to Plausible. It is written against the web standard Request
4
+ * and Response, so it runs anywhere fetch does: an Astro endpoint, Cloudflare, Netlify, Vercel, or through the
5
+ * `swaHandler` adapter, Azure Static Web Apps managed functions.
6
+ *
7
+ * It checks the domain so the endpoint only relays for your sites, passes the visitor's IP in `X-Forwarded-For`
8
+ * and their user agent (Plausible drops events without a real visitor IP), caps the body size, and can validate
9
+ * custom events.
10
+ */
11
+ /** Per property pattern that an event's props must match. Only the listed properties are forwarded. */
12
+ export type RelayRule = Record<string, RegExp | string>;
13
+ export interface RelayOptions {
14
+ /** Site domain as entered in Plausible, or several when one relay serves several sites. */
15
+ domain: string | string[];
16
+ /**
17
+ * The Plausible host. Events are posted to `<upstream>/api/event`. Self-hosted: `https://plausible.example.com`.
18
+ * @default "https://plausible.io"
19
+ */
20
+ upstream?: string;
21
+ /**
22
+ * Largest request body accepted, in bytes.
23
+ * @default 16384
24
+ */
25
+ maxBodyLength?: number;
26
+ /**
27
+ * Validation for custom events, by event name. An event with a rule is forwarded only if every listed property
28
+ * is a string matching its pattern, and with only those properties. Events without a rule pass through as is.
29
+ */
30
+ events?: Record<string, RelayRule>;
31
+ /** Called when Plausible cannot be reached. */
32
+ onError?: (error: unknown) => void;
33
+ }
34
+ /** Incoming request headers with lowercased names, as Node and most platforms give them. */
35
+ export type RelayHeaders = Record<string, string | string[] | undefined>;
36
+ export interface RelayForward {
37
+ url: string;
38
+ body: string;
39
+ headers: Record<string, string>;
40
+ }
41
+ export interface RelayResult {
42
+ status: number;
43
+ contentType: string;
44
+ body: string;
45
+ }
46
+ export interface Relay {
47
+ /** Builds the upstream request, or null when the event is not for this relay or not valid. */
48
+ forward(rawBody: unknown, headers: RelayHeaders, clientIp?: string): RelayForward | null;
49
+ /** Sends a forward to Plausible. Throws when Plausible cannot be reached. */
50
+ send(forward: RelayForward): Promise<RelayResult>;
51
+ /** Handles a web standard request end to end. Pass the visitor IP when the platform gives it to you. */
52
+ handle(request: Request, context?: {
53
+ clientIp?: string;
54
+ }): Promise<Response>;
55
+ }
56
+ export declare const DEFAULT_UPSTREAM = "https://plausible.io";
57
+ export declare const DEFAULT_MAX_BODY_LENGTH: number;
58
+ /** Visitor IP from platform headers, without any port. Undefined when no header carries one. */
59
+ export declare function clientIp(headers: RelayHeaders): string | undefined;
60
+ export declare function createRelay(options: RelayOptions): Relay;
61
+ /** The subset of the Azure Functions v3 context the adapter uses. */
62
+ export interface SwaContext {
63
+ log: {
64
+ warn(message: string): void;
65
+ };
66
+ res?: {
67
+ status: number;
68
+ headers?: Record<string, string>;
69
+ body?: string;
70
+ };
71
+ }
72
+ export interface SwaRequest {
73
+ rawBody?: unknown;
74
+ headers?: RelayHeaders;
75
+ }
76
+ /**
77
+ * Adapter for Azure Static Web Apps managed functions (Node, `function.json` model). `astro-plausible init` writes
78
+ * a function that uses this.
79
+ */
80
+ export declare function swaHandler(relay: Relay): (context: SwaContext, req: SwaRequest) => Promise<void>;
81
+ //#endregion
82
+ //# sourceMappingURL=relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.d.ts","names":[],"sources":["../src/relay.ts"],"mappings":";;;;;;;;;;;YAWY,YAAY,eAAe;iBAEtB;;EAEf;;;;;EAMA;;;;;EAMA;;;;;EAMA,SAAS,eAAe;;EAGxB,WAAW;;;YAID,eAAe;iBAEV;EACf;EACA;EACA,SAAS;;iBAGM;EACf;EACA;EACA;;iBAGe;;EAEf,QAAQ,kBAAkB,SAAS,cAAc,oBAAoB;;EAErE,KAAK,SAAS,eAAe,QAAQ;;EAErC,OAAO,SAAS,SAAS;IAAY;MAAsB,QAAQ;;qBAGxD;qBACA;;wBAmBG,SAAS,SAAS;wBA+ClB,YAAY,SAAS,eAAe;;iBAwEnC;EACf;IAAO,KAAK;;EACZ;IAAQ;IAAgB,UAAU;IAAwB;;;iBAG3C;EACf;EACA,UAAU;;;;;;wBAOI,WAAW,OAAO,SAAS,SAAS,YAAY,KAAK,eAAe"}
package/dist/relay.js ADDED
@@ -0,0 +1,156 @@
1
+ //#region src/relay.ts
2
+ const DEFAULT_UPSTREAM = "https://plausible.io";
3
+ const DEFAULT_MAX_BODY_LENGTH = 16384;
4
+ const IP_HEADERS = [
5
+ "x-azure-clientip",
6
+ "x-client-ip",
7
+ "cf-connecting-ip",
8
+ "x-nf-client-connection-ip",
9
+ "x-real-ip",
10
+ "x-forwarded-for"
11
+ ];
12
+ function header(headers, name) {
13
+ const value = headers[name];
14
+ return Array.isArray(value) ? value[0] : value;
15
+ }
16
+ /** Visitor IP from platform headers, without any port. Undefined when no header carries one. */
17
+ function clientIp(headers) {
18
+ for (const name of IP_HEADERS) {
19
+ const raw = header(headers, name)?.split(",")[0]?.trim();
20
+ if (!raw) continue;
21
+ const bracketed = raw.match(/^\[(.+)\]:\d+$/);
22
+ if (bracketed) return bracketed[1];
23
+ const ipv4WithPort = raw.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/);
24
+ return ipv4WithPort ? ipv4WithPort[1] : raw;
25
+ }
26
+ }
27
+ function compileRules(events = {}) {
28
+ const compiled = /* @__PURE__ */ new Map();
29
+ for (const [name, rule] of Object.entries(events)) {
30
+ const entries = Object.entries(rule).map(([property, pattern]) => [property, pattern instanceof RegExp ? pattern : new RegExp(pattern)]);
31
+ if (entries.length === 0) throw new Error(`[@sjohansson/astro-plausible] Rule for event "${name}" lists no properties.`);
32
+ compiled.set(name, entries);
33
+ }
34
+ return compiled;
35
+ }
36
+ /** Applies the rule for an event. Returns the props to forward, or null when the event is not valid. */
37
+ function applyRule(rules, props) {
38
+ const source = props && typeof props === "object" ? props : {};
39
+ const kept = {};
40
+ for (const [property, pattern] of rules) {
41
+ const value = source[property];
42
+ if (typeof value !== "string" || !pattern.test(value)) return null;
43
+ kept[property] = value;
44
+ }
45
+ return kept;
46
+ }
47
+ function headersToRecord(headers) {
48
+ const record = {};
49
+ headers.forEach((value, name) => {
50
+ record[name.toLowerCase()] = value;
51
+ });
52
+ return record;
53
+ }
54
+ function createRelay(options) {
55
+ const domains = new Set(Array.isArray(options.domain) ? options.domain : [options.domain]);
56
+ if (domains.size === 0 || [...domains].some((d) => typeof d !== "string" || d.length === 0)) throw new Error("[@sjohansson/astro-plausible] domain is required.");
57
+ const upstream = `${(options.upstream ?? "https://plausible.io").replace(/\/$/, "")}/api/event`;
58
+ const maxBodyLength = options.maxBodyLength ?? 16384;
59
+ const rules = compileRules(options.events);
60
+ const relay = {
61
+ forward(rawBody, headers, ip) {
62
+ if (typeof rawBody !== "string" || rawBody.length === 0 || rawBody.length > maxBodyLength) return null;
63
+ let event;
64
+ try {
65
+ event = JSON.parse(rawBody);
66
+ } catch {
67
+ return null;
68
+ }
69
+ if (!event || typeof event !== "object" || Array.isArray(event)) return null;
70
+ if (typeof event["d"] !== "string" || !domains.has(event["d"])) return null;
71
+ const name = event["n"];
72
+ if (typeof name !== "string" || name.length === 0) return null;
73
+ if (rules.has(name)) {
74
+ const kept = applyRule(rules.get(name) ?? [], event["p"]);
75
+ if (!kept) return null;
76
+ event["p"] = kept;
77
+ }
78
+ const forwardHeaders = { "content-type": "application/json" };
79
+ const userAgent = header(headers, "user-agent");
80
+ if (userAgent) forwardHeaders["user-agent"] = userAgent;
81
+ const visitorIp = ip ?? clientIp(headers);
82
+ if (visitorIp) forwardHeaders["x-forwarded-for"] = visitorIp;
83
+ return {
84
+ url: upstream,
85
+ body: JSON.stringify(event),
86
+ headers: forwardHeaders
87
+ };
88
+ },
89
+ async send(forward) {
90
+ const res = await fetch(forward.url, {
91
+ method: "POST",
92
+ headers: forward.headers,
93
+ body: forward.body
94
+ });
95
+ return {
96
+ status: res.status,
97
+ contentType: res.headers.get("content-type") ?? "text/plain",
98
+ body: await res.text()
99
+ };
100
+ },
101
+ async handle(request, context = {}) {
102
+ if (request.method !== "POST") return new Response(null, {
103
+ status: 405,
104
+ headers: { allow: "POST" }
105
+ });
106
+ if (Number(request.headers.get("content-length")) > maxBodyLength) return new Response(null, { status: 413 });
107
+ const forward = relay.forward(await request.text(), headersToRecord(request.headers), context.clientIp);
108
+ if (!forward) return new Response(null, { status: 400 });
109
+ try {
110
+ const result = await relay.send(forward);
111
+ return new Response(result.body, {
112
+ status: result.status,
113
+ headers: {
114
+ "content-type": result.contentType,
115
+ "cache-control": "no-store"
116
+ }
117
+ });
118
+ } catch (error) {
119
+ options.onError?.(error);
120
+ return new Response(null, { status: 502 });
121
+ }
122
+ }
123
+ };
124
+ return relay;
125
+ }
126
+ /**
127
+ * Adapter for Azure Static Web Apps managed functions (Node, `function.json` model). `astro-plausible init` writes
128
+ * a function that uses this.
129
+ */
130
+ function swaHandler(relay) {
131
+ return async (context, req) => {
132
+ const forward = relay.forward(req.rawBody, req.headers ?? {});
133
+ if (!forward) {
134
+ context.res = { status: 400 };
135
+ return;
136
+ }
137
+ try {
138
+ const result = await relay.send(forward);
139
+ context.res = {
140
+ status: result.status,
141
+ headers: {
142
+ "content-type": result.contentType,
143
+ "cache-control": "no-store"
144
+ },
145
+ body: result.body
146
+ };
147
+ } catch (error) {
148
+ context.log.warn(`Plausible forward failed: ${error}`);
149
+ context.res = { status: 502 };
150
+ }
151
+ };
152
+ }
153
+ //#endregion
154
+ export { DEFAULT_MAX_BODY_LENGTH, DEFAULT_UPSTREAM, clientIp, createRelay, swaHandler };
155
+
156
+ //# sourceMappingURL=relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.js","names":[],"sources":["../src/relay.ts"],"sourcesContent":["/**\n * The relay forwards tracker events from your domain to Plausible. It is written against the web standard Request\n * and Response, so it runs anywhere fetch does: an Astro endpoint, Cloudflare, Netlify, Vercel, or through the\n * `swaHandler` adapter, Azure Static Web Apps managed functions.\n *\n * It checks the domain so the endpoint only relays for your sites, passes the visitor's IP in `X-Forwarded-For`\n * and their user agent (Plausible drops events without a real visitor IP), caps the body size, and can validate\n * custom events.\n */\n\n/** Per property pattern that an event's props must match. Only the listed properties are forwarded. */\nexport type RelayRule = Record<string, RegExp | string>;\n\nexport interface RelayOptions {\n /** Site domain as entered in Plausible, or several when one relay serves several sites. */\n domain: string | string[];\n\n /**\n * The Plausible host. Events are posted to `<upstream>/api/event`. Self-hosted: `https://plausible.example.com`.\n * @default \"https://plausible.io\"\n */\n upstream?: string;\n\n /**\n * Largest request body accepted, in bytes.\n * @default 16384\n */\n maxBodyLength?: number;\n\n /**\n * Validation for custom events, by event name. An event with a rule is forwarded only if every listed property\n * is a string matching its pattern, and with only those properties. Events without a rule pass through as is.\n */\n events?: Record<string, RelayRule>;\n\n /** Called when Plausible cannot be reached. */\n onError?: (error: unknown) => void;\n}\n\n/** Incoming request headers with lowercased names, as Node and most platforms give them. */\nexport type RelayHeaders = Record<string, string | string[] | undefined>;\n\nexport interface RelayForward {\n url: string;\n body: string;\n headers: Record<string, string>;\n}\n\nexport interface RelayResult {\n status: number;\n contentType: string;\n body: string;\n}\n\nexport interface Relay {\n /** Builds the upstream request, or null when the event is not for this relay or not valid. */\n forward(rawBody: unknown, headers: RelayHeaders, clientIp?: string): RelayForward | null;\n /** Sends a forward to Plausible. Throws when Plausible cannot be reached. */\n send(forward: RelayForward): Promise<RelayResult>;\n /** Handles a web standard request end to end. Pass the visitor IP when the platform gives it to you. */\n handle(request: Request, context?: { clientIp?: string }): Promise<Response>;\n}\n\nexport const DEFAULT_UPSTREAM = \"https://plausible.io\";\nexport const DEFAULT_MAX_BODY_LENGTH = 16 * 1024;\n\n// Platform headers that carry the visitor IP, most specific first. x-forwarded-for is last because proxies append\n// to it and some let the client seed it.\nconst IP_HEADERS = [\n \"x-azure-clientip\",\n \"x-client-ip\",\n \"cf-connecting-ip\",\n \"x-nf-client-connection-ip\",\n \"x-real-ip\",\n \"x-forwarded-for\",\n];\n\nfunction header(headers: RelayHeaders, name: string): string | undefined {\n const value = headers[name];\n return Array.isArray(value) ? value[0] : value;\n}\n\n/** Visitor IP from platform headers, without any port. Undefined when no header carries one. */\nexport function clientIp(headers: RelayHeaders): string | undefined {\n for (const name of IP_HEADERS) {\n const raw = header(headers, name)?.split(\",\")[0]?.trim();\n if (!raw) continue;\n const bracketed = raw.match(/^\\[(.+)\\]:\\d+$/);\n if (bracketed) return bracketed[1];\n const ipv4WithPort = raw.match(/^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/);\n return ipv4WithPort ? ipv4WithPort[1] : raw;\n }\n return undefined;\n}\n\nfunction compileRules(events: Record<string, RelayRule> = {}): Map<string, [string, RegExp][]> {\n const compiled = new Map<string, [string, RegExp][]>();\n for (const [name, rule] of Object.entries(events)) {\n const entries = Object.entries(rule).map(([property, pattern]): [string, RegExp] => [\n property,\n pattern instanceof RegExp ? pattern : new RegExp(pattern),\n ]);\n if (entries.length === 0) {\n throw new Error(`[@sjohansson/astro-plausible] Rule for event \"${name}\" lists no properties.`);\n }\n compiled.set(name, entries);\n }\n return compiled;\n}\n\n/** Applies the rule for an event. Returns the props to forward, or null when the event is not valid. */\nfunction applyRule(rules: [string, RegExp][], props: unknown): Record<string, string> | null {\n const source = props && typeof props === \"object\" ? (props as Record<string, unknown>) : {};\n const kept: Record<string, string> = {};\n for (const [property, pattern] of rules) {\n const value = source[property];\n if (typeof value !== \"string\" || !pattern.test(value)) return null;\n kept[property] = value;\n }\n return kept;\n}\n\nfunction headersToRecord(headers: Headers): RelayHeaders {\n const record: RelayHeaders = {};\n headers.forEach((value, name) => {\n record[name.toLowerCase()] = value;\n });\n return record;\n}\n\nexport function createRelay(options: RelayOptions): Relay {\n const domains = new Set(Array.isArray(options.domain) ? options.domain : [options.domain]);\n if (domains.size === 0 || [...domains].some((d) => typeof d !== \"string\" || d.length === 0)) {\n throw new Error(\"[@sjohansson/astro-plausible] domain is required.\");\n }\n const upstream = `${(options.upstream ?? DEFAULT_UPSTREAM).replace(/\\/$/, \"\")}/api/event`;\n const maxBodyLength = options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH;\n const rules = compileRules(options.events);\n\n const relay: Relay = {\n forward(rawBody, headers, ip) {\n if (typeof rawBody !== \"string\" || rawBody.length === 0 || rawBody.length > maxBodyLength) return null;\n\n let event: Record<string, unknown>;\n try {\n event = JSON.parse(rawBody);\n } catch {\n return null;\n }\n if (!event || typeof event !== \"object\" || Array.isArray(event)) return null;\n if (typeof event[\"d\"] !== \"string\" || !domains.has(event[\"d\"])) return null;\n const name = event[\"n\"];\n if (typeof name !== \"string\" || name.length === 0) return null;\n\n if (rules.has(name)) {\n const kept = applyRule(rules.get(name) ?? [], event[\"p\"]);\n if (!kept) return null;\n event[\"p\"] = kept;\n }\n\n const forwardHeaders: Record<string, string> = { \"content-type\": \"application/json\" };\n const userAgent = header(headers, \"user-agent\");\n if (userAgent) forwardHeaders[\"user-agent\"] = userAgent;\n const visitorIp = ip ?? clientIp(headers);\n if (visitorIp) forwardHeaders[\"x-forwarded-for\"] = visitorIp;\n\n return { url: upstream, body: JSON.stringify(event), headers: forwardHeaders };\n },\n\n async send(forward) {\n const res = await fetch(forward.url, { method: \"POST\", headers: forward.headers, body: forward.body });\n return {\n status: res.status,\n contentType: res.headers.get(\"content-type\") ?? \"text/plain\",\n body: await res.text(),\n };\n },\n\n async handle(request, context = {}) {\n if (request.method !== \"POST\") return new Response(null, { status: 405, headers: { allow: \"POST\" } });\n const length = Number(request.headers.get(\"content-length\"));\n if (length > maxBodyLength) return new Response(null, { status: 413 });\n\n const forward = relay.forward(await request.text(), headersToRecord(request.headers), context.clientIp);\n if (!forward) return new Response(null, { status: 400 });\n\n try {\n const result = await relay.send(forward);\n return new Response(result.body, {\n status: result.status,\n headers: { \"content-type\": result.contentType, \"cache-control\": \"no-store\" },\n });\n } catch (error) {\n options.onError?.(error);\n return new Response(null, { status: 502 });\n }\n },\n };\n return relay;\n}\n\n/** The subset of the Azure Functions v3 context the adapter uses. */\nexport interface SwaContext {\n log: { warn(message: string): void };\n res?: { status: number; headers?: Record<string, string>; body?: string };\n}\n\nexport interface SwaRequest {\n rawBody?: unknown;\n headers?: RelayHeaders;\n}\n\n/**\n * Adapter for Azure Static Web Apps managed functions (Node, `function.json` model). `astro-plausible init` writes\n * a function that uses this.\n */\nexport function swaHandler(relay: Relay): (context: SwaContext, req: SwaRequest) => Promise<void> {\n return async (context, req) => {\n const forward = relay.forward(req.rawBody, req.headers ?? {});\n if (!forward) {\n context.res = { status: 400 };\n return;\n }\n try {\n const result = await relay.send(forward);\n context.res = {\n status: result.status,\n headers: { \"content-type\": result.contentType, \"cache-control\": \"no-store\" },\n body: result.body,\n };\n } catch (error) {\n context.log.warn(`Plausible forward failed: ${error}`);\n context.res = { status: 502 };\n }\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,155 @@
1
+ const IP_HEADERS = [
2
+ "x-azure-clientip",
3
+ "x-client-ip",
4
+ "cf-connecting-ip",
5
+ "x-nf-client-connection-ip",
6
+ "x-real-ip",
7
+ "x-forwarded-for"
8
+ ];
9
+ function header(headers, name) {
10
+ const value = headers[name];
11
+ return Array.isArray(value) ? value[0] : value;
12
+ }
13
+ /** Visitor IP from platform headers, without any port. Undefined when no header carries one. */
14
+ function clientIp(headers) {
15
+ for (const name of IP_HEADERS) {
16
+ const raw = header(headers, name)?.split(",")[0]?.trim();
17
+ if (!raw) continue;
18
+ const bracketed = raw.match(/^\[(.+)\]:\d+$/);
19
+ if (bracketed) return bracketed[1];
20
+ const ipv4WithPort = raw.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/);
21
+ return ipv4WithPort ? ipv4WithPort[1] : raw;
22
+ }
23
+ }
24
+ function compileRules(events = {}) {
25
+ const compiled = /* @__PURE__ */ new Map();
26
+ for (const [name, rule] of Object.entries(events)) {
27
+ const entries = Object.entries(rule).map(([property, pattern]) => [property, pattern instanceof RegExp ? pattern : new RegExp(pattern)]);
28
+ if (entries.length === 0) throw new Error(`[@sjohansson/astro-plausible] Rule for event "${name}" lists no properties.`);
29
+ compiled.set(name, entries);
30
+ }
31
+ return compiled;
32
+ }
33
+ /** Applies the rule for an event. Returns the props to forward, or null when the event is not valid. */
34
+ function applyRule(rules, props) {
35
+ const source = props && typeof props === "object" ? props : {};
36
+ const kept = {};
37
+ for (const [property, pattern] of rules) {
38
+ const value = source[property];
39
+ if (typeof value !== "string" || !pattern.test(value)) return null;
40
+ kept[property] = value;
41
+ }
42
+ return kept;
43
+ }
44
+ function headersToRecord(headers) {
45
+ const record = {};
46
+ headers.forEach((value, name) => {
47
+ record[name.toLowerCase()] = value;
48
+ });
49
+ return record;
50
+ }
51
+ function createRelay(options) {
52
+ const domains = new Set(Array.isArray(options.domain) ? options.domain : [options.domain]);
53
+ if (domains.size === 0 || [...domains].some((d) => typeof d !== "string" || d.length === 0)) throw new Error("[@sjohansson/astro-plausible] domain is required.");
54
+ const upstream = `${(options.upstream ?? "https://plausible.io").replace(/\/$/, "")}/api/event`;
55
+ const maxBodyLength = options.maxBodyLength ?? 16384;
56
+ const rules = compileRules(options.events);
57
+ const relay = {
58
+ forward(rawBody, headers, ip) {
59
+ if (typeof rawBody !== "string" || rawBody.length === 0 || rawBody.length > maxBodyLength) return null;
60
+ let event;
61
+ try {
62
+ event = JSON.parse(rawBody);
63
+ } catch {
64
+ return null;
65
+ }
66
+ if (!event || typeof event !== "object" || Array.isArray(event)) return null;
67
+ if (typeof event["d"] !== "string" || !domains.has(event["d"])) return null;
68
+ const name = event["n"];
69
+ if (typeof name !== "string" || name.length === 0) return null;
70
+ if (rules.has(name)) {
71
+ const kept = applyRule(rules.get(name) ?? [], event["p"]);
72
+ if (!kept) return null;
73
+ event["p"] = kept;
74
+ }
75
+ const forwardHeaders = { "content-type": "application/json" };
76
+ const userAgent = header(headers, "user-agent");
77
+ if (userAgent) forwardHeaders["user-agent"] = userAgent;
78
+ const visitorIp = ip ?? clientIp(headers);
79
+ if (visitorIp) forwardHeaders["x-forwarded-for"] = visitorIp;
80
+ return {
81
+ url: upstream,
82
+ body: JSON.stringify(event),
83
+ headers: forwardHeaders
84
+ };
85
+ },
86
+ async send(forward) {
87
+ const res = await fetch(forward.url, {
88
+ method: "POST",
89
+ headers: forward.headers,
90
+ body: forward.body
91
+ });
92
+ return {
93
+ status: res.status,
94
+ contentType: res.headers.get("content-type") ?? "text/plain",
95
+ body: await res.text()
96
+ };
97
+ },
98
+ async handle(request, context = {}) {
99
+ if (request.method !== "POST") return new Response(null, {
100
+ status: 405,
101
+ headers: { allow: "POST" }
102
+ });
103
+ if (Number(request.headers.get("content-length")) > maxBodyLength) return new Response(null, { status: 413 });
104
+ const forward = relay.forward(await request.text(), headersToRecord(request.headers), context.clientIp);
105
+ if (!forward) return new Response(null, { status: 400 });
106
+ try {
107
+ const result = await relay.send(forward);
108
+ return new Response(result.body, {
109
+ status: result.status,
110
+ headers: {
111
+ "content-type": result.contentType,
112
+ "cache-control": "no-store"
113
+ }
114
+ });
115
+ } catch (error) {
116
+ options.onError?.(error);
117
+ return new Response(null, { status: 502 });
118
+ }
119
+ }
120
+ };
121
+ return relay;
122
+ }
123
+ /**
124
+ * Adapter for Azure Static Web Apps managed functions (Node, `function.json` model). `astro-plausible init` writes
125
+ * a function that uses this.
126
+ */
127
+ function swaHandler(relay) {
128
+ return async (context, req) => {
129
+ const forward = relay.forward(req.rawBody, req.headers ?? {});
130
+ if (!forward) {
131
+ context.res = { status: 400 };
132
+ return;
133
+ }
134
+ try {
135
+ const result = await relay.send(forward);
136
+ context.res = {
137
+ status: result.status,
138
+ headers: {
139
+ "content-type": result.contentType,
140
+ "cache-control": "no-store"
141
+ },
142
+ body: result.body
143
+ };
144
+ } catch (error) {
145
+ context.log.warn(`Plausible forward failed: ${error}`);
146
+ context.res = { status: 502 };
147
+ }
148
+ };
149
+ }
150
+ //#endregion
151
+ //#region src/templates/swa.ts
152
+ const relay = createRelay(__ASTRO_PLAUSIBLE_RELAY_CONFIG__);
153
+ var swa_default = swaHandler(relay);
154
+ //#endregion
155
+ export { swa_default as default, relay };
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "@sjohansson/astro-plausible",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.0",
4
+ "description": "First-party Plausible analytics for Astro sites. The official tracker is bundled into your site's own JavaScript and events go through a relay on your domain, so ad blockers do not drop them.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./integration": {
14
+ "types": "./dist/integration.d.ts",
15
+ "import": "./dist/integration.js"
16
+ },
17
+ "./client": {
18
+ "types": "./dist/client.d.ts",
19
+ "import": "./dist/client.js"
20
+ },
21
+ "./relay": {
22
+ "types": "./dist/relay.d.ts",
23
+ "import": "./dist/relay.js"
24
+ }
25
+ },
26
+ "bin": {
27
+ "astro-plausible": "./dist/bin.js"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "keywords": [
33
+ "astro",
34
+ "astro-integration",
35
+ "plausible",
36
+ "analytics",
37
+ "privacy",
38
+ "proxy",
39
+ "static-site"
40
+ ],
41
+ "author": "sjohansson",
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/sjohansson/astro-components.git",
46
+ "directory": "packages/astro-plausible"
47
+ },
48
+ "engines": {
49
+ "node": ">=22"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "dependencies": {
55
+ "@plausible-analytics/tracker": "^0.4.7"
56
+ },
57
+ "peerDependencies": {
58
+ "astro": "^5.0.0 || ^6.0.0 || ^7.0.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "astro": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "sideEffects": false,
66
+ "scripts": {
67
+ "build": "tsdown",
68
+ "dev": "tsdown --watch",
69
+ "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
70
+ "test": "vitest"
71
+ }
6
72
  }