apibara 2.0.0-beta.9 → 2.1.0-beta.1

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.
Files changed (83) hide show
  1. package/dist/chunks/add.mjs +44 -0
  2. package/dist/chunks/build.mjs +3 -3
  3. package/dist/chunks/dev.mjs +22 -18
  4. package/dist/chunks/init.mjs +41 -0
  5. package/dist/chunks/prepare.mjs +0 -2
  6. package/dist/chunks/start.mjs +56 -0
  7. package/dist/cli/index.mjs +5 -1
  8. package/dist/config/index.d.mts +1 -1
  9. package/dist/config/index.d.ts +1 -1
  10. package/dist/core/index.mjs +61 -97
  11. package/dist/create/index.d.mts +17 -0
  12. package/dist/create/index.d.ts +17 -0
  13. package/dist/create/index.mjs +961 -0
  14. package/dist/rollup/index.d.mts +2 -1
  15. package/dist/rollup/index.d.ts +2 -1
  16. package/dist/rollup/index.mjs +130 -167
  17. package/dist/runtime/dev.d.ts +3 -0
  18. package/dist/runtime/dev.mjs +55 -0
  19. package/dist/runtime/index.d.ts +2 -0
  20. package/dist/runtime/index.mjs +2 -0
  21. package/dist/runtime/internal/app.d.ts +2 -0
  22. package/dist/runtime/internal/app.mjs +56 -0
  23. package/dist/runtime/internal/logger.d.ts +14 -0
  24. package/dist/runtime/internal/logger.mjs +45 -0
  25. package/dist/runtime/start.d.ts +3 -0
  26. package/dist/runtime/start.mjs +41 -0
  27. package/dist/types/index.d.mts +22 -19
  28. package/dist/types/index.d.ts +22 -19
  29. package/package.json +34 -13
  30. package/runtime-meta.d.ts +2 -0
  31. package/runtime-meta.mjs +7 -0
  32. package/src/cli/commands/add.ts +44 -0
  33. package/src/cli/commands/build.ts +5 -3
  34. package/src/cli/commands/dev.ts +28 -18
  35. package/src/cli/commands/init.ts +40 -0
  36. package/src/cli/commands/prepare.ts +0 -2
  37. package/src/cli/commands/start.ts +61 -0
  38. package/src/cli/index.ts +3 -0
  39. package/src/config/index.ts +5 -4
  40. package/src/core/apibara.ts +4 -2
  41. package/src/core/build/build.ts +2 -0
  42. package/src/core/build/dev.ts +1 -0
  43. package/src/core/build/error.ts +0 -1
  44. package/src/core/build/prepare.ts +5 -2
  45. package/src/core/build/prod.ts +10 -6
  46. package/src/core/build/types.ts +4 -95
  47. package/src/core/config/defaults.ts +1 -4
  48. package/src/core/config/loader.ts +1 -0
  49. package/src/core/config/resolvers/runtime-config.resolver.ts +1 -1
  50. package/src/core/config/update.ts +2 -3
  51. package/src/core/path.ts +11 -0
  52. package/src/core/scan.ts +40 -0
  53. package/src/create/add.ts +225 -0
  54. package/src/create/colors.ts +15 -0
  55. package/src/create/constants.ts +98 -0
  56. package/src/create/index.ts +2 -0
  57. package/src/create/init.ts +164 -0
  58. package/src/create/templates.ts +465 -0
  59. package/src/create/types.ts +34 -0
  60. package/src/create/utils.ts +412 -0
  61. package/src/rollup/config.ts +67 -189
  62. package/src/rollup/index.ts +1 -0
  63. package/src/rollup/plugins/config.ts +12 -0
  64. package/src/rollup/plugins/esm-shim.ts +69 -0
  65. package/src/rollup/plugins/indexers.ts +17 -0
  66. package/src/runtime/dev.ts +64 -0
  67. package/src/runtime/index.ts +2 -0
  68. package/src/runtime/internal/app.ts +78 -0
  69. package/src/runtime/internal/logger.ts +70 -0
  70. package/src/runtime/start.ts +48 -0
  71. package/src/types/apibara.ts +8 -0
  72. package/src/types/config.ts +28 -27
  73. package/src/types/hooks.ts +1 -0
  74. package/src/types/virtual/config.d.ts +3 -0
  75. package/src/types/virtual/indexers.d.ts +10 -0
  76. package/dist/internal/citty/index.d.mts +0 -1
  77. package/dist/internal/citty/index.d.ts +0 -1
  78. package/dist/internal/citty/index.mjs +0 -1
  79. package/dist/internal/consola/index.d.mts +0 -2
  80. package/dist/internal/consola/index.d.ts +0 -2
  81. package/dist/internal/consola/index.mjs +0 -1
  82. package/src/internal/citty/index.ts +0 -1
  83. package/src/internal/consola/index.ts +0 -1
@@ -0,0 +1,961 @@
1
+ import consola$1, { consola } from 'consola';
2
+ import prompts from 'prompts';
3
+ import colors from 'picocolors';
4
+ import fs from 'node:fs';
5
+ import path, { basename } from 'node:path';
6
+ import { Project, SyntaxKind } from 'ts-morph';
7
+
8
+ const {
9
+ blue,
10
+ blueBright,
11
+ cyan,
12
+ gray,
13
+ green,
14
+ greenBright,
15
+ magenta,
16
+ red,
17
+ redBright,
18
+ reset,
19
+ yellow
20
+ } = colors;
21
+
22
+ const chains = [
23
+ {
24
+ name: "starknet",
25
+ display: "Starknet",
26
+ color: blue,
27
+ networks: [
28
+ { name: "mainnet", display: "Mainnet", color: blue },
29
+ { name: "sepolia", display: "Sepolia", color: yellow }
30
+ ]
31
+ },
32
+ {
33
+ name: "ethereum",
34
+ display: "Ethereum",
35
+ color: green,
36
+ networks: [
37
+ { name: "mainnet", display: "Mainnet", color: blue },
38
+ { name: "sepolia", display: "Sepolia", color: yellow }
39
+ ]
40
+ },
41
+ {
42
+ name: "beaconchain",
43
+ display: "Beacon Chain",
44
+ color: yellow,
45
+ networks: [{ name: "mainnet", display: "Mainnet", color: yellow }]
46
+ }
47
+ ];
48
+ const networks = [
49
+ { name: "mainnet", display: "Mainnet", color: blue },
50
+ { name: "sepolia", display: "Sepolia", color: green },
51
+ { name: "other", display: "Other", color: red }
52
+ ];
53
+ const storages = [
54
+ { name: "postgres", display: "Postgres", color: green },
55
+ { name: "none", display: "None", color: red }
56
+ ];
57
+ const packageVersions = {
58
+ // Required Dependencies
59
+ apibara: "^2.1.0-beta.1",
60
+ "@apibara/indexer": "^2.1.0-beta.1",
61
+ "@apibara/protocol": "^2.1.0-beta.1",
62
+ // Chain Dependencies
63
+ "@apibara/evm": "^2.1.0-beta.1",
64
+ "@apibara/beaconchain": "^2.1.0-beta.1",
65
+ "@apibara/starknet": "^2.1.0-beta.1",
66
+ // Storage Dependencies
67
+ "@apibara/plugin-drizzle": "^2.1.0-beta.1",
68
+ "@apibara/plugin-mongo": "^2.1.0-beta.1",
69
+ "@apibara/plugin-sqlite": "^2.1.0-beta.1",
70
+ // Postgres Dependencies
71
+ "@electric-sql/pglite": "^0.2.17",
72
+ "drizzle-orm": "^0.37.0",
73
+ pg: "^8.13.1",
74
+ "@types/pg": "^8.11.10",
75
+ "drizzle-kit": "^0.29.0",
76
+ // Typescript Dependencies
77
+ typescript: "^5.6.2",
78
+ "@rollup/plugin-typescript": "^11.1.6",
79
+ "@types/node": "^20.5.2"
80
+ };
81
+ const dnaUrls = {
82
+ ethereum: "https://ethereum.preview.apibara.org",
83
+ ethereumSepolia: "https://ethereum-sepolia.preview.apibara.org",
84
+ beaconchain: "https://beaconchain.preview.apibara.org",
85
+ starknet: "https://starknet.preview.apibara.org",
86
+ starknetSepolia: "https://starknet-sepolia.preview.apibara.org"
87
+ };
88
+
89
+ function isEmpty(path2) {
90
+ const files = fs.readdirSync(path2);
91
+ return files.length === 0 || files.length === 1 && files[0] === ".git";
92
+ }
93
+ function emptyDir(dir) {
94
+ if (!fs.existsSync(dir)) {
95
+ return;
96
+ }
97
+ for (const file of fs.readdirSync(dir)) {
98
+ if (file === ".git") {
99
+ continue;
100
+ }
101
+ fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });
102
+ }
103
+ }
104
+ function validateLanguage(language, throwError = false) {
105
+ if (!language) {
106
+ return false;
107
+ }
108
+ if (language === "typescript" || language === "ts" || language === "javascript" || language === "js") {
109
+ return true;
110
+ }
111
+ if (throwError) {
112
+ throw new Error(
113
+ `Invalid language ${cyan("(--language | -l)")}: ${red(language)}. Options: ${blue("typescript, ts")} or ${yellow("javascript, js")} | default: ${cyan("typescript")}`
114
+ );
115
+ }
116
+ return false;
117
+ }
118
+ function getLanguageFromAlias(alias) {
119
+ if (alias === "ts" || alias === "typescript") {
120
+ return "typescript";
121
+ }
122
+ if (alias === "js" || alias === "javascript") {
123
+ return "javascript";
124
+ }
125
+ throw new Error(
126
+ `Invalid language ${cyan("(--language | -l)")}: ${red(alias)}. Options: ${blue("typescript, ts")} or ${yellow("javascript, js")}`
127
+ );
128
+ }
129
+ function validateIndexerId(indexerId, throwError = false) {
130
+ if (!indexerId) {
131
+ return false;
132
+ }
133
+ if (!/^[a-z0-9-]+$/.test(indexerId)) {
134
+ if (throwError) {
135
+ throw new Error(
136
+ `Invalid indexer ID ${cyan("(--indexer-id)")}: ${red(indexerId)}. Indexer ID must contain only lowercase letters, numbers, and hyphens.`
137
+ );
138
+ }
139
+ return false;
140
+ }
141
+ return true;
142
+ }
143
+ function validateChain(chain, throwError = false) {
144
+ if (!chain) {
145
+ return false;
146
+ }
147
+ if (chain) {
148
+ if (chain === "starknet" || chain === "ethereum" || chain === "beaconchain")
149
+ return true;
150
+ if (throwError) {
151
+ throw new Error(
152
+ `Invalid chain ${cyan("(--chain)")}: ${red(chain)}. Chain must be one of ${blue("starknet, ethereum, beaconchain")}.`
153
+ );
154
+ }
155
+ return false;
156
+ }
157
+ return true;
158
+ }
159
+ function validateNetwork(chain, network, throwError = false) {
160
+ if (!network) {
161
+ return false;
162
+ }
163
+ if (network === "other") {
164
+ return true;
165
+ }
166
+ if (chain) {
167
+ if (chain === "starknet") {
168
+ if (network === "mainnet" || network === "sepolia") {
169
+ return true;
170
+ }
171
+ if (throwError) {
172
+ throw new Error(
173
+ `Invalid network ${cyan("(--network)")}: ${red(network)}. For chain ${blue("starknet")}, network must be one of ${blue("mainnet, sepolia, other")}.`
174
+ );
175
+ }
176
+ return false;
177
+ }
178
+ if (chain === "ethereum") {
179
+ if (network === "mainnet" || network === "goerli") {
180
+ return true;
181
+ }
182
+ if (throwError) {
183
+ throw new Error(
184
+ `Invalid network ${cyan("(--network)")}: ${red(network)}. For chain ${blue("ethereum")}, network must be one of ${blue("mainnet, goerli, other")}.`
185
+ );
186
+ }
187
+ return false;
188
+ }
189
+ if (chain === "beaconchain") {
190
+ if (network === "mainnet") {
191
+ return true;
192
+ }
193
+ if (throwError) {
194
+ throw new Error(
195
+ `Invalid network ${cyan("(--network)")}: ${red(network)}. For chain ${blue("beaconchain")}, network must be ${blue("mainnet, other")}.`
196
+ );
197
+ }
198
+ return false;
199
+ }
200
+ }
201
+ if (networks.find((n) => n.name === network)) {
202
+ return true;
203
+ }
204
+ if (throwError) {
205
+ throw new Error(
206
+ `Invalid network ${cyan("(--network)")}: ${red(network)}. Network must be one of ${blue("mainnet, sepolia, goerli, other")}.`
207
+ );
208
+ }
209
+ return false;
210
+ }
211
+ function validateStorage(storage, throwError = false) {
212
+ if (!storage) {
213
+ return false;
214
+ }
215
+ if (storage === "postgres" || storage === "none") {
216
+ return true;
217
+ }
218
+ if (throwError) {
219
+ throw new Error(
220
+ `Invalid storage ${cyan("(--storage)")}: ${red(storage)}. Storage must be one of ${blue("postgres, none")}.`
221
+ );
222
+ }
223
+ return false;
224
+ }
225
+ function validateDnaUrl(dnaUrl, throwError = false) {
226
+ if (!dnaUrl) {
227
+ return false;
228
+ }
229
+ if (!dnaUrl.startsWith("https://") && !dnaUrl.startsWith("http://")) {
230
+ if (throwError) {
231
+ throw new Error(
232
+ `Invalid DNA URL ${cyan("(--dna-url)")}: ${red(dnaUrl)}. DNA URL must start with ${blue("https:// or http://")}.`
233
+ );
234
+ }
235
+ return false;
236
+ }
237
+ return true;
238
+ }
239
+ function hasApibaraConfig(cwd) {
240
+ const configPathJS = path.join(cwd, "apibara.config.js");
241
+ const configPathTS = path.join(cwd, "apibara.config.ts");
242
+ return fs.existsSync(configPathJS) || fs.existsSync(configPathTS);
243
+ }
244
+ function getApibaraConfigLanguage(cwd) {
245
+ const configPathJS = path.join(cwd, "apibara.config.js");
246
+ const configPathTS = path.join(cwd, "apibara.config.ts");
247
+ if (fs.existsSync(configPathJS)) {
248
+ return "javascript";
249
+ }
250
+ if (fs.existsSync(configPathTS)) {
251
+ return "typescript";
252
+ }
253
+ throw new Error(red("\u2716") + " No apibara.config found");
254
+ }
255
+ function getDnaUrl(chain, network) {
256
+ if (chain === "ethereum") {
257
+ if (network === "mainnet") {
258
+ return dnaUrls.ethereum;
259
+ }
260
+ if (network === "sepolia") {
261
+ return dnaUrls.ethereumSepolia;
262
+ }
263
+ }
264
+ if (chain === "beaconchain") {
265
+ if (network === "mainnet") {
266
+ return dnaUrls.beaconchain;
267
+ }
268
+ }
269
+ if (chain === "starknet") {
270
+ if (network === "mainnet") {
271
+ return dnaUrls.starknet;
272
+ }
273
+ if (network === "sepolia") {
274
+ return dnaUrls.starknetSepolia;
275
+ }
276
+ }
277
+ throw new Error(red("\u2716") + " Invalid chain or network");
278
+ }
279
+ function convertKebabToCamelCase(_str) {
280
+ let str = _str;
281
+ if (!str || typeof str !== "string") {
282
+ return "";
283
+ }
284
+ if (/^[a-z][a-zA-Z0-9]*$/.test(str)) {
285
+ return str;
286
+ }
287
+ str = str.trim().replace(/^-+|-+$/g, "");
288
+ if (!str) {
289
+ return "";
290
+ }
291
+ return str.replace(/[-_]+/g, "-").split("-").filter(Boolean).map((word, index) => {
292
+ const _word = word.toLowerCase();
293
+ if (index > 0) {
294
+ return _word.charAt(0).toUpperCase() + _word.slice(1);
295
+ }
296
+ return _word;
297
+ }).join("");
298
+ }
299
+ async function checkFileExists(path2, options) {
300
+ const { askPrompt = false, fileName, allowIgnore = false } = options ?? {};
301
+ if (!fs.existsSync(path2)) {
302
+ return {
303
+ exists: false,
304
+ overwrite: false
305
+ };
306
+ }
307
+ if (askPrompt) {
308
+ const { overwrite } = await prompts({
309
+ type: "select",
310
+ name: "overwrite",
311
+ message: `${fileName ?? basename(path2)} already exists. Please choose how to proceed:`,
312
+ initial: 0,
313
+ choices: [
314
+ ...allowIgnore ? [
315
+ {
316
+ title: "Keep original file",
317
+ value: "ignore"
318
+ }
319
+ ] : [],
320
+ {
321
+ title: "Cancel operation",
322
+ value: "no"
323
+ },
324
+ {
325
+ title: "Overwrite file",
326
+ value: "yes"
327
+ }
328
+ ]
329
+ });
330
+ if (overwrite === "no") {
331
+ cancelOperation();
332
+ }
333
+ if (overwrite === "ignore") {
334
+ return {
335
+ exists: true,
336
+ overwrite: false
337
+ };
338
+ }
339
+ return {
340
+ exists: true,
341
+ overwrite: true
342
+ };
343
+ }
344
+ return {
345
+ exists: true,
346
+ overwrite: false
347
+ };
348
+ }
349
+ function cancelOperation(message) {
350
+ throw new Error(red("\u2716") + (message ?? " Operation cancelled"));
351
+ }
352
+ function getPackageManager() {
353
+ const userAgent = process.env.npm_config_user_agent;
354
+ const pkgInfo = pkgFromUserAgent(userAgent);
355
+ if (pkgInfo) {
356
+ return pkgInfo;
357
+ }
358
+ return {
359
+ name: "npm"
360
+ };
361
+ }
362
+ function pkgFromUserAgent(userAgent) {
363
+ if (!userAgent)
364
+ return void 0;
365
+ const pkgSpec = userAgent.split(" ")[0];
366
+ const pkgSpecArr = pkgSpec.split("/");
367
+ return {
368
+ name: pkgSpecArr[0],
369
+ version: pkgSpecArr[1]
370
+ };
371
+ }
372
+
373
+ function generatePackageJson(isTypeScript) {
374
+ return {
375
+ name: "apibara-app",
376
+ version: "0.1.0",
377
+ private: true,
378
+ type: "module",
379
+ scripts: {
380
+ prepare: "apibara prepare",
381
+ dev: "apibara dev",
382
+ start: "apibara start",
383
+ build: "apibara build",
384
+ ...isTypeScript && { typecheck: "tsc --noEmit" }
385
+ },
386
+ dependencies: {
387
+ "@apibara/indexer": packageVersions["@apibara/indexer"],
388
+ "@apibara/protocol": packageVersions["@apibara/protocol"],
389
+ apibara: packageVersions.apibara
390
+ },
391
+ devDependencies: {
392
+ ...isTypeScript && {
393
+ "@rollup/plugin-typescript": packageVersions["@rollup/plugin-typescript"],
394
+ "@types/node": packageVersions["@types/node"],
395
+ typescript: packageVersions.typescript
396
+ }
397
+ }
398
+ };
399
+ }
400
+ function generateTsConfig() {
401
+ return {
402
+ $schema: "https://json.schemastore.org/tsconfig",
403
+ display: "Default",
404
+ compilerOptions: {
405
+ forceConsistentCasingInFileNames: true,
406
+ target: "ES2022",
407
+ lib: ["ESNext"],
408
+ module: "ESNext",
409
+ moduleResolution: "bundler",
410
+ skipLibCheck: true,
411
+ types: ["node"],
412
+ noEmit: true,
413
+ strict: true,
414
+ baseUrl: "."
415
+ },
416
+ include: [".", "./.apibara/types"],
417
+ exclude: ["node_modules"]
418
+ };
419
+ }
420
+ function generateApibaraConfig(isTypeScript) {
421
+ return `${isTypeScript ? 'import typescript from "@rollup/plugin-typescript";\nimport type { Plugin } from "apibara/rollup";\n' : ""}import { defineConfig } from "apibara/config";
422
+
423
+ export default defineConfig({
424
+ runtimeConfig: {},${isTypeScript ? `
425
+ rollupConfig: {
426
+ plugins: [typescript()${isTypeScript ? " as Plugin" : ""}],
427
+ },` : ""}
428
+ });
429
+ `;
430
+ }
431
+ function generateIndexer({
432
+ indexerId,
433
+ storage,
434
+ chain,
435
+ language
436
+ }) {
437
+ return `${chain === "ethereum" ? `import { EvmStream } from "@apibara/evm";` : chain === "beaconchain" ? `import { BeaconChainStream } from "@apibara/beaconchain";` : chain === "starknet" ? `import { StarknetStream } from "@apibara/starknet";` : ""}
438
+ import { defineIndexer } from "@apibara/indexer";
439
+ ${storage === "postgres" ? `import { drizzleStorage } from "@apibara/plugin-drizzle";` : ""}
440
+ ${language === "typescript" ? `import type { ApibaraRuntimeConfig } from "apibara/types";` : ""}
441
+ import { useLogger } from "@apibara/indexer/plugins";
442
+ ${storage === "postgres" ? `import { getDrizzlePgDatabase } from "../lib/db";` : ""}
443
+
444
+
445
+ export default function (runtimeConfig${language === "typescript" ? ": ApibaraRuntimeConfig" : ""}) {
446
+ const indexerId = "${indexerId}";
447
+ const { startingBlock, streamUrl${storage === "postgres" ? ", postgresConnectionString" : ""} } = runtimeConfig[indexerId];
448
+ ${storage === "postgres" ? "const { db } = getDrizzlePgDatabase(postgresConnectionString);" : ""}
449
+
450
+ return defineIndexer(${chain === "ethereum" ? "EvmStream" : chain === "beaconchain" ? "BeaconChainStream" : chain === "starknet" ? "StarknetStream" : ""})({
451
+ streamUrl,
452
+ finality: "accepted",
453
+ startingBlock: BigInt(startingBlock),
454
+ filter: {
455
+ header: "always",
456
+ },
457
+ plugins: [${storage === "postgres" ? "drizzleStorage({ db, persistState: true })" : ""}],
458
+ async transform({ endCursor, finality }) {
459
+ const logger = useLogger();
460
+
461
+ logger.info(
462
+ "Transforming block | orderKey: ",
463
+ endCursor?.orderKey,
464
+ " | finality: ",
465
+ finality
466
+ );
467
+
468
+ ${storage === "postgres" ? `// Example snippet to insert data into db using drizzle with postgres
469
+ // const { db } = useDrizzleStorage();
470
+ // const { logs } = block;
471
+ // for (const log of logs) {
472
+ // await db.insert(exampleTable).values({
473
+ // number: Number(endCursor?.orderKey),
474
+ // hash: log.transactionHash,
475
+ // });
476
+ // }` : ""}
477
+ },
478
+ });
479
+ }
480
+ `;
481
+ }
482
+ async function createIndexerFile(options) {
483
+ const indexerFilePath = path.join(
484
+ options.cwd,
485
+ "indexers",
486
+ `${options.indexerFileId}.indexer.${options.language === "typescript" ? "ts" : "js"}`
487
+ );
488
+ const { exists, overwrite } = await checkFileExists(indexerFilePath, {
489
+ askPrompt: true
490
+ });
491
+ if (exists && !overwrite)
492
+ return;
493
+ const indexerContent = generateIndexer(options);
494
+ fs.mkdirSync(path.dirname(indexerFilePath), { recursive: true });
495
+ fs.writeFileSync(indexerFilePath, indexerContent);
496
+ }
497
+ function updatePackageJson({
498
+ cwd,
499
+ chain,
500
+ storage,
501
+ language
502
+ }) {
503
+ const packageJson = JSON.parse(
504
+ fs.readFileSync(path.join(cwd, "package.json"), "utf8")
505
+ );
506
+ if (chain === "ethereum") {
507
+ packageJson.dependencies["@apibara/evm"] = packageVersions["@apibara/evm"];
508
+ } else if (chain === "beaconchain") {
509
+ packageJson.dependencies["@apibara/beaconchain"] = packageVersions["@apibara/beaconchain"];
510
+ } else if (chain === "starknet") {
511
+ packageJson.dependencies["@apibara/starknet"] = packageVersions["@apibara/starknet"];
512
+ }
513
+ if (storage === "postgres") {
514
+ packageJson.scripts["drizzle:generate"] = "drizzle-kit generate";
515
+ packageJson.scripts["drizzle:migrate"] = "drizzle-kit migrate";
516
+ packageJson.dependencies["@apibara/plugin-drizzle"] = packageVersions["@apibara/plugin-drizzle"];
517
+ packageJson.dependencies["drizzle-orm"] = packageVersions["drizzle-orm"];
518
+ packageJson.dependencies["@electric-sql/pglite"] = packageVersions["@electric-sql/pglite"];
519
+ packageJson.dependencies["drizzle-kit"] = packageVersions["drizzle-kit"];
520
+ packageJson.dependencies["pg"] = packageVersions["pg"];
521
+ if (language === "typescript") {
522
+ packageJson.devDependencies["@types/pg"] = packageVersions["@types/pg"];
523
+ }
524
+ }
525
+ fs.writeFileSync(
526
+ path.join(cwd, "package.json"),
527
+ JSON.stringify(packageJson, null, 2)
528
+ );
529
+ }
530
+ function updateApibaraConfigFile({
531
+ indexerId,
532
+ cwd,
533
+ chain,
534
+ storage,
535
+ language,
536
+ network,
537
+ dnaUrl
538
+ }) {
539
+ const pathToConfig = path.join(
540
+ cwd,
541
+ `apibara.config.${language === "typescript" ? "ts" : "js"}`
542
+ );
543
+ const runtimeConfigString = `{
544
+ startingBlock: 0,
545
+ streamUrl: "${dnaUrl ?? getDnaUrl(chain, network)}"${storage === "postgres" ? `,
546
+ postgresConnectionString: process.env["POSTGRES_CONNECTION_STRING"] ?? "memory://${indexerId}"` : ""}}`;
547
+ const project = new Project();
548
+ const sourceFile = project.addSourceFileAtPath(pathToConfig);
549
+ const defineConfigCall = sourceFile.getFirstDescendantByKind(
550
+ SyntaxKind.CallExpression
551
+ );
552
+ if (!defineConfigCall)
553
+ return;
554
+ const configObjectExpression = defineConfigCall.getArguments()[0];
555
+ const runtimeConfigObject = configObjectExpression.getProperty("runtimeConfig");
556
+ if (!runtimeConfigObject) {
557
+ configObjectExpression.addPropertyAssignment({
558
+ name: "runtimeConfig",
559
+ initializer: `{
560
+ "${indexerId}": ${runtimeConfigString}
561
+ }`
562
+ });
563
+ } else {
564
+ const runtimeConfigProp = runtimeConfigObject.asKindOrThrow(
565
+ SyntaxKind.PropertyAssignment
566
+ );
567
+ const runtimeConfigObj = runtimeConfigProp.getInitializerOrThrow().asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
568
+ runtimeConfigObj.addPropertyAssignment({
569
+ name: `"${indexerId}"`,
570
+ initializer: runtimeConfigString
571
+ });
572
+ }
573
+ sourceFile.saveSync();
574
+ sourceFile.formatText({
575
+ tabSize: 2,
576
+ insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: true,
577
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true
578
+ });
579
+ }
580
+ async function createDrizzleStorageFiles(options) {
581
+ const { cwd, language, storage } = options;
582
+ if (storage !== "postgres")
583
+ return;
584
+ const fileExtension = language === "typescript" ? "ts" : "js";
585
+ const drizzleConfigFileName = `drizzle.config.${fileExtension}`;
586
+ const drizzleConfigPath = path.join(cwd, drizzleConfigFileName);
587
+ const { exists, overwrite } = await checkFileExists(drizzleConfigPath, {
588
+ askPrompt: true,
589
+ allowIgnore: true
590
+ });
591
+ if (!exists || overwrite) {
592
+ const drizzleConfigContent = `${language === "typescript" ? 'import type { Config } from "drizzle-kit";' : ""}
593
+
594
+ export default {
595
+ schema: "./lib/schema.ts",
596
+ out: "./drizzle",
597
+ dialect: "postgresql",
598
+ dbCredentials: {
599
+ url: process.env["POSTGRES_CONNECTION_STRING"] ?? "",
600
+ },
601
+ }${language === "typescript" ? " satisfies Config" : ""};`;
602
+ fs.writeFileSync(drizzleConfigPath, drizzleConfigContent);
603
+ consola.success(`Created ${cyan(drizzleConfigFileName)}`);
604
+ }
605
+ const schemaFileName = `schema.${fileExtension}`;
606
+ const schemaPath = path.join(cwd, "lib", schemaFileName);
607
+ const { exists: schemaExists, overwrite: schemaOverwrite } = await checkFileExists(schemaPath, {
608
+ askPrompt: true,
609
+ allowIgnore: true,
610
+ fileName: `lib/${schemaFileName}`
611
+ });
612
+ if (!schemaExists || schemaOverwrite) {
613
+ const schemaContent = `// --- Add your pg table schemas here ----
614
+
615
+ // import { bigint, pgTable, text, uuid } from "drizzle-orm/pg-core";
616
+
617
+ // export const exampleTable = pgTable("example_table", {
618
+ // id: uuid("id").primaryKey().defaultRandom(),
619
+ // number: bigint("number", { mode: "number" }),
620
+ // hash: text("hash"),
621
+ // });
622
+
623
+ export {};
624
+ `;
625
+ fs.mkdirSync(path.dirname(schemaPath), { recursive: true });
626
+ fs.writeFileSync(schemaPath, schemaContent);
627
+ consola.success(`Created ${cyan("lib/schema.ts")}`);
628
+ }
629
+ const dbFileName = `db.${fileExtension}`;
630
+ const dbPath = path.join(cwd, "lib", dbFileName);
631
+ const { exists: dbExists, overwrite: dbOverwrite } = await checkFileExists(
632
+ dbPath,
633
+ {
634
+ askPrompt: true,
635
+ fileName: `lib/${dbFileName}`,
636
+ allowIgnore: true
637
+ }
638
+ );
639
+ if (!dbExists || dbOverwrite) {
640
+ const dbContent = `import * as schema from "./schema";
641
+ import { drizzle as nodePgDrizzle } from "drizzle-orm/node-postgres";
642
+ import { drizzle as pgLiteDrizzle } from "drizzle-orm/pglite";
643
+ import pg from "pg";
644
+
645
+
646
+ export function getDrizzlePgDatabase(connectionString${language === "typescript" ? ": string" : ""}) {
647
+ // Create pglite instance
648
+ if (connectionString.includes("memory")) {
649
+ return {
650
+ db: pgLiteDrizzle({
651
+ schema,
652
+ connection: {
653
+ dataDir: connectionString,
654
+ },
655
+ }),
656
+ };
657
+ }
658
+
659
+ // Create node-postgres instance
660
+ const pool = new pg.Pool({
661
+ connectionString,
662
+ });
663
+
664
+ return { db: nodePgDrizzle(pool, { schema }) };
665
+ }`;
666
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
667
+ fs.writeFileSync(dbPath, dbContent);
668
+ consola.success(`Created ${cyan(`lib/${dbFileName}`)}`);
669
+ }
670
+ console.log("\n");
671
+ if (!schemaExists || schemaOverwrite) {
672
+ consola.info(
673
+ `Make sure to export your pgTables in ${cyan(`lib/${schemaFileName}`)}`
674
+ );
675
+ console.log();
676
+ consola.info(`${magenta("Example:")}
677
+
678
+ ${yellow(`
679
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
680
+ \u2502 lib/schema.ts \u2502
681
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
682
+
683
+ import { bigint, pgTable, text, uuid } from "drizzle-orm/pg-core";
684
+
685
+ export const exampleTable = pgTable("example_table", {
686
+ id: uuid("id").primaryKey().defaultRandom(),
687
+ number: bigint("number", { mode: "number" }),
688
+ hash: text("hash"),
689
+ });`)}`);
690
+ console.log("\n");
691
+ }
692
+ consola.info(
693
+ `Run ${green(`${options.packageManager} run drizzle:generate`)} & ${green(`${options.packageManager} run drizzle:migrate`)} to generate and apply migrations.`
694
+ );
695
+ }
696
+ async function createStorageRelatedFiles(options) {
697
+ const { storage } = options;
698
+ if (storage === "postgres") {
699
+ await createDrizzleStorageFiles(options);
700
+ }
701
+ }
702
+
703
+ async function initializeProject({
704
+ argTargetDir,
705
+ argLanguage,
706
+ argNoCreateIndexer
707
+ }) {
708
+ const cwd = process.cwd();
709
+ validateLanguage(argLanguage, true);
710
+ console.log();
711
+ const result = await prompts(
712
+ [
713
+ {
714
+ type: () => argTargetDir && (!fs.existsSync(argTargetDir) || isEmpty(argTargetDir)) ? null : "select",
715
+ name: "overwrite",
716
+ message: () => (argTargetDir === "." ? "Current directory" : `Target directory "${argTargetDir}"`) + " is not empty. Please choose how to proceed:",
717
+ initial: 0,
718
+ choices: [
719
+ {
720
+ title: "Cancel operation",
721
+ value: "no"
722
+ },
723
+ {
724
+ title: "Remove existing files and continue",
725
+ value: "yes"
726
+ },
727
+ {
728
+ title: "Ignore files and continue",
729
+ value: "ignore"
730
+ }
731
+ ],
732
+ hint: "\nCurrent Working Directory: " + cwd
733
+ },
734
+ {
735
+ type: (_, { overwrite: overwrite2 }) => {
736
+ if (overwrite2 === "no") {
737
+ cancelOperation();
738
+ }
739
+ return null;
740
+ },
741
+ name: "overwriteChecker"
742
+ },
743
+ {
744
+ type: argLanguage ? null : "select",
745
+ name: "prompt_language",
746
+ message: "Select a language:",
747
+ choices: [
748
+ {
749
+ title: "Typescript",
750
+ value: "typescript"
751
+ },
752
+ {
753
+ title: "Javascript",
754
+ value: "javascript"
755
+ }
756
+ ]
757
+ }
758
+ ],
759
+ {
760
+ onCancel: () => {
761
+ cancelOperation();
762
+ }
763
+ }
764
+ );
765
+ const { overwrite, prompt_language } = result;
766
+ const root = path.join(cwd, argTargetDir);
767
+ if (overwrite === "yes") {
768
+ emptyDir(root);
769
+ } else if (!fs.existsSync(root)) {
770
+ fs.mkdirSync(root, { recursive: true });
771
+ }
772
+ const lang = argLanguage ? getLanguageFromAlias(argLanguage) : prompt_language;
773
+ const isTs = lang === "typescript";
774
+ const configExt = isTs ? "ts" : "js";
775
+ console.log("\n");
776
+ consola$1.info(`Initializing project in ${argTargetDir}
777
+
778
+ `);
779
+ const packageJson = generatePackageJson(isTs);
780
+ fs.writeFileSync(
781
+ path.join(root, "package.json"),
782
+ JSON.stringify(packageJson, null, 2) + "\n"
783
+ );
784
+ consola$1.success("Created ", cyan("package.json"));
785
+ if (isTs) {
786
+ const tsConfig = generateTsConfig();
787
+ fs.writeFileSync(
788
+ path.join(root, "tsconfig.json"),
789
+ JSON.stringify(tsConfig, null, 2) + "\n"
790
+ );
791
+ consola$1.success("Created ", cyan("tsconfig.json"));
792
+ }
793
+ const apibaraConfig = generateApibaraConfig(isTs);
794
+ fs.writeFileSync(
795
+ path.join(root, `apibara.config.${configExt}`),
796
+ apibaraConfig
797
+ );
798
+ consola$1.success("Created ", cyan(`apibara.config.${configExt}`), "\n\n");
799
+ consola$1.ready(green("Project initialized successfully"));
800
+ console.log();
801
+ if (!argNoCreateIndexer) {
802
+ consola$1.info("Let's create an indexer\n");
803
+ await addIndexer({});
804
+ } else {
805
+ const pkgManager = getPackageManager();
806
+ consola$1.info(
807
+ "Run ",
808
+ green(`${pkgManager.name} run install`),
809
+ " to install all dependencies"
810
+ );
811
+ }
812
+ }
813
+
814
+ async function addIndexer({
815
+ argIndexerId,
816
+ argChain,
817
+ argNetwork,
818
+ argStorage,
819
+ argDnaUrl
820
+ }) {
821
+ const configExists = hasApibaraConfig(process.cwd());
822
+ if (!configExists) {
823
+ consola$1.error("No apibara.config found in the current directory.");
824
+ const prompt_initialize = await prompts({
825
+ type: "confirm",
826
+ name: "prompt_initialize",
827
+ message: reset(
828
+ "Do you want to initialize a apibara project here before adding an indexer?"
829
+ )
830
+ });
831
+ if (prompt_initialize.prompt_initialize) {
832
+ await initializeProject({
833
+ argTargetDir: process.cwd(),
834
+ argNoCreateIndexer: true
835
+ });
836
+ } else {
837
+ consola$1.info(
838
+ `Initialize a project with ${cyan("apibara init")} before adding an indexer`
839
+ );
840
+ throw new Error(
841
+ red("\u2716") + " Operation cancelled: No apibara.config found"
842
+ );
843
+ }
844
+ }
845
+ const language = getApibaraConfigLanguage(process.cwd());
846
+ validateIndexerId(argIndexerId, true);
847
+ validateChain(argChain, true);
848
+ validateNetwork(argChain, argNetwork, true);
849
+ validateStorage(argStorage, true);
850
+ validateDnaUrl(argDnaUrl, true);
851
+ const result = await prompts(
852
+ [
853
+ {
854
+ type: argIndexerId ? null : "text",
855
+ name: "prompt_indexerId",
856
+ message: reset("Indexer ID:"),
857
+ initial: argIndexerId ?? "my-indexer",
858
+ validate: (id) => validateIndexerId(id) || "Invalid indexer ID cannot be empty and must be in kebab-case format"
859
+ },
860
+ {
861
+ type: argChain ? null : "select",
862
+ name: "prompt_chain",
863
+ message: reset("Select a chain:"),
864
+ choices: chains.map((chain) => ({
865
+ title: chain.color(chain.display),
866
+ value: chain
867
+ }))
868
+ },
869
+ {
870
+ type: argNetwork ? null : "select",
871
+ name: "prompt_network",
872
+ message: reset("Select a network:"),
873
+ choices: (chain) => [
874
+ ...(chain?.networks ?? chains.find((c) => c.name === argChain)?.networks ?? []).map((network) => ({
875
+ title: network.color(network.display),
876
+ value: network
877
+ })),
878
+ {
879
+ title: cyan("Other"),
880
+ value: {
881
+ color: cyan,
882
+ display: "Other",
883
+ name: "other"
884
+ }
885
+ }
886
+ ]
887
+ },
888
+ {
889
+ type: (network) => {
890
+ if (network || argNetwork) {
891
+ return network?.name === "other" || argNetwork === "other" ? "text" : null;
892
+ }
893
+ return null;
894
+ },
895
+ name: "prompt_dnaUrl",
896
+ message: reset("Enter a DNA URL:"),
897
+ validate: (url) => validateDnaUrl(url) || "Provide a valid DNA Url"
898
+ },
899
+ {
900
+ type: argStorage ? null : "select",
901
+ name: "prompt_storage",
902
+ message: reset("Select a storage:"),
903
+ choices: storages.map((storage) => ({
904
+ title: storage.color(storage.display),
905
+ value: storage
906
+ }))
907
+ }
908
+ ],
909
+ {
910
+ onCancel: () => {
911
+ cancelOperation();
912
+ }
913
+ }
914
+ );
915
+ const {
916
+ prompt_indexerId,
917
+ prompt_chain,
918
+ prompt_network,
919
+ prompt_storage,
920
+ prompt_dnaUrl
921
+ } = result;
922
+ if (!argIndexerId && !prompt_indexerId) {
923
+ throw new Error(red("\u2716") + " Indexer ID is required");
924
+ }
925
+ if (!argChain && !prompt_chain) {
926
+ throw new Error(red("\u2716") + " Chain is required");
927
+ }
928
+ if (!argNetwork && !prompt_network) {
929
+ throw new Error(red("\u2716") + " Network is required");
930
+ }
931
+ const indexerFileId = argIndexerId ?? prompt_indexerId;
932
+ const pkgManager = getPackageManager();
933
+ const options = {
934
+ cwd: process.cwd(),
935
+ indexerFileId,
936
+ indexerId: convertKebabToCamelCase(indexerFileId),
937
+ chain: argChain ?? prompt_chain?.name,
938
+ network: argNetwork ?? prompt_network?.name,
939
+ storage: argStorage ?? prompt_storage?.name,
940
+ dnaUrl: argDnaUrl ?? prompt_dnaUrl,
941
+ language,
942
+ packageManager: pkgManager.name
943
+ };
944
+ updateApibaraConfigFile(options);
945
+ consola$1.success(
946
+ `Updated ${cyan("apibara.config." + (language === "typescript" ? "ts" : "js"))}`
947
+ );
948
+ updatePackageJson(options);
949
+ consola$1.success(`Updated ${cyan("package.json")}`);
950
+ await createIndexerFile(options);
951
+ consola$1.success(
952
+ `Created ${cyan(`${indexerFileId}.indexer.${language === "typescript" ? "ts" : "js"}`)}`
953
+ );
954
+ await createStorageRelatedFiles(options);
955
+ console.log();
956
+ consola$1.info(
957
+ `Before running the indexer, run ${cyan(`${options.packageManager} run install`)}${language === "typescript" ? " & " + cyan(`${options.packageManager} run prepare`) : ""}`
958
+ );
959
+ }
960
+
961
+ export { addIndexer, initializeProject };