@kenkaiiii/gg-pixel 4.3.72 → 4.3.74
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/cli.js +307 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +299 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +307 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -461,17 +461,25 @@ async function install(opts = {}) {
|
|
|
461
461
|
const home = opts.homeDir ?? (0, import_node_os2.homedir)();
|
|
462
462
|
const nodeRoot = findProjectRoot(cwd);
|
|
463
463
|
const pythonRoot = findPythonProjectRoot(cwd);
|
|
464
|
-
|
|
464
|
+
const goRoot = findGoProjectRoot(cwd);
|
|
465
|
+
const rubyRoot = findRubyProjectRoot(cwd);
|
|
466
|
+
if (!nodeRoot && !pythonRoot && !goRoot && !rubyRoot) {
|
|
465
467
|
throw new Error(
|
|
466
|
-
`No project found at ${cwd}: looked for package.json
|
|
468
|
+
`No project found at ${cwd}: looked for package.json, pyproject.toml/setup.py/requirements.txt/Pipfile, go.mod, Gemfile/*.gemspec.`
|
|
467
469
|
);
|
|
468
470
|
}
|
|
469
|
-
const
|
|
470
|
-
if (
|
|
471
|
+
const closestRoot = pickClosestRoot([nodeRoot, pythonRoot, goRoot, rubyRoot]);
|
|
472
|
+
if (closestRoot === goRoot && goRoot) {
|
|
473
|
+
return installGo({ projectRoot: goRoot, opts, ingestUrl, fetchFn, home });
|
|
474
|
+
}
|
|
475
|
+
if (closestRoot === rubyRoot && rubyRoot) {
|
|
476
|
+
return installRuby({ projectRoot: rubyRoot, opts, ingestUrl, fetchFn, home });
|
|
477
|
+
}
|
|
478
|
+
if (closestRoot === pythonRoot && pythonRoot) {
|
|
471
479
|
return installPython({ projectRoot: pythonRoot, opts, ingestUrl, fetchFn, home });
|
|
472
480
|
}
|
|
473
481
|
if (!nodeRoot) {
|
|
474
|
-
throw new Error("Internal:
|
|
482
|
+
throw new Error("Internal: closest root is Node but nodeRoot is null");
|
|
475
483
|
}
|
|
476
484
|
const pkgPath = (0, import_node_path2.join)(nodeRoot, "package.json");
|
|
477
485
|
const pkg = JSON.parse((0, import_node_fs3.readFileSync)(pkgPath, "utf8"));
|
|
@@ -707,6 +715,9 @@ function isCommonJsEntry(entryPath, pkg) {
|
|
|
707
715
|
}
|
|
708
716
|
function detectJsProjectKind(pkg, projectRoot) {
|
|
709
717
|
const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
718
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(projectRoot, "wrangler.toml")) || (0, import_node_fs3.existsSync)((0, import_node_path2.join)(projectRoot, "wrangler.jsonc")) || (0, import_node_fs3.existsSync)((0, import_node_path2.join)(projectRoot, "wrangler.json"))) {
|
|
719
|
+
return "cloudflare-workers";
|
|
720
|
+
}
|
|
710
721
|
if ("electron" in all) return "electron";
|
|
711
722
|
if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(projectRoot, "src-tauri")) || "@tauri-apps/api" in all) return "tauri";
|
|
712
723
|
if ("react-native" in all) return "react-native";
|
|
@@ -737,8 +748,12 @@ function wireFramework(w) {
|
|
|
737
748
|
return wireTauri(w);
|
|
738
749
|
case "react-native":
|
|
739
750
|
return wireReactNative(w);
|
|
751
|
+
case "cloudflare-workers":
|
|
752
|
+
return wireWorkers(w);
|
|
740
753
|
case "python":
|
|
741
|
-
|
|
754
|
+
case "go":
|
|
755
|
+
case "ruby":
|
|
756
|
+
throw new Error(`Internal: ${w.kind} should have been handled earlier`);
|
|
742
757
|
}
|
|
743
758
|
}
|
|
744
759
|
function wireNode({ projectRoot, pkg, ingestUrl }) {
|
|
@@ -764,6 +779,7 @@ function wireNextjs({ projectRoot, projectKey, ingestUrl }) {
|
|
|
764
779
|
const serverInitPath = pickPath(projectRoot, ["instrumentation.ts", "instrumentation.js"]);
|
|
765
780
|
const finalServerPath = serverInitPath ?? (0, import_node_path2.join)(projectRoot, "instrumentation.ts");
|
|
766
781
|
writeNextInstrumentation(finalServerPath, ingestUrl);
|
|
782
|
+
patchNextConfig(projectRoot);
|
|
767
783
|
const clientInitPath = (0, import_node_path2.join)(projectRoot, "gg-pixel.client.mjs");
|
|
768
784
|
(0, import_node_fs3.writeFileSync)(clientInitPath, renderBrowserInitFile(ingestUrl, projectKey), "utf8");
|
|
769
785
|
const layoutPath = findNextLayout(projectRoot);
|
|
@@ -836,6 +852,56 @@ function findNextLayout(projectRoot) {
|
|
|
836
852
|
}
|
|
837
853
|
return null;
|
|
838
854
|
}
|
|
855
|
+
function patchNextConfig(projectRoot) {
|
|
856
|
+
const candidates = ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.cjs"];
|
|
857
|
+
let configPath = null;
|
|
858
|
+
for (const c of candidates) {
|
|
859
|
+
const p = (0, import_node_path2.join)(projectRoot, c);
|
|
860
|
+
if ((0, import_node_fs3.existsSync)(p)) {
|
|
861
|
+
configPath = p;
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (!configPath) {
|
|
866
|
+
configPath = (0, import_node_path2.join)(projectRoot, "next.config.ts");
|
|
867
|
+
(0, import_node_fs3.writeFileSync)(
|
|
868
|
+
configPath,
|
|
869
|
+
`import type { NextConfig } from "next";
|
|
870
|
+
|
|
871
|
+
const nextConfig: NextConfig = {
|
|
872
|
+
// Keeps Next's bundler from trying to compile better-sqlite3 (native dep).
|
|
873
|
+
serverExternalPackages: ["@kenkaiiii/gg-pixel"],
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
export default nextConfig;
|
|
877
|
+
`,
|
|
878
|
+
"utf8"
|
|
879
|
+
);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const content = (0, import_node_fs3.readFileSync)(configPath, "utf8");
|
|
883
|
+
if (content.includes("@kenkaiiii/gg-pixel")) return;
|
|
884
|
+
if (content.includes("serverExternalPackages")) {
|
|
885
|
+
const updated = content.replace(
|
|
886
|
+
/serverExternalPackages\s*:\s*\[([^\]]*)\]/,
|
|
887
|
+
(_match, inside) => {
|
|
888
|
+
const trimmed = inside.trim();
|
|
889
|
+
const sep2 = trimmed.length > 0 ? ", " : "";
|
|
890
|
+
return `serverExternalPackages: [${trimmed}${sep2}"@kenkaiiii/gg-pixel"]`;
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
if (updated !== content) (0, import_node_fs3.writeFileSync)(configPath, updated, "utf8");
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const objStart = /(const\s+\w+\s*:\s*NextConfig\s*=\s*\{|module\.exports\s*=\s*\{|export\s+default\s*\{)/;
|
|
897
|
+
const m = objStart.exec(content);
|
|
898
|
+
if (m) {
|
|
899
|
+
const insertAt = m.index + m[0].length;
|
|
900
|
+
const updated = content.slice(0, insertAt) + `
|
|
901
|
+
serverExternalPackages: ["@kenkaiiii/gg-pixel"],` + content.slice(insertAt);
|
|
902
|
+
(0, import_node_fs3.writeFileSync)(configPath, updated, "utf8");
|
|
903
|
+
}
|
|
904
|
+
}
|
|
839
905
|
function wireSveltekit({ projectRoot, projectKey, ingestUrl }) {
|
|
840
906
|
const serverPath = (0, import_node_path2.join)(projectRoot, "src/hooks.server.ts");
|
|
841
907
|
const clientPath = (0, import_node_path2.join)(projectRoot, "src/hooks.client.ts");
|
|
@@ -997,6 +1063,48 @@ function wireTauri({ projectRoot, pkg, projectKey, ingestUrl }) {
|
|
|
997
1063
|
]
|
|
998
1064
|
};
|
|
999
1065
|
}
|
|
1066
|
+
function wireWorkers({ projectRoot, projectKey, ingestUrl }) {
|
|
1067
|
+
const initPath = (0, import_node_path2.join)(projectRoot, "gg-pixel.workers.snippet.ts");
|
|
1068
|
+
(0, import_node_fs3.writeFileSync)(
|
|
1069
|
+
initPath,
|
|
1070
|
+
`// gg-pixel \u2014 Cloudflare Workers wiring snippet.
|
|
1071
|
+
// Auto-generated by ggcoder pixel install. Wrap your default export with
|
|
1072
|
+
// withPixel(...) so any throw in your handler is auto-reported. Example:
|
|
1073
|
+
//
|
|
1074
|
+
// import { withPixel } from "@kenkaiiii/gg-pixel/workers";
|
|
1075
|
+
//
|
|
1076
|
+
// export default withPixel(
|
|
1077
|
+
// { projectKey: ${JSON.stringify(projectKey)} },
|
|
1078
|
+
// {
|
|
1079
|
+
// async fetch(req, env, ctx) { /* your code */ },
|
|
1080
|
+
// async scheduled(evt, env, ctx) { /* your code */ },
|
|
1081
|
+
// },
|
|
1082
|
+
// );
|
|
1083
|
+
//
|
|
1084
|
+
// For manual reports inside a handler:
|
|
1085
|
+
//
|
|
1086
|
+
// import { reportPixel } from "@kenkaiiii/gg-pixel/workers";
|
|
1087
|
+
// reportPixel(ctx, { projectKey: ${JSON.stringify(projectKey)} }, {
|
|
1088
|
+
// message: "user clicked the broken button",
|
|
1089
|
+
// });
|
|
1090
|
+
//
|
|
1091
|
+
// Your project_key is publishable \u2014 safe to commit.
|
|
1092
|
+
|
|
1093
|
+
import { withPixel, reportPixel } from "@kenkaiiii/gg-pixel/workers";
|
|
1094
|
+
export const PIXEL_KEY = ${JSON.stringify(projectKey)};
|
|
1095
|
+
export const PIXEL_INGEST = ${JSON.stringify(ingestUrl)};
|
|
1096
|
+
export { withPixel, reportPixel };
|
|
1097
|
+
`,
|
|
1098
|
+
"utf8"
|
|
1099
|
+
);
|
|
1100
|
+
return {
|
|
1101
|
+
primaryInitPath: initPath,
|
|
1102
|
+
entryWiring: { kind: "no_entry_found" },
|
|
1103
|
+
warnings: [
|
|
1104
|
+
`Cloudflare Workers default exports can't be auto-wrapped safely. Open ${initPath} for a 3-line snippet you can paste into your worker.`
|
|
1105
|
+
]
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1000
1108
|
function wireReactNative({ projectRoot }) {
|
|
1001
1109
|
return {
|
|
1002
1110
|
primaryInitPath: (0, import_node_path2.join)(projectRoot, "(not-installed)"),
|
|
@@ -1102,10 +1210,191 @@ function detectPythonPackageManager(projectRoot) {
|
|
|
1102
1210
|
if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(projectRoot, "Pipfile.lock"))) return "pipenv";
|
|
1103
1211
|
return "pip";
|
|
1104
1212
|
}
|
|
1105
|
-
function
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1213
|
+
function pickClosestRoot(roots) {
|
|
1214
|
+
let best = null;
|
|
1215
|
+
for (const r of roots) {
|
|
1216
|
+
if (!r) continue;
|
|
1217
|
+
if (!best || r.length > best.length) best = r;
|
|
1218
|
+
}
|
|
1219
|
+
return best;
|
|
1220
|
+
}
|
|
1221
|
+
var GO_MARKER = "go.mod";
|
|
1222
|
+
var RUBY_MARKERS = ["Gemfile"];
|
|
1223
|
+
function findGoProjectRoot(start) {
|
|
1224
|
+
let dir = start;
|
|
1225
|
+
for (let i = 0; i < 20; i++) {
|
|
1226
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(dir, GO_MARKER))) return dir;
|
|
1227
|
+
const parent = (0, import_node_path2.dirname)(dir);
|
|
1228
|
+
if (parent === dir) return null;
|
|
1229
|
+
dir = parent;
|
|
1230
|
+
}
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
function findRubyProjectRoot(start) {
|
|
1234
|
+
let dir = start;
|
|
1235
|
+
for (let i = 0; i < 20; i++) {
|
|
1236
|
+
for (const m of RUBY_MARKERS) {
|
|
1237
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(dir, m))) return dir;
|
|
1238
|
+
}
|
|
1239
|
+
try {
|
|
1240
|
+
const entries = (0, import_node_fs3.readdirSync)(dir);
|
|
1241
|
+
if (entries.some((e) => e.endsWith(".gemspec"))) return dir;
|
|
1242
|
+
} catch {
|
|
1243
|
+
}
|
|
1244
|
+
const parent = (0, import_node_path2.dirname)(dir);
|
|
1245
|
+
if (parent === dir) return null;
|
|
1246
|
+
dir = parent;
|
|
1247
|
+
}
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
async function installGo(ctx) {
|
|
1251
|
+
const { projectRoot, opts, ingestUrl, fetchFn, home } = ctx;
|
|
1252
|
+
const projectName = opts.projectName ?? readGoModuleName(projectRoot) ?? projectRoot.split("/").pop() ?? "unnamed";
|
|
1253
|
+
const projectsJsonPath = (0, import_node_path2.join)(home, ".gg", "projects.json");
|
|
1254
|
+
const envFilePath = (0, import_node_path2.join)(projectRoot, ".env");
|
|
1255
|
+
const existing = findMappingByPath(projectsJsonPath, projectRoot);
|
|
1256
|
+
const existingKey = readEnvKey(envFilePath, "GG_PIXEL_KEY");
|
|
1257
|
+
let created;
|
|
1258
|
+
let reused = false;
|
|
1259
|
+
if (existing && existingKey) {
|
|
1260
|
+
created = { id: existing.id, key: existingKey };
|
|
1261
|
+
reused = true;
|
|
1262
|
+
} else {
|
|
1263
|
+
created = await createProject(fetchFn, ingestUrl, projectName);
|
|
1264
|
+
}
|
|
1265
|
+
const packageInstalled = opts.skipPackageInstall ? false : runGoGet(projectRoot);
|
|
1266
|
+
const initFilePath = (0, import_node_path2.join)(projectRoot, "gg_pixel_init.go");
|
|
1267
|
+
(0, import_node_fs3.writeFileSync)(
|
|
1268
|
+
initFilePath,
|
|
1269
|
+
`// gg-pixel init \u2014 auto-generated by ggcoder pixel install.
|
|
1270
|
+
package main
|
|
1271
|
+
|
|
1272
|
+
import (
|
|
1273
|
+
"os"
|
|
1274
|
+
gg "github.com/kenkaiiii/gg-pixel-go"
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
func init() {
|
|
1278
|
+
key := os.Getenv("GG_PIXEL_KEY")
|
|
1279
|
+
if key == "" {
|
|
1280
|
+
key = ${JSON.stringify(created.key)}
|
|
1281
|
+
}
|
|
1282
|
+
_ = gg.Init(gg.Options{ProjectKey: key, IngestURL: ${JSON.stringify(`${ingestUrl}/ingest`)}})
|
|
1283
|
+
}
|
|
1284
|
+
`,
|
|
1285
|
+
"utf8"
|
|
1286
|
+
);
|
|
1287
|
+
writeEnvKey(envFilePath, "GG_PIXEL_KEY", created.key);
|
|
1288
|
+
writeProjectsMapping(projectsJsonPath, created.id, projectName, projectRoot);
|
|
1289
|
+
return {
|
|
1290
|
+
projectId: created.id,
|
|
1291
|
+
projectKey: created.key,
|
|
1292
|
+
projectName,
|
|
1293
|
+
projectKind: "go",
|
|
1294
|
+
initFilePath,
|
|
1295
|
+
envFilePath,
|
|
1296
|
+
projectsJsonPath,
|
|
1297
|
+
packageManager: "pip",
|
|
1298
|
+
packageInstalled,
|
|
1299
|
+
entryWiring: { kind: "no_entry_found" },
|
|
1300
|
+
reused,
|
|
1301
|
+
warnings: [
|
|
1302
|
+
"Add `defer ggpixel.Recover()` near the top of your main() so panics are captured before the process exits."
|
|
1303
|
+
]
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
function readGoModuleName(projectRoot) {
|
|
1307
|
+
try {
|
|
1308
|
+
const content = (0, import_node_fs3.readFileSync)((0, import_node_path2.join)(projectRoot, "go.mod"), "utf8");
|
|
1309
|
+
const match = /^\s*module\s+(\S+)\s*$/m.exec(content);
|
|
1310
|
+
if (!match) return null;
|
|
1311
|
+
return match[1].split("/").pop() ?? null;
|
|
1312
|
+
} catch {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
function runGoGet(projectRoot) {
|
|
1317
|
+
const result = (0, import_node_child_process2.spawnSync)("go", ["get", "github.com/kenkaiiii/gg-pixel-go@latest"], {
|
|
1318
|
+
cwd: projectRoot,
|
|
1319
|
+
stdio: "inherit"
|
|
1320
|
+
});
|
|
1321
|
+
return result.status === 0;
|
|
1322
|
+
}
|
|
1323
|
+
async function installRuby(ctx) {
|
|
1324
|
+
const { projectRoot, opts, ingestUrl, fetchFn, home } = ctx;
|
|
1325
|
+
const projectName = opts.projectName ?? readRubyAppName(projectRoot) ?? projectRoot.split("/").pop() ?? "unnamed";
|
|
1326
|
+
const projectsJsonPath = (0, import_node_path2.join)(home, ".gg", "projects.json");
|
|
1327
|
+
const envFilePath = (0, import_node_path2.join)(projectRoot, ".env");
|
|
1328
|
+
const existing = findMappingByPath(projectsJsonPath, projectRoot);
|
|
1329
|
+
const existingKey = readEnvKey(envFilePath, "GG_PIXEL_KEY");
|
|
1330
|
+
let created;
|
|
1331
|
+
let reused = false;
|
|
1332
|
+
if (existing && existingKey) {
|
|
1333
|
+
created = { id: existing.id, key: existingKey };
|
|
1334
|
+
reused = true;
|
|
1335
|
+
} else {
|
|
1336
|
+
created = await createProject(fetchFn, ingestUrl, projectName);
|
|
1337
|
+
}
|
|
1338
|
+
const packageInstalled = opts.skipPackageInstall ? false : runRubyInstall(projectRoot);
|
|
1339
|
+
const initFilePath = (0, import_node_path2.join)(projectRoot, "gg_pixel_init.rb");
|
|
1340
|
+
(0, import_node_fs3.writeFileSync)(
|
|
1341
|
+
initFilePath,
|
|
1342
|
+
`# gg-pixel init \u2014 auto-generated by ggcoder pixel install.
|
|
1343
|
+
require "gg_pixel"
|
|
1344
|
+
GGPixel.init(
|
|
1345
|
+
project_key: ENV["GG_PIXEL_KEY"] || ${JSON.stringify(created.key)},
|
|
1346
|
+
ingest_url: ${JSON.stringify(`${ingestUrl}/ingest`)},
|
|
1347
|
+
)
|
|
1348
|
+
`,
|
|
1349
|
+
"utf8"
|
|
1350
|
+
);
|
|
1351
|
+
writeEnvKey(envFilePath, "GG_PIXEL_KEY", created.key);
|
|
1352
|
+
writeProjectsMapping(projectsJsonPath, created.id, projectName, projectRoot);
|
|
1353
|
+
return {
|
|
1354
|
+
projectId: created.id,
|
|
1355
|
+
projectKey: created.key,
|
|
1356
|
+
projectName,
|
|
1357
|
+
projectKind: "ruby",
|
|
1358
|
+
initFilePath,
|
|
1359
|
+
envFilePath,
|
|
1360
|
+
projectsJsonPath,
|
|
1361
|
+
packageManager: "pip",
|
|
1362
|
+
packageInstalled,
|
|
1363
|
+
entryWiring: { kind: "no_entry_found" },
|
|
1364
|
+
reused,
|
|
1365
|
+
warnings: [
|
|
1366
|
+
`Add \`require "./gg_pixel_init"\` at the top of your entry script (often \`config/application.rb\` for Rails, \`app.rb\` for Sinatra, or your main file).`
|
|
1367
|
+
]
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function readRubyAppName(projectRoot) {
|
|
1371
|
+
try {
|
|
1372
|
+
const entries = (0, import_node_fs3.readdirSync)(projectRoot);
|
|
1373
|
+
const gemspec = entries.find((e) => e.endsWith(".gemspec"));
|
|
1374
|
+
if (!gemspec) return null;
|
|
1375
|
+
return gemspec.replace(/\.gemspec$/, "");
|
|
1376
|
+
} catch {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function runRubyInstall(projectRoot) {
|
|
1381
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(projectRoot, "Gemfile"))) {
|
|
1382
|
+
try {
|
|
1383
|
+
const content = (0, import_node_fs3.readFileSync)((0, import_node_path2.join)(projectRoot, "Gemfile"), "utf8");
|
|
1384
|
+
if (!content.includes("gg_pixel")) {
|
|
1385
|
+
(0, import_node_fs3.writeFileSync)(
|
|
1386
|
+
(0, import_node_path2.join)(projectRoot, "Gemfile"),
|
|
1387
|
+
content + (content.endsWith("\n") ? "" : "\n") + 'gem "gg_pixel"\n',
|
|
1388
|
+
"utf8"
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
} catch {
|
|
1392
|
+
}
|
|
1393
|
+
const r = (0, import_node_child_process2.spawnSync)("bundle", ["install"], { cwd: projectRoot, stdio: "inherit" });
|
|
1394
|
+
if (r.status === 0) return true;
|
|
1395
|
+
}
|
|
1396
|
+
const r2 = (0, import_node_child_process2.spawnSync)("gem", ["install", "gg_pixel"], { cwd: projectRoot, stdio: "inherit" });
|
|
1397
|
+
return r2.status === 0;
|
|
1109
1398
|
}
|
|
1110
1399
|
async function installPython(ctx) {
|
|
1111
1400
|
const { projectRoot, opts, ingestUrl, fetchFn, home } = ctx;
|