@sjohansson/astro-plausible 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +205 -2
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +198 -0
- package/dist/bin.js.map +1 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +27 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/integration.d.ts +29 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +40 -0
- package/dist/integration.js.map +1 -0
- package/dist/options-B-SB8PRR.js +50 -0
- package/dist/options-B-SB8PRR.js.map +1 -0
- package/dist/options-C15OXXsk.d.ts +61 -0
- package/dist/options-C15OXXsk.d.ts.map +1 -0
- package/dist/relay.d.ts +92 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +189 -0
- package/dist/relay.js.map +1 -0
- package/dist/templates/swa.mjs +187 -0
- package/package.json +69 -3
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,206 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Astro Plausible
|
|
2
2
|
|
|
3
|
-
|
|
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). A client can send any of those headers, and the first one present wins, so on a platform that sets only
|
|
96
|
+
one of them, name it with `ipHeaders`.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// src/pages/api/event.ts, an Astro server endpoint
|
|
100
|
+
import { createRelay } from "@sjohansson/astro-plausible/relay";
|
|
101
|
+
import type { APIRoute } from "astro";
|
|
102
|
+
|
|
103
|
+
export const prerender = false;
|
|
104
|
+
|
|
105
|
+
const relay = createRelay({ domain: "example.com" });
|
|
106
|
+
|
|
107
|
+
export const POST: APIRoute = ({ request, clientAddress }) => relay.handle(request, { clientIp: clientAddress });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// functions/api/event.ts, a Cloudflare Pages function
|
|
112
|
+
import { createRelay } from "@sjohansson/astro-plausible/relay";
|
|
113
|
+
|
|
114
|
+
// Cloudflare sets cf-connecting-ip; any other IP header could come from the client.
|
|
115
|
+
const relay = createRelay({ domain: "example.com", ipHeaders: ["cf-connecting-ip"] });
|
|
116
|
+
|
|
117
|
+
export const onRequestPost = ({ request }) => relay.handle(request);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Relay options:
|
|
121
|
+
|
|
122
|
+
| Option | Default | Notes |
|
|
123
|
+
| --------------- | ------------------------ | ------------------------------------------------------------------------- |
|
|
124
|
+
| `domain` | | Required. One domain or several. Events for other domains get a 400. |
|
|
125
|
+
| `upstream` | `"https://plausible.io"` | Events are posted to `<upstream>/api/event`. |
|
|
126
|
+
| `ipHeaders` | | Headers to read the visitor IP from, in order, when none is passed in. |
|
|
127
|
+
| `maxBodyLength` | `16384` | Largest request body accepted, in bytes. |
|
|
128
|
+
| `events` | | Validation for custom events, see below. |
|
|
129
|
+
| `onError` | | Called when Plausible cannot be reached. The response is a 502 either way. |
|
|
130
|
+
|
|
131
|
+
The relay answers 405 for anything but POST, 413 for a body over the limit, 400 for anything it will not forward,
|
|
132
|
+
and otherwise passes Plausible's status and body through. The visitor IP goes in `X-Forwarded-For`.
|
|
133
|
+
|
|
134
|
+
### Validating custom events
|
|
135
|
+
|
|
136
|
+
If you build anything on custom events, say vote counts, the relay can make sure only well-formed ones get through.
|
|
137
|
+
A rule lists the properties an event may carry and a pattern for each. Events with a rule are forwarded only with
|
|
138
|
+
those properties, each matching its pattern. Events without a rule pass through untouched.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { voteRelayRule } from "@sjohansson/astro-votes";
|
|
142
|
+
|
|
143
|
+
const relay = createRelay({
|
|
144
|
+
domain: "example.com",
|
|
145
|
+
events: {
|
|
146
|
+
...voteRelayRule(),
|
|
147
|
+
signup: { plan: /^(free|pro)$/ },
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The same for the CLI:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pnpm exec astro-plausible init --domain example.com \
|
|
156
|
+
--rule "vote:vote=^[A-Za-z0-9._/-]{1,200}:(?:up|down)(?:-undo)?$" \
|
|
157
|
+
--rule "signup:plan=^(free|pro)$"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Tracking events
|
|
161
|
+
|
|
162
|
+
`window.plausible("signup")` works as in any Plausible setup. The typed way is the tracker's own `track`:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { track } from "@sjohansson/astro-plausible/client";
|
|
166
|
+
|
|
167
|
+
track("signup", { props: { plan: "pro" } });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Without the integration
|
|
171
|
+
|
|
172
|
+
Start the tracker yourself when you want a function for `customProperties`, a `transformRequest` hook, or control
|
|
173
|
+
over where it runs.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { startPlausible } from "@sjohansson/astro-plausible/client";
|
|
177
|
+
|
|
178
|
+
startPlausible({
|
|
179
|
+
domain: "example.com",
|
|
180
|
+
transformRequest: (payload) => (payload.u.includes("/admin") ? null : payload),
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The first call wins; later calls do nothing. The tracker itself allows a single `init()`.
|
|
185
|
+
|
|
186
|
+
### A path other than `/api/event`
|
|
187
|
+
|
|
188
|
+
Set `endpoint` to wherever the relay answers, for example `/stats/api/event`. Static Web Apps managed functions
|
|
189
|
+
always live under `/api`; a rewrite from another path to `/api/event` in `staticwebapp.config.json` may do it, but
|
|
190
|
+
that is untested and the Static Web Apps documentation says route rules for the API only support redirects and
|
|
191
|
+
roles.
|
|
192
|
+
|
|
193
|
+
### Local development
|
|
194
|
+
|
|
195
|
+
The integration leaves the tracker out of `astro dev`, and the tracker ignores localhost unless
|
|
196
|
+
`captureOnLocalhost` is set. To test the whole chain locally, set both and run the site through something that
|
|
197
|
+
serves the relay too, such as the Static Web Apps CLI.
|
|
198
|
+
|
|
199
|
+
## Keeping the tracker current
|
|
200
|
+
|
|
201
|
+
The tracker is a normal dependency, so Renovate or a routine upgrade brings new Plausible releases to your site.
|
|
202
|
+
Nothing is vendored.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
// 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
|
+
// src/bin.ts
|
|
192
|
+
main(process.argv.slice(2), process.env).then((code) => {
|
|
193
|
+
process.exitCode = code;
|
|
194
|
+
}, (error) => {
|
|
195
|
+
process.exitCode = fail(error instanceof Error ? error.message : String(error));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -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 // A replacement function, so `$` sequences in the patterns are inserted as is.\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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"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { n as PlausibleOptions } from "./options-C15OXXsk.js";
|
|
2
|
+
import { CustomProperties, PlausibleConfig, PlausibleEventOptions, PlausibleRequestPayload, track } from "@plausible-analytics/tracker";
|
|
3
|
+
export interface PlausibleClientOptions extends Omit<PlausibleOptions, "customProperties"> {
|
|
4
|
+
/** Custom properties for every event, or a function of the event name that returns them. */
|
|
5
|
+
customProperties?: PlausibleConfig["customProperties"];
|
|
6
|
+
/** Change or drop a request before it is sent. Return null to drop it. */
|
|
7
|
+
transformRequest?: PlausibleConfig["transformRequest"];
|
|
8
|
+
}
|
|
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
|
+
export declare function startPlausible(options: PlausibleClientOptions): void;
|
|
14
|
+
export { type CustomProperties, type PlausibleEventOptions, type PlausibleRequestPayload, track };
|
|
15
|
+
//# 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,27 @@
|
|
|
1
|
+
import { n as plausibleConfig } from "./options-B-SB8PRR.js";
|
|
2
|
+
import { init, track } from "@plausible-analytics/tracker";
|
|
3
|
+
// 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
|
+
export { startPlausible, track };
|
|
26
|
+
|
|
27
|
+
//# 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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { n as PlausibleOptions, r as plausibleConfig, t as DEFAULT_ENDPOINT } from "./options-C15OXXsk.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,29 @@
|
|
|
1
|
+
import { n as PlausibleOptions } from "./options-C15OXXsk.js";
|
|
2
|
+
import { AstroIntegration } from "astro";
|
|
3
|
+
export interface PlausibleIntegrationOptions extends PlausibleOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Whether to add the tracker to pages. Default is to add it for `astro build` and leave it out of `astro dev`,
|
|
6
|
+
* so local browsing does not show up in the dashboard. Set `true` to track in dev too (with
|
|
7
|
+
* `captureOnLocalhost: true`, or the tracker ignores localhost anyway).
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** The client script injected on every page. Exported for tests. */
|
|
12
|
+
export declare function clientScript(options: PlausibleOptions): string;
|
|
13
|
+
/**
|
|
14
|
+
* Astro integration. Bundles the Plausible tracker into every page and points it at `endpoint`, which defaults to
|
|
15
|
+
* `/api/event` on the site itself. Pair it with the relay from `astro-plausible init` so events reach Plausible
|
|
16
|
+
* from your own domain.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```js
|
|
20
|
+
* // astro.config.mjs
|
|
21
|
+
* import plausible from "@sjohansson/astro-plausible/integration";
|
|
22
|
+
*
|
|
23
|
+
* export default defineConfig({
|
|
24
|
+
* integrations: [plausible({ domain: "example.com" })],
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export default function plausibleIntegration(options: PlausibleIntegrationOptions): AstroIntegration;
|
|
29
|
+
//# 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,40 @@
|
|
|
1
|
+
import { n as plausibleConfig } from "./options-B-SB8PRR.js";
|
|
2
|
+
// 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
|
+
export { clientScript, plausibleIntegration as default };
|
|
39
|
+
|
|
40
|
+
//# 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,50 @@
|
|
|
1
|
+
// 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
|
+
export { plausibleConfig as n, DEFAULT_ENDPOINT as t };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=options-B-SB8PRR.js.map
|