@patchstack/connect 0.5.16 → 0.5.18
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 +32 -5
- package/README.md +5 -5
- package/dist/cli.js +41 -9
- package/dist/cli.js.map +1 -1
- package/dist/protect.cjs +4 -1
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.edge.js +4 -1
- package/dist/protect.edge.js.map +1 -1
- package/dist/protect.js +4 -1
- package/dist/protect.js.map +1 -1
- package/package.json +1 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -8,9 +8,15 @@ Use the package setup flow below for an existing JS/Node application. Work in it
|
|
|
8
8
|
|
|
9
9
|
### Plain HTML sites
|
|
10
10
|
|
|
11
|
-
For a standalone site made of HTML, CSS, and browser JavaScript, with no package-managed application or server request handler, use the Patchstack Connector directly. Do not create `package.json`, install a framework, invent build hooks, or add a server just to run Connect. `setup` requires an existing `package.json`; it is not a standalone HTML installer.
|
|
11
|
+
For a standalone site made of HTML, CSS, and browser JavaScript, with no package-managed application or server request handler, use the Patchstack Connector directly. Do not create `package.json`, install a framework, invent build hooks, or add a server just to run Connect, unless the person chooses that in step 1. `setup` requires an existing `package.json`; it is not a standalone HTML installer.
|
|
12
12
|
|
|
13
|
-
1. Use the public site UUID or widget snippet for the correct site in the Patchstack dashboard. An existing `.patchstackrc.json` can also supply `siteUuid`.
|
|
13
|
+
1. Use the public site UUID or widget snippet for the correct site in the Patchstack dashboard. An existing `.patchstackrc.json` can also supply `siteUuid`. Never invent a UUID or use a claim token or API key as the widget identifier. If neither is available, stop before editing the page and give the person these options in plain words, then wait for their choice:
|
|
14
|
+
|
|
15
|
+
- **Add the widget with a site from the dashboard.** They create an "Application" site in the Patchstack dashboard and paste its site ID or widget snippet. You add the tag in step 2. Nothing else is added to their folder. They get the widget only.
|
|
16
|
+
- **Create a Node project in the folder and let Connect set it up.** No trip to the dashboard first: `setup` creates the site and prints the link that connects it to their account. It adds `package.json`, a lockfile and `node_modules` to their folder. Packages they add later are checked for known security problems. A page with no packages of its own has little to check at first, and there is still no runtime protection without a server. Follow "Creating a Node project for a plain HTML site" below.
|
|
17
|
+
- **Stop here.** Nothing is changed.
|
|
18
|
+
|
|
19
|
+
Do not pick for them, and do not create the Node project unless they choose it.
|
|
14
20
|
2. Add one widget tag before `</body>` in the page or shared layout. Preserve an existing correct tag. For a page published directly without a build step, disable the widget's build-mode onboarding with `data-build-mode="false"`:
|
|
15
21
|
|
|
16
22
|
```html
|
|
@@ -22,6 +28,25 @@ For a standalone site made of HTML, CSS, and browser JavaScript, with no package
|
|
|
22
28
|
|
|
23
29
|
Report this as **Patchstack Connector installed**, with any remaining preview or publishing step. This path does not inventory local JavaScript files or scripts loaded from a CDN, scan npm dependencies, or install runtime exploit protection. External APIs used by the page require their own server-side integration.
|
|
24
30
|
|
|
31
|
+
#### Creating a Node project for a plain HTML site
|
|
32
|
+
|
|
33
|
+
Only when the person chose this option in step 1 above. Work in the folder that holds the site's pages.
|
|
34
|
+
|
|
35
|
+
1. Create the project and install Connect:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
npm init -y
|
|
39
|
+
npm install --save @patchstack/connect
|
|
40
|
+
npx @patchstack/connect setup
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
In a hosted builder, scope `PATCHSTACK_ENVIRONMENT=sandbox` to the `setup` command, as in "Automated setup". `setup` creates the site, writes its `siteUuid` to `.patchstackrc.json`, adds `"postinstall": "patchstack-connect scan"` to `package.json`, and prints the dashboard link. With no `build` script it adds no build steps.
|
|
44
|
+
2. Put the widget on the pages. `setup` adds the tag only to `index.html`, `public/index.html` or `src/app.html`. For any other page it lists the widget under `Missing` and prints the tag to add. Add one tag before `</body>` on each page, or in the shared layout. Because the pages are published without a build step, the tag needs `data-build-mode="false"`, as in step 2 above. If `setup` already added the tag to `index.html`, add that attribute to it. Later scans keep the attribute: they rewrite the tag only when the site ID changes.
|
|
45
|
+
3. Two `✘` lines are expected and need no fix:
|
|
46
|
+
- `Runtime protection: no server file found`. A plain HTML site has no server to guard. Do not add one. `setup` leaves a generic guard in `patchstack/`, which nothing loads until a server does.
|
|
47
|
+
- `Deploy project to protect live app`. Publishing the pages is the person's step.
|
|
48
|
+
4. End as in "The message you end on", with the dashboard link from the `Next:` line. Say that the widget, and a check of the packages the site installs, are active, and that runtime protection is not. Remind them to publish the pages when ready, and that `node_modules` is not part of the site. Do not publish anything yourself.
|
|
49
|
+
|
|
25
50
|
### JS/Node applications — the usual path
|
|
26
51
|
|
|
27
52
|
1. Check what is already done with `npx @patchstack/connect guide` (read-only). If the project is already provisioned, reuse it — see "Before you start — never install twice".
|
|
@@ -240,7 +265,9 @@ Handle it in this order:
|
|
|
240
265
|
|
|
241
266
|
**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
267
|
|
|
243
|
-
|
|
268
|
+
**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.
|
|
269
|
+
|
|
270
|
+
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
271
|
|
|
245
272
|
```html
|
|
246
273
|
<script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer></script>
|
|
@@ -304,7 +331,7 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
|
|
|
304
331
|
and `mark-build` only read and write files; `scan` and `mark-build` also report the dependency
|
|
305
332
|
manifest they read.
|
|
306
333
|
|
|
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.
|
|
334
|
+
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
335
|
|
|
309
336
|
**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
337
|
|
|
@@ -746,7 +773,7 @@ Remove only the pieces that are actually present — check for each first. If no
|
|
|
746
773
|
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
774
|
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
775
|
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.
|
|
776
|
+
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
777
|
|
|
751
778
|
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
779
|
|
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
|
|
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
|
|
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
|
-
|
|
2989
|
-
|
|
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 (!
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
]
|