@patchstack/connect 0.5.16 → 0.5.17

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/AGENT-INSTALL.md CHANGED
@@ -240,7 +240,9 @@ Handle it in this order:
240
240
 
241
241
  **Bun-managed projects:** `bun run` does not execute npm-style `pre`/`post` scripts, so wire the build script directly instead: `"build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"`.
242
242
 
243
- 3. **Verify the Patchstack Connector** — a floating control whose form follows the site's claim state: while the site is unclaimed it is a one-time "Connect this website" panel. It is part of the install and on by default; do not ask whether to add it. `scan` installs it automatically into a plain HTML shell **or a JSX root** (Next, Remix, React Router, TanStack Start, Gatsby), and `mark-build` carries it into built HTML. Only when `scan` reported that it found no editable shell at all — a root whose head mechanism is not a plain script tag, e.g. Nuxt's `useHead` or an Astro layout — add the one-liner it printed to the root layout yourself, just before `</body>` (never a JS entry point), reading `siteUuid` from `.patchstackrc.json`. On those same roots the widget also needs the production marker above the tag — `scan` adds it automatically to a JSX root, and prints it to paste when it finds no anchor. A server-rendered site without the marker serves the build-mode claim flow to its visitors:
243
+ **Checking a build yourself:** run it through the package manager (`npm run build`), never the framework's own CLI (`astro build`, `vite build`, `next build`). Calling the CLI directly skips the `prebuild`/`postbuild` hooks, so the build is not scanned, not marked and not reported, and it tells you nothing about what the deployed build will carry.
244
+
245
+ 3. **Verify the Patchstack Connector** — a floating control whose form follows the site's claim state: while the site is unclaimed it is a one-time "Connect this website" panel. It is part of the install and on by default; do not ask whether to add it. `scan` installs it automatically into a plain HTML shell, **a JSX root** (Next, Remix, React Router, TanStack Start, Gatsby) **or an Astro layout**, and `mark-build` carries it into built HTML. Only when `scan` reported that it found no editable shell at all — a root whose head mechanism is not a plain script tag, e.g. Nuxt's `useHead` — add the one-liner it printed to the root layout yourself, just before `</body>` (never a JS entry point), reading `siteUuid` from `.patchstackrc.json`. On those same roots the widget also needs the production marker above the tag — `scan` adds it automatically to a JSX root or Astro layout, and prints it to paste when it finds no anchor. A server-rendered site without the marker serves the build-mode claim flow to its visitors:
244
246
 
245
247
  ```html
246
248
  <script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer></script>
@@ -304,7 +306,7 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
304
306
  and `mark-build` only read and write files; `scan` and `mark-build` also report the dependency
305
307
  manifest they read.
306
308
 
307
- 5. **Commit** `.patchstackrc.json`, the updated `package.json`, the guard/framework source changes, and the layout/HTML file carrying the widget tag (and the production marker, when `scan` wrote one into a JSX root), so every developer and CI run reports to the same site.
309
+ 5. **Commit** `.patchstackrc.json`, the updated `package.json`, the guard/framework source changes, and the layout/HTML file carrying the widget tag (and the production marker, when `scan` wrote one into a JSX root or Astro layout), so every developer and CI run reports to the same site.
308
310
 
309
311
  **Do not commit `.patchstackrc.local.json`.** That file holds the API key issued at provision; the scan writes it and adds it to `.gitignore`, and tells you if it could not. `.patchstackrc.json` holds only the site UUID and settings, and the UUID is public by design — it ships in the widget tag in served HTML.
310
312
 
@@ -746,7 +748,7 @@ Remove only the pieces that are actually present — check for each first. If no
746
748
  5. **Signal Patchstack that the package is being removed**: run `npx @patchstack/connect uninstall` (while the package is still installed and `.patchstackrc.json` still exists). If the site was never claimed, this deletes its anonymous record on Patchstack; if the site is claimed, it is only flagged — the record stays until its owner removes it in the dashboard. A failed signal must not stop the uninstall; continue with the remaining steps.
747
749
  6. **Uninstall the package** with the manager matching the lockfile: `npm uninstall` / `pnpm remove` / `yarn remove` / `bun remove` `@patchstack/connect`. Don't hand-edit `node_modules` or the lockfile.
748
750
  7. **Delete `.patchstackrc.json` and `.patchstackrc.local.json`** (the second holds the API key and is git-ignored, so it is present locally even when the repo shows nothing), remove the `.gitignore` entry setup added for it, and remove `PATCHSTACK_SITE_UUID`, `PATCHSTACK_API_KEY` (and public-prefixed variants like `NEXT_PUBLIC_PATCHSTACK_SITE_UUID`) from env files and CI variables.
749
- 8. **Commit** the changes. Reporting stops immediately. On HTML shells the `window.__PATCHSTACK_PROD__` flag that `mark-build` stamped on production builds lives only in build output — the next build simply won't contain it (rebuild if build output is committed). On JSX roots `scan` wrote the same marker into source; remove that managed `#region patchstack` block (or the hand-pasted equivalent) with the widget tag in step 2.
751
+ 8. **Commit** the changes. Reporting stops immediately. On HTML shells the `window.__PATCHSTACK_PROD__` flag that `mark-build` stamped on production builds lives only in build output — the next build simply won't contain it (rebuild if build output is committed). On JSX roots and Astro layouts `scan` wrote the same marker into source; remove that managed `#region patchstack` block (or the hand-pasted equivalent) with the widget tag in step 2.
750
752
 
751
753
  The `uninstall` signal is the only account-side effect local removal can have: it deletes an *unclaimed* (anonymous) record and merely flags a *claimed* one. A claimed site keeps using a site slot until its owner removes it in the dashboard at https://app.patchstack.com — end your report by telling the user this, alongside the site UUID from step 1. Never attempt to authenticate or remove a claimed site on the user's behalf.
752
754
 
package/README.md CHANGED
@@ -123,8 +123,8 @@ patchstack-connect scan [options] Scan the lockfile and POST to
123
123
  one and Connect persists it. After a
124
124
  successful post, adds/updates the Patchstack
125
125
  Connector tag in the root HTML shell. Also adds the
126
- production marker to a JSX root shell, before the
127
- post (opt out of both with "widget": false in
126
+ production marker to a JSX root or Astro layout,
127
+ before the post (opt out of both with "widget": false in
128
128
  .patchstackrc.json)
129
129
  patchstack-connect setup [options] Run scan, manage the widget, and idempotently
130
130
  install + verify runtime protection and wire
@@ -376,14 +376,14 @@ The Patchstack Connector is a floating control whose form follows the site's cla
376
376
  <script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer data-patchstack-connect-widget="true"></script>
377
377
  ```
378
378
 
379
- - **`scan`** installs the widget tag into a plain HTML shell, and — where there is none — into a JSX root (`src/routes/__root.tsx`, `app/layout.tsx`, …), just before `</body>`. The same tag serves both: JSX reads `defer` as a boolean attribute and passes `data-*` through. A server-rendered app has no HTML shell at all, so without this its published site carries no widget and Patchstack never hears from the live page.
379
+ - **`scan`** installs the widget tag into a plain HTML shell, and — where there is none — into a JSX root (`src/routes/__root.tsx`, `app/layout.tsx`, …) or an Astro layout (`src/layouts/Layout.astro`, `Base.astro`, or the first layout that closes `</body>`), just before `</body>`. The same tag serves both: JSX reads `defer` as a boolean attribute and passes `data-*` through. A server-rendered app has no HTML shell at all, so without this its published site carries no widget and Patchstack never hears from the live page.
380
380
  - **`scan`** also adds the production marker when the root shell is JSX rather than HTML (`src/routes/__root.tsx`, `app/layout.tsx`, …), above the widget tag and guarded by the framework's production expression. A server-rendered app emits no built HTML for `mark-build` to stamp, so without it the widget reads the published site as build mode and shows the claim flow to visitors instead of the report form.
381
381
 
382
- Re-runs update the tag in place (the `data-patchstack-connect-widget` attribute marks it as managed by Connect); a pre-existing manual widget tag is left untouched. `--dry-run` never edits anything; a failed post still skips the widget tag (it needs the site UUID) but the production marker may already have been written, since it runs before the post. Projects whose root layout is code rather than HTML (Next.js, Nuxt, Astro, …) get the exact snippet and target file printed instead — `guide` shows framework-specific placement.
382
+ Re-runs update the tag in place (the `data-patchstack-connect-widget` attribute marks it as managed by Connect); a pre-existing manual widget tag is left untouched. `--dry-run` never edits anything; a failed post still skips the widget tag (it needs the site UUID) but the production marker may already have been written, since it runs before the post. Projects whose root layout is code that Connect does not edit (Nuxt, …) get the exact snippet and target file printed instead — `guide` shows framework-specific placement.
383
383
 
384
384
  - **`mark-build`** ensures the same tag in built HTML output, covering builds whose source shell Connect couldn't edit, and stamps `window.__PATCHSTACK_PROD__` so the widget hides the claim/login UI on the published site (owners reach it by appending `#patchstack` to the live URL). It then reports what it did — `stamped`, `withheld`, `no-pages` for a server-rendered build, or `no-output` — alongside the same manifest `scan` sent before the bundler ran, so the dashboard can say why a published app is or is not reporting its build. That report is the second half of one build, not a second build: Patchstack keeps one copy of the manifest and reads the two together. It is sent only for a site that is already registered, and never carries the site's address or name, which `mark-build` does not resolve. The marker says the page is the live site, so **only a production build carries it**: the environment is read the same way `scan` reads it (the build platform's own tier or branch name, then the hosted builder the project belongs to), and a local or preview build gets the widget tag, no marker, and any marker an earlier build left behind removed. Publishing a static build by hand from your machine is the case that needs `--production` (or `PATCHSTACK_ENVIRONMENT=production`), because nothing in that environment can say the build is a deployment.
385
385
 
386
- - **Opting out:** persist `"widget": false` in `.patchstackrc.json` to disable both the widget tag and the production marker (dependency scanning only). Without it, the next successful scan re-adds the managed tag, and the next scan re-adds the marker on a JSX root.
386
+ - **Opting out:** persist `"widget": false` in `.patchstackrc.json` to disable both the widget tag and the production marker (dependency scanning only). Without it, the next successful scan re-adds the managed tag, and the next scan re-adds the marker on a JSX root or Astro layout.
387
387
 
388
388
  ## Programmatic API
389
389
 
package/dist/cli.js CHANGED
@@ -2985,8 +2985,9 @@ function productionGate(framework) {
2985
2985
  const mapped = framework !== null ? PRODUCTION_GATES[framework] : void 0;
2986
2986
  return mapped ?? DEFAULT_PRODUCTION_GATE;
2987
2987
  }
2988
- function hasJsxShell(framework) {
2989
- return framework !== null && JSX_SHELL_FRAMEWORKS.has(framework);
2988
+ var ASTRO = "astro";
2989
+ function hasEditableShell(framework) {
2990
+ return framework !== null && (JSX_SHELL_FRAMEWORKS.has(framework) || framework === ASTRO);
2990
2991
  }
2991
2992
  function buildSourceMarkerSnippet(framework, checksum = null) {
2992
2993
  const gate = productionGate(framework);
@@ -2994,6 +2995,11 @@ function buildSourceMarkerSnippet(framework, checksum = null) {
2994
2995
  if (checksum !== null && checksum !== "") {
2995
2996
  statements.push(`window.__PATCHSTACK_BUILD__=${JSON.stringify(checksum)};`);
2996
2997
  }
2998
+ if (framework === ASTRO) {
2999
+ return `{${gate} && (
3000
+ <script is:inline ${MARKER_ATTR}="true">${statements.join("")}</script>
3001
+ )}`;
3002
+ }
2997
3003
  return `{${gate} && (
2998
3004
  <script
2999
3005
  ${MARKER_ATTR}="true"
@@ -3005,8 +3011,9 @@ var PROD_MARKER_GLOBAL = "__PATCHSTACK_PROD__";
3005
3011
  var REGION_OPEN = "{/* #region patchstack (managed by patchstack-connect \u2014 do not edit) */}";
3006
3012
  var REGION_CLOSE = "{/* #endregion patchstack */}";
3007
3013
  var REGION_RE = /[ \t]*\{\/\* #region patchstack[\s\S]*?#endregion patchstack \*\/\}\n?/g;
3008
- function findJsxShellAnchor(source, tagName) {
3014
+ function findJsxShellAnchor(source, tagName, from = 0) {
3009
3015
  const candidates = new RegExp(`^([ \\t]*)<${tagName}(?=[\\s/>])`, "gm");
3016
+ candidates.lastIndex = from;
3010
3017
  let candidate;
3011
3018
  while ((candidate = candidates.exec(source)) !== null) {
3012
3019
  let quote = null;
@@ -3060,8 +3067,14 @@ function findJsxShellAnchor(source, tagName) {
3060
3067
  }
3061
3068
  return null;
3062
3069
  }
3070
+ function astroTemplateStart(source) {
3071
+ const open2 = /^\s*---[ \t]*\r?\n/.exec(source);
3072
+ if (open2 === null) return 0;
3073
+ const close = /^---[ \t]*$/m.exec(source.slice(open2[0].length));
3074
+ return close === null ? 0 : open2[0].length + close.index + close[0].length;
3075
+ }
3063
3076
  function ensureMarkerInJsxShell(source, framework, checksum = null) {
3064
- if (!hasJsxShell(framework)) {
3077
+ if (!hasEditableShell(framework)) {
3065
3078
  return { source, action: "unsupported" };
3066
3079
  }
3067
3080
  const stripped = source.replace(REGION_RE, "");
@@ -3069,8 +3082,9 @@ function ensureMarkerInJsxShell(source, framework, checksum = null) {
3069
3082
  return { source: stripped, action: "manual" };
3070
3083
  }
3071
3084
  const block = (indent) => [REGION_OPEN, ...buildSourceMarkerSnippet(framework, checksum).split("\n"), REGION_CLOSE].map((line) => `${indent}${line}`).join("\n");
3085
+ const from = framework === ASTRO ? astroTemplateStart(stripped) : 0;
3072
3086
  for (const tagName of ["head", "body"]) {
3073
- const anchor = findJsxShellAnchor(stripped, tagName);
3087
+ const anchor = findJsxShellAnchor(stripped, tagName, from);
3074
3088
  if (anchor === null) continue;
3075
3089
  const indent = `${anchor.indent} `;
3076
3090
  const remainder = stripped.slice(anchor.end);
@@ -5765,7 +5779,7 @@ var WIDGET_FILE_CANDIDATES = {
5765
5779
  "react-router": ["app/root.tsx", "src/root.tsx"],
5766
5780
  "tanstack-start": ["src/routes/__root.tsx", "app/routes/__root.tsx"],
5767
5781
  sveltekit: ["src/app.html"],
5768
- astro: ["src/layouts/Layout.astro"],
5782
+ astro: ["src/layouts/Layout.astro", "src/layouts/Base.astro", "src/layouts/BaseLayout.astro"],
5769
5783
  gatsby: ["src/html.js"]
5770
5784
  };
5771
5785
  var GENERIC_WIDGET_FILES = ["index.html", "public/index.html"];
@@ -5909,6 +5923,24 @@ function resolveWidgetFileHint(cwd, framework) {
5909
5923
  return candidate;
5910
5924
  }
5911
5925
  }
5926
+ return framework === "astro" ? findAstroDocumentLayout(cwd) : null;
5927
+ }
5928
+ function findAstroDocumentLayout(cwd) {
5929
+ const dir = path12.join("src", "layouts");
5930
+ let names;
5931
+ try {
5932
+ names = readdirSync5(path12.join(cwd, dir)).filter((name) => name.endsWith(".astro")).sort();
5933
+ } catch {
5934
+ return null;
5935
+ }
5936
+ for (const name of names) {
5937
+ try {
5938
+ if (/<\/body>/i.test(readFileSync11(path12.join(cwd, dir, name), "utf8"))) {
5939
+ return path12.posix.join("src", "layouts", name);
5940
+ }
5941
+ } catch {
5942
+ }
5943
+ }
5912
5944
  return null;
5913
5945
  }
5914
5946
  async function collectGuideState(cwd) {
@@ -6095,7 +6127,7 @@ function guideMissing(state, known = {}) {
6095
6127
  if (needsSourceProductionMarker(state) && !state.productionMarkerWired) {
6096
6128
  const gate = productionGate(state.framework);
6097
6129
  missing.push(
6098
- hasJsxShell(state.framework) ? {
6130
+ hasEditableShell(state.framework) ? {
6099
6131
  text: "Your live app does not tell Patchstack it is live yet",
6100
6132
  hint: [`Run: npx @patchstack/connect scan (it edits ${state.widgetFileHint})`],
6101
6133
  detail: [`Or add inside <head>:`, ...buildSourceMarkerSnippet(state.framework).split("\n").map((line) => ` ${line}`)]
@@ -10304,7 +10336,7 @@ function reportSourceWidget(siteUuid, framework, report) {
10304
10336
  });
10305
10337
  };
10306
10338
  try {
10307
- const hint = hasJsxShell(framework) ? resolveWidgetFileHint(process.cwd(), framework) : null;
10339
+ const hint = hasEditableShell(framework) ? resolveWidgetFileHint(process.cwd(), framework) : null;
10308
10340
  const jsxShell = hint !== null && !hint.toLowerCase().endsWith(".html") ? hint : null;
10309
10341
  const result = ensureSourceWidget(process.cwd(), siteUuid, jsxShell);
10310
10342
  switch (result.action) {
@@ -10351,7 +10383,7 @@ function reportSourceMarker(framework, checksum, report) {
10351
10383
  case "unsupported":
10352
10384
  report.missing.push({
10353
10385
  text: "Your live app does not tell Patchstack it is live yet",
10354
- hint: hasJsxShell(framework) ? [`Add this inside <head> in ${shell}:`, ...buildSourceMarkerSnippet(framework).split("\n").map((line) => ` ${line}`)] : [
10386
+ hint: hasEditableShell(framework) ? [`Add this inside <head> in ${shell}:`, ...buildSourceMarkerSnippet(framework).split("\n").map((line) => ` ${line}`)] : [
10355
10387
  `Add this inside <head> in ${shell}, only when ${productionGate(framework)}:`,
10356
10388
  " <script>window.__PATCHSTACK_PROD__=true;</script>"
10357
10389
  ]