@forgecart/cli 2.202606151453.0 → 2.202607061936.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/dist/src/commands/init.d.ts +8 -0
- package/dist/src/commands/init.js +48 -6
- package/dist/src/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/templates/storefront/next.config.js +20 -0
- package/templates/storefront/package.json +1 -1
- package/templates/storefront/src/app/__forge_beacon/route.ts +36 -0
- package/templates/storefront/src/app/layout.tsx +2 -0
- package/templates/storefront/src/app/ping/route.ts +22 -0
- package/templates/storefront/src/components/ForgeErrorBeacon.tsx +71 -0
- package/templates/storefront/src/instrumentation.ts +69 -0
|
@@ -26,6 +26,14 @@ export interface InitOptions {
|
|
|
26
26
|
apiUrl: string;
|
|
27
27
|
/** Whether to also scaffold a NestJS application (planned for a later phase). */
|
|
28
28
|
application?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Serve-in-place mode: the target directory is ALREADY a scaffolded,
|
|
31
|
+
* dependency- and dev-compile-warmed storefront (materialized into the pod's
|
|
32
|
+
* `/workspace` from the image's pre-warmed tree). Write ONLY the per-channel
|
|
33
|
+
* config + `.env.local` and skip the template copy entirely — copying would
|
|
34
|
+
* clobber the warm `.next` and bust the Turbopack dev cache.
|
|
35
|
+
*/
|
|
36
|
+
configOnly?: boolean;
|
|
29
37
|
}
|
|
30
38
|
/**
|
|
31
39
|
* Run `forgecart init`.
|
|
@@ -42,6 +42,16 @@ async function isDirectory(path) {
|
|
|
42
42
|
return false;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
/** True if the path exists and is a regular file. */
|
|
46
|
+
async function isFile(path) {
|
|
47
|
+
try {
|
|
48
|
+
const info = await stat(path);
|
|
49
|
+
return info.isFile();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
45
55
|
/**
|
|
46
56
|
* Read a `package.json` and normalize it for content comparison. Returns null
|
|
47
57
|
* when the file is missing or not valid JSON — callers treat that as "manifests
|
|
@@ -121,6 +131,19 @@ function buildEnvLocal(channelToken, apiUrl) {
|
|
|
121
131
|
'',
|
|
122
132
|
].join('\n');
|
|
123
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Write the per-channel config the storefront reads at runtime: `.forgecart/
|
|
136
|
+
* config.json` (channel token, api url, app flag) and `.env.local` (the SDK
|
|
137
|
+
* client env). Shared by the full scaffold and the `--config-only` serve-in-place
|
|
138
|
+
* path, so both write byte-identical config — the only difference is whether the
|
|
139
|
+
* template tree was copied first.
|
|
140
|
+
*/
|
|
141
|
+
async function writeChannelConfig(targetDir, channelToken, options) {
|
|
142
|
+
const configDir = join(targetDir, '.forgecart');
|
|
143
|
+
await mkdir(configDir, { recursive: true });
|
|
144
|
+
await writeFile(join(configDir, 'config.json'), `${JSON.stringify(buildConfig(channelToken, options), null, 2)}\n`, 'utf8');
|
|
145
|
+
await writeFile(join(targetDir, '.env.local'), buildEnvLocal(channelToken, options.apiUrl), 'utf8');
|
|
146
|
+
}
|
|
124
147
|
/** Print the post-scaffold next steps. */
|
|
125
148
|
function printNextSteps(targetDir, application, depsInstalled) {
|
|
126
149
|
print('');
|
|
@@ -141,6 +164,16 @@ function printNextSteps(targetDir, application, depsInstalled) {
|
|
|
141
164
|
print('');
|
|
142
165
|
}
|
|
143
166
|
}
|
|
167
|
+
/** Print the post-scaffold summary for the `--config-only` serve-in-place path. */
|
|
168
|
+
function printConfigOnly(targetDir) {
|
|
169
|
+
print('');
|
|
170
|
+
print('Storefront configured (pre-warmed: dependencies and dev cache already in place).');
|
|
171
|
+
print('');
|
|
172
|
+
print('Next steps:');
|
|
173
|
+
print(` cd ${targetDir}`);
|
|
174
|
+
print(' npm run dev');
|
|
175
|
+
print('');
|
|
176
|
+
}
|
|
144
177
|
/**
|
|
145
178
|
* Run `forgecart init`.
|
|
146
179
|
*
|
|
@@ -156,6 +189,18 @@ export async function runInit(name, options) {
|
|
|
156
189
|
throw new Error('A channel token is required. Pass it with --token <token> (or -t <token>).');
|
|
157
190
|
}
|
|
158
191
|
const targetDir = name ? resolve(process.cwd(), name) : process.cwd();
|
|
192
|
+
// Serve-in-place: the target is already a warm storefront materialized from the
|
|
193
|
+
// image's pre-warmed tree (node_modules + a warm `.next`). Write only the
|
|
194
|
+
// per-channel config + env and return — copying the template would clobber the
|
|
195
|
+
// pre-warmed `.next` and bust the Turbopack dev cache the bake exists to provide.
|
|
196
|
+
if (options.configOnly) {
|
|
197
|
+
if (!(await isFile(join(targetDir, 'package.json')))) {
|
|
198
|
+
throw new Error(`--config-only requires an existing storefront at ${targetDir} (no package.json found).`);
|
|
199
|
+
}
|
|
200
|
+
await writeChannelConfig(targetDir, channelToken, options);
|
|
201
|
+
printConfigOnly(targetDir);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
159
204
|
const templateDir = getStorefrontTemplateDir();
|
|
160
205
|
if (!(await isDirectory(templateDir))) {
|
|
161
206
|
throw new Error(`Storefront template not found at ${templateDir}.`);
|
|
@@ -168,13 +213,9 @@ export async function runInit(name, options) {
|
|
|
168
213
|
recursive: true,
|
|
169
214
|
});
|
|
170
215
|
}
|
|
171
|
-
|
|
172
|
-
await mkdir(configDir, { recursive: true });
|
|
173
|
-
const config = buildConfig(channelToken, options);
|
|
174
|
-
await writeFile(join(configDir, 'config.json'), `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
175
|
-
await writeFile(join(targetDir, '.env.local'), buildEnvLocal(channelToken, options.apiUrl), 'utf8');
|
|
216
|
+
await writeChannelConfig(targetDir, channelToken, options);
|
|
176
217
|
await writeDepsReadyMarker(targetDir, plan);
|
|
177
|
-
printNextSteps(name ?? '.',
|
|
218
|
+
printNextSteps(name ?? '.', options.application ?? false, plan.source === 'warmed');
|
|
178
219
|
}
|
|
179
220
|
/** Build the `init [name]` command. */
|
|
180
221
|
export function createInitCommand() {
|
|
@@ -184,6 +225,7 @@ export function createInitCommand() {
|
|
|
184
225
|
.option('-t, --token <token>', 'ForgeCart channel token (required)')
|
|
185
226
|
.option('--api-url <url>', 'Shop API base URL written to the storefront environment', DEFAULT_SHOP_API_URL)
|
|
186
227
|
.option('-a, --application', 'Also scaffold a NestJS application (TODO: next phase)')
|
|
228
|
+
.option('--config-only', 'Write only per-channel config into an existing pre-warmed storefront (skip the template copy)')
|
|
187
229
|
.action(async (name, options) => {
|
|
188
230
|
await runInit(name, options);
|
|
189
231
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAErC,0DAA0D;AAC1D,MAAM,CAAC,MAAM,oBAAoB,GAAG,2BAA2B,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,wBAAwB,CAAC;AAEnE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -4,6 +4,26 @@ const nextConfig = {
|
|
|
4
4
|
// storefront with `next start` without a full node_modules tree.
|
|
5
5
|
output: 'standalone',
|
|
6
6
|
reactStrictMode: true,
|
|
7
|
+
// The visual-editor preview runs THIS dev server inside a workspace pod and is
|
|
8
|
+
// reached cross-origin through the channel's preview ingress host
|
|
9
|
+
// (`<code>.127.0.0.1.nip.io:8081` locally; `<code>.dev|test.forgecart.dev` and
|
|
10
|
+
// `<code>.forgecart.dev` deployed) — never the pod's own `localhost:3000`. Next
|
|
11
|
+
// 16 ENFORCES `allowedDevOrigins`: a dev request whose host is not
|
|
12
|
+
// localhost/127.0.0.1 and not listed here is blocked, which silently kills the
|
|
13
|
+
// `/_next` HMR websocket (and the dashboard iframe's dev channel) with no page
|
|
14
|
+
// error. The `*` wildcard matches across dots, so `*.forgecart.dev` covers every
|
|
15
|
+
// env's preview subdomain. Dev-only — `next start` (deployed storefronts) ignores it.
|
|
16
|
+
allowedDevOrigins: ['*.127.0.0.1.nip.io', '*.dev.forgecart.dev', '*.forgecart.dev'],
|
|
17
|
+
experimental: {
|
|
18
|
+
// Persist Turbopack's dev compile artifacts to `.next` so a pod can serve
|
|
19
|
+
// a PRE-WARMED `.next` baked at build time instead of paying a cold first
|
|
20
|
+
// compile. The cache is path-keyed, so it is only valid when warmed at the
|
|
21
|
+
// SAME absolute path the pod serves from (/workspace) — see the workspace-pod
|
|
22
|
+
// Dockerfile `storefront-prewarm` stage and the STOREFRONT materialization
|
|
23
|
+
// initContainer (serve-in-place; never the /opt→/workspace copy). Default-on
|
|
24
|
+
// in Next 16.1+; set explicitly to document the contract.
|
|
25
|
+
turbopackFileSystemCacheForDev: true,
|
|
26
|
+
},
|
|
7
27
|
// Dev-only `data-fc-source` stamping for the ForgeCart visual editor. The
|
|
8
28
|
// loader marks host JSX elements with their source file:line:col so the
|
|
9
29
|
// dashboard can map a sprayed region back to code. It runs under Turbopack
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEV-ONLY same-origin error-beacon forwarder.
|
|
3
|
+
*
|
|
4
|
+
* The storefront's client beacon (`ForgeErrorBeacon`) POSTs a browser-surfaced
|
|
5
|
+
* runtime error here — same-origin, so the browser never learns the pod-internal
|
|
6
|
+
* receiver address. This handler forwards the body SERVER-side to the in-pod
|
|
7
|
+
* workspace-manager receiver at `http://127.0.0.1:<FORGE_BEACON_PORT>/__forge_beacon`,
|
|
8
|
+
* which folds it into the supervisor's runtime state so the shop's recovery brain
|
|
9
|
+
* can originate a fix for a crash the dev-server's stderr scan cannot see.
|
|
10
|
+
*
|
|
11
|
+
* Gated entirely on `NODE_ENV === 'development'`: a deployed `next start` storefront
|
|
12
|
+
* answers 404 here and forwards nothing — the whole beacon path ships only in the
|
|
13
|
+
* in-pod dev-server. `force-dynamic` keeps it out of the static prerender so it
|
|
14
|
+
* behaves identically under `next dev` and a dev build.
|
|
15
|
+
*/
|
|
16
|
+
export const dynamic = 'force-dynamic';
|
|
17
|
+
|
|
18
|
+
const BEACON_PORT = process.env.FORGE_BEACON_PORT ?? '3002';
|
|
19
|
+
|
|
20
|
+
export async function POST(request: Request): Promise<Response> {
|
|
21
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
22
|
+
return new Response(null, { status: 404 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const body = await request.text();
|
|
26
|
+
// Forward server-side to the loopback receiver. A delivery failure is swallowed —
|
|
27
|
+
// the beacon is a best-effort backstop, never a hard dependency of the page — but
|
|
28
|
+
// the client still gets a clean 204 so it never retries against a flapping pod.
|
|
29
|
+
await fetch(`http://127.0.0.1:${BEACON_PORT}/__forge_beacon`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'content-type': 'application/json' },
|
|
32
|
+
body,
|
|
33
|
+
}).catch(() => undefined);
|
|
34
|
+
|
|
35
|
+
return new Response(null, { status: 204 });
|
|
36
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
+
import { ForgeErrorBeacon } from '../components/ForgeErrorBeacon';
|
|
4
5
|
import { ForgecartDesigner } from '../components/ForgecartDesigner';
|
|
5
6
|
import { Header } from '../components/Header';
|
|
6
7
|
import { getCart } from '../lib/cart-actions';
|
|
@@ -28,6 +29,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
|
|
28
29
|
</footer>
|
|
29
30
|
</CartProvider>
|
|
30
31
|
<ForgecartDesigner />
|
|
32
|
+
<ForgeErrorBeacon />
|
|
31
33
|
</body>
|
|
32
34
|
</html>
|
|
33
35
|
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Liveness ping for the in-pod dev-server readiness probe.
|
|
3
|
+
*
|
|
4
|
+
* Returns a static 200 with no layout, data fetch, or session. The workspace's
|
|
5
|
+
* readiness probe hits THIS route (not `/`) on a fast cadence while the store is
|
|
6
|
+
* STARTING and then a slow liveness cadence forever — and `/` renders the
|
|
7
|
+
* cart-aware layout, whose `activeOrder` call MATERIALISES an active order on every
|
|
8
|
+
* request. Probing `/` therefore minted a throwaway order on every poll; probing a
|
|
9
|
+
* route handler renders nothing and touches no session, so readiness polling stays
|
|
10
|
+
* side-effect-free.
|
|
11
|
+
*
|
|
12
|
+
* `force-dynamic` keeps it out of the static prerender so a production build serves
|
|
13
|
+
* it on demand exactly like `next dev` does — the probe always gets a fresh 200.
|
|
14
|
+
*/
|
|
15
|
+
export const dynamic = 'force-dynamic';
|
|
16
|
+
|
|
17
|
+
export function GET(): Response {
|
|
18
|
+
return new Response('ok', {
|
|
19
|
+
status: 200,
|
|
20
|
+
headers: { 'content-type': 'text/plain' },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DEV-ONLY storefront error beacon. Renders nothing.
|
|
7
|
+
*
|
|
8
|
+
* Registers `window.onerror` + `window.onunhandledrejection` and POSTs the failure
|
|
9
|
+
* detail to the same-origin Next API route `/__forge_beacon`, which forwards it
|
|
10
|
+
* server-side to the in-pod workspace-manager receiver. This surfaces the crash
|
|
11
|
+
* class the pod's dev-server stderr scan CANNOT see: a client-component runtime
|
|
12
|
+
* error or a hydration/SSR-boundary throw that never reaches the dev-server's
|
|
13
|
+
* output. The browser only ever talks SAME-ORIGIN — the API route is the only thing
|
|
14
|
+
* that knows the pod-internal receiver address.
|
|
15
|
+
*
|
|
16
|
+
* Production builds ship zero bytes of this: the literal `NODE_ENV` check is inlined
|
|
17
|
+
* by the bundler, so the effect body is dead-code-eliminated from `next build` /
|
|
18
|
+
* `next start` output — a deployed storefront registers no global error handlers and
|
|
19
|
+
* has no beacon API route reachable in prod (the route itself is dev-gated too).
|
|
20
|
+
*
|
|
21
|
+
* The beacon is best-effort and never blocks the page: the POST is fire-and-forget
|
|
22
|
+
* with `keepalive` so it survives an unload, and a delivery failure is swallowed —
|
|
23
|
+
* the page's own error boundary still renders. The full
|
|
24
|
+
* browser → API route → pod → supervisor-fold path is exercised at the e2e layer,
|
|
25
|
+
* not in this template.
|
|
26
|
+
*/
|
|
27
|
+
export function ForgeErrorBeacon() {
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const send = (message: string, stack?: string) => {
|
|
34
|
+
const body = JSON.stringify({
|
|
35
|
+
message,
|
|
36
|
+
stack,
|
|
37
|
+
route: window.location.pathname,
|
|
38
|
+
source: 'browser',
|
|
39
|
+
});
|
|
40
|
+
// Same-origin POST; keepalive lets it survive a navigation/unload. A failed
|
|
41
|
+
// delivery is intentionally swallowed — the beacon is a backstop signal, not
|
|
42
|
+
// a hard dependency of the page.
|
|
43
|
+
void fetch('/__forge_beacon', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'content-type': 'application/json' },
|
|
46
|
+
body,
|
|
47
|
+
keepalive: true,
|
|
48
|
+
}).catch(() => undefined);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const onError = (event: ErrorEvent) => {
|
|
52
|
+
send(event.message || 'window.onerror', event.error?.stack);
|
|
53
|
+
};
|
|
54
|
+
const onRejection = (event: PromiseRejectionEvent) => {
|
|
55
|
+
const reason = event.reason;
|
|
56
|
+
const message =
|
|
57
|
+
reason instanceof Error ? reason.message : String(reason ?? 'unhandledrejection');
|
|
58
|
+
const stack = reason instanceof Error ? reason.stack : undefined;
|
|
59
|
+
send(message, stack);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
window.addEventListener('error', onError);
|
|
63
|
+
window.addEventListener('unhandledrejection', onRejection);
|
|
64
|
+
return () => {
|
|
65
|
+
window.removeEventListener('error', onError);
|
|
66
|
+
window.removeEventListener('unhandledrejection', onRejection);
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEV-ONLY server-error beacon via Next's `onRequestError` instrumentation hook.
|
|
3
|
+
*
|
|
4
|
+
* This is the PRIMARY server-side crash-detection surface for the in-pod dev-server:
|
|
5
|
+
* when a Server Component, a server action, or a route handler THROWS while rendering
|
|
6
|
+
* a request under `next dev`, Next invokes `onRequestError` with the structured error
|
|
7
|
+
* + request + routing context. The hook POSTs that detail SERVER-side, straight to the
|
|
8
|
+
* in-pod workspace-manager loopback receiver at
|
|
9
|
+
* `http://127.0.0.1:<FORGE_BEACON_PORT>/__forge_beacon`, which folds it into the
|
|
10
|
+
* supervisor's runtime state so the shop's recovery brain can originate a fix for a
|
|
11
|
+
* crash the `/ping` liveness probe cannot see (the dev-server process stays up and
|
|
12
|
+
* `/ping` stays 200, but `/` 500s per request).
|
|
13
|
+
*
|
|
14
|
+
* This complements the browser beacon (`ForgeErrorBeacon`): that one catches
|
|
15
|
+
* CLIENT-component / hydration throws that surface only in the browser; this one
|
|
16
|
+
* catches SERVER render throws that surface only in the dev-server. Both POST the
|
|
17
|
+
* same `/__forge_beacon` shape — the only difference is `source` (`'browser'` vs
|
|
18
|
+
* `'server'`).
|
|
19
|
+
*
|
|
20
|
+
* Gated entirely on `NODE_ENV === 'development'`: a deployed `next start` storefront
|
|
21
|
+
* registers no beacon (the literal check is inlined by the bundler, so the body is
|
|
22
|
+
* dead-code-eliminated from a production build) and the loopback receiver does not
|
|
23
|
+
* exist outside the pod. Next compiles `instrumentation.ts` for BOTH the Node and Edge
|
|
24
|
+
* runtimes; the loopback POST is a Node-only concern, so the hook also gates on the
|
|
25
|
+
* Node runtime — the Edge bundle then carries an inert no-op.
|
|
26
|
+
*
|
|
27
|
+
* The hook NEVER throws out of itself: a failed delivery is swallowed (the beacon is a
|
|
28
|
+
* best-effort backstop, never a hard dependency of the request), so a flapping receiver
|
|
29
|
+
* can never mask or replace the underlying request error Next is already reporting.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const BEACON_PORT = process.env.FORGE_BEACON_PORT ?? '3002';
|
|
33
|
+
|
|
34
|
+
interface OnRequestErrorRequest {
|
|
35
|
+
path: string;
|
|
36
|
+
method: string;
|
|
37
|
+
headers: { [key: string]: string | string[] | undefined };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface OnRequestErrorContext {
|
|
41
|
+
routerKind: string;
|
|
42
|
+
routePath: string;
|
|
43
|
+
routeType: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function onRequestError(
|
|
47
|
+
error: unknown,
|
|
48
|
+
request: OnRequestErrorRequest,
|
|
49
|
+
context: OnRequestErrorContext,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
if (process.env.NODE_ENV !== 'development' || process.env.NEXT_RUNTIME !== 'nodejs') {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const err = error as { message?: string; digest?: string } | undefined;
|
|
56
|
+
const body = JSON.stringify({
|
|
57
|
+
message: typeof err?.message === 'string' ? err.message : String(error),
|
|
58
|
+
digest: typeof err?.digest === 'string' ? err.digest : undefined,
|
|
59
|
+
route: request.path,
|
|
60
|
+
routeType: context.routeType,
|
|
61
|
+
source: 'server',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await fetch(`http://127.0.0.1:${BEACON_PORT}/__forge_beacon`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'content-type': 'application/json' },
|
|
67
|
+
body,
|
|
68
|
+
}).catch(() => undefined);
|
|
69
|
+
}
|