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

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 (90) hide show
  1. package/dist/chunks/add.mjs +49 -0
  2. package/dist/chunks/build.mjs +3 -3
  3. package/dist/chunks/dev.mjs +41 -19
  4. package/dist/chunks/init.mjs +37 -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 +127 -134
  11. package/dist/create/index.d.mts +18 -0
  12. package/dist/create/index.d.ts +18 -0
  13. package/dist/create/index.mjs +1025 -0
  14. package/dist/rolldown/index.d.mts +7 -0
  15. package/dist/rolldown/index.d.ts +7 -0
  16. package/dist/rolldown/index.mjs +90 -0
  17. package/dist/runtime/dev.d.ts +3 -0
  18. package/dist/runtime/dev.mjs +58 -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 +64 -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 +46 -0
  27. package/dist/types/index.d.mts +35 -29
  28. package/dist/types/index.d.ts +35 -29
  29. package/package.json +40 -22
  30. package/runtime-meta.d.ts +2 -0
  31. package/runtime-meta.mjs +7 -0
  32. package/src/cli/commands/add.ts +50 -0
  33. package/src/cli/commands/build.ts +5 -3
  34. package/src/cli/commands/dev.ts +50 -19
  35. package/src/cli/commands/init.ts +36 -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 +15 -5
  42. package/src/core/build/dev.ts +44 -22
  43. package/src/core/build/error.ts +9 -15
  44. package/src/core/build/prepare.ts +5 -2
  45. package/src/core/build/prod.ts +24 -15
  46. package/src/core/build/types.ts +12 -95
  47. package/src/core/config/defaults.ts +4 -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 +3 -4
  51. package/src/core/path.ts +11 -0
  52. package/src/core/scan.ts +40 -0
  53. package/src/create/add.ts +239 -0
  54. package/src/create/colors.ts +15 -0
  55. package/src/create/constants.ts +97 -0
  56. package/src/create/index.ts +2 -0
  57. package/src/create/init.ts +178 -0
  58. package/src/create/templates.ts +501 -0
  59. package/src/create/types.ts +34 -0
  60. package/src/create/utils.ts +422 -0
  61. package/src/rolldown/config.ts +83 -0
  62. package/src/rolldown/index.ts +2 -0
  63. package/src/rolldown/plugins/config.ts +13 -0
  64. package/src/rolldown/plugins/indexers.ts +17 -0
  65. package/src/runtime/dev.ts +67 -0
  66. package/src/runtime/index.ts +2 -0
  67. package/src/runtime/internal/app.ts +86 -0
  68. package/src/runtime/internal/logger.ts +70 -0
  69. package/src/runtime/start.ts +53 -0
  70. package/src/types/apibara.ts +8 -0
  71. package/src/types/config.ts +37 -31
  72. package/src/types/hooks.ts +8 -4
  73. package/src/types/index.ts +1 -1
  74. package/src/types/rolldown.ts +5 -0
  75. package/src/types/virtual/config.d.ts +3 -0
  76. package/src/types/virtual/indexers.d.ts +13 -0
  77. package/dist/internal/citty/index.d.mts +0 -1
  78. package/dist/internal/citty/index.d.ts +0 -1
  79. package/dist/internal/citty/index.mjs +0 -1
  80. package/dist/internal/consola/index.d.mts +0 -2
  81. package/dist/internal/consola/index.d.ts +0 -2
  82. package/dist/internal/consola/index.mjs +0 -1
  83. package/dist/rollup/index.d.mts +0 -5
  84. package/dist/rollup/index.d.ts +0 -5
  85. package/dist/rollup/index.mjs +0 -187
  86. package/src/internal/citty/index.ts +0 -1
  87. package/src/internal/consola/index.ts +0 -1
  88. package/src/rollup/config.ts +0 -209
  89. package/src/rollup/index.ts +0 -1
  90. package/src/types/rollup.ts +0 -8
@@ -0,0 +1,1025 @@
1
+ import path, { basename } from 'node:path';
2
+ import consola$1, { consola } from 'consola';
3
+ import prompts from 'prompts';
4
+ import colors from 'picocolors';
5
+ import fs from 'node:fs';
6
+ import { Project, SyntaxKind } from 'ts-morph';
7
+ import * as prettier from 'prettier';
8
+
9
+ const {
10
+ blue,
11
+ blueBright,
12
+ cyan,
13
+ gray,
14
+ green,
15
+ greenBright,
16
+ magenta,
17
+ red,
18
+ redBright,
19
+ reset,
20
+ yellow
21
+ } = colors;
22
+
23
+ const chains = [
24
+ {
25
+ name: "starknet",
26
+ display: "Starknet",
27
+ color: blue,
28
+ networks: [
29
+ { name: "mainnet", display: "Mainnet", color: blue },
30
+ { name: "sepolia", display: "Sepolia", color: yellow }
31
+ ]
32
+ },
33
+ {
34
+ name: "ethereum",
35
+ display: "Ethereum",
36
+ color: green,
37
+ networks: [
38
+ { name: "mainnet", display: "Mainnet", color: blue },
39
+ { name: "sepolia", display: "Sepolia", color: yellow }
40
+ ]
41
+ },
42
+ {
43
+ name: "beaconchain",
44
+ display: "Beacon Chain",
45
+ color: yellow,
46
+ networks: [{ name: "mainnet", display: "Mainnet", color: yellow }]
47
+ }
48
+ ];
49
+ const networks = [
50
+ { name: "mainnet", display: "Mainnet", color: blue },
51
+ { name: "sepolia", display: "Sepolia", color: green },
52
+ { name: "other", display: "Other", color: red }
53
+ ];
54
+ const storages = [
55
+ { name: "postgres", display: "Postgres", color: green },
56
+ { name: "none", display: "None", color: red }
57
+ ];
58
+ const packageVersions = {
59
+ // Required Dependencies
60
+ apibara: "next",
61
+ "@apibara/indexer": "next",
62
+ "@apibara/protocol": "next",
63
+ // Chain Dependencies
64
+ "@apibara/evm": "next",
65
+ "@apibara/beaconchain": "next",
66
+ "@apibara/starknet": "next",
67
+ // Storage Dependencies
68
+ "@apibara/plugin-drizzle": "next",
69
+ "@apibara/plugin-mongo": "next",
70
+ "@apibara/plugin-sqlite": "next",
71
+ // Postgres Dependencies
72
+ "@electric-sql/pglite": "^0.2.17",
73
+ "drizzle-orm": "^0.37.0",
74
+ pg: "^8.13.1",
75
+ "@types/pg": "^8.11.10",
76
+ "drizzle-kit": "^0.29.0",
77
+ // Typescript Dependencies
78
+ typescript: "^5.6.2",
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
+ async function formatFile(path2) {
373
+ const file = fs.readFileSync(path2, "utf8");
374
+ const formatted = await prettier.format(file, {
375
+ filepath: path2,
376
+ tabWidth: 2
377
+ });
378
+ fs.writeFileSync(path2, formatted);
379
+ }
380
+
381
+ function generatePackageJson(isTypeScript) {
382
+ return {
383
+ name: "apibara-app",
384
+ version: "0.1.0",
385
+ private: true,
386
+ type: "module",
387
+ scripts: {
388
+ ...isTypeScript && { prepare: "apibara prepare" },
389
+ dev: "apibara dev",
390
+ start: "apibara start",
391
+ build: "apibara build",
392
+ ...isTypeScript && { typecheck: "tsc --noEmit" }
393
+ },
394
+ dependencies: {
395
+ "@apibara/indexer": packageVersions["@apibara/indexer"],
396
+ "@apibara/protocol": packageVersions["@apibara/protocol"],
397
+ apibara: packageVersions.apibara
398
+ },
399
+ devDependencies: {
400
+ ...isTypeScript && {
401
+ "@types/node": packageVersions["@types/node"],
402
+ typescript: packageVersions.typescript
403
+ }
404
+ }
405
+ };
406
+ }
407
+ function generateTsConfig() {
408
+ return {
409
+ $schema: "https://json.schemastore.org/tsconfig",
410
+ display: "Default",
411
+ compilerOptions: {
412
+ forceConsistentCasingInFileNames: true,
413
+ target: "ES2022",
414
+ lib: ["ESNext"],
415
+ module: "ESNext",
416
+ moduleResolution: "bundler",
417
+ skipLibCheck: true,
418
+ types: ["node"],
419
+ noEmit: true,
420
+ strict: true,
421
+ baseUrl: "."
422
+ },
423
+ include: [".", "./.apibara/types"],
424
+ exclude: ["node_modules"]
425
+ };
426
+ }
427
+ function generateApibaraConfig(isTypeScript) {
428
+ return `import { defineConfig } from "apibara/config";
429
+
430
+ export default defineConfig({
431
+ runtimeConfig: {},
432
+ });
433
+ `;
434
+ }
435
+ function generateIndexer({
436
+ indexerId,
437
+ storage,
438
+ chain,
439
+ language
440
+ }) {
441
+ return `import { defineIndexer } from "@apibara/indexer";
442
+ import { useLogger } from "@apibara/indexer/plugins";
443
+ ${storage === "postgres" ? `import { drizzleStorage } from "@apibara/plugin-drizzle";` : ""}
444
+ ${storage === "postgres" ? `import { drizzle } from "@apibara/plugin-drizzle";` : ""}
445
+ ${chain === "ethereum" ? `import { EvmStream } from "@apibara/evm";` : chain === "beaconchain" ? `import { BeaconChainStream } from "@apibara/beaconchain";` : chain === "starknet" ? `import { StarknetStream } from "@apibara/starknet";` : ""}
446
+ ${language === "typescript" ? `import type { ApibaraRuntimeConfig } from "apibara/types";` : ""}
447
+ ${storage === "postgres" ? `import * as schema from "../lib/schema";` : ""}
448
+
449
+
450
+ export default function (runtimeConfig${language === "typescript" ? ": ApibaraRuntimeConfig" : ""}) {
451
+ const indexerId = "${indexerId}";
452
+ const { startingBlock, streamUrl${storage === "postgres" ? ", postgresConnectionString" : ""} } = runtimeConfig[indexerId];
453
+ ${storage === "postgres" ? `const db = drizzle({
454
+ schema,
455
+ connectionString: postgresConnectionString,
456
+ });` : ""}
457
+
458
+ return defineIndexer(${chain === "ethereum" ? "EvmStream" : chain === "beaconchain" ? "BeaconChainStream" : chain === "starknet" ? "StarknetStream" : ""})({
459
+ streamUrl,
460
+ finality: "accepted",
461
+ startingBlock: BigInt(startingBlock),
462
+ filter: {
463
+ header: "always",
464
+ },
465
+ plugins: [${storage === "postgres" ? "drizzleStorage({ db, migrate: { migrationsFolder: './drizzle' } })" : ""}],
466
+ async transform({ endCursor, finality }) {
467
+ const logger = useLogger();
468
+
469
+ logger.info(
470
+ "Transforming block | orderKey: ",
471
+ endCursor?.orderKey,
472
+ " | finality: ",
473
+ finality
474
+ );
475
+
476
+ ${storage === "postgres" ? `// Example snippet to insert data into db using drizzle with postgres
477
+ // const { db: database } = useDrizzleStorage();
478
+
479
+ // await database.insert(schema.cursorTable).values({
480
+ // endCursor: Number(endCursor?.orderKey),
481
+ // uniqueKey: \`\${endCursor?.uniqueKey}\`,
482
+ // });` : ""}
483
+ },
484
+ });
485
+ }
486
+ `;
487
+ }
488
+ async function createIndexerFile(options) {
489
+ const indexerFilePath = path.join(
490
+ options.cwd,
491
+ "indexers",
492
+ `${options.indexerFileId}.indexer.${options.language === "typescript" ? "ts" : "js"}`
493
+ );
494
+ const { exists, overwrite } = await checkFileExists(indexerFilePath, {
495
+ askPrompt: true
496
+ });
497
+ if (exists && !overwrite)
498
+ return;
499
+ const indexerContent = generateIndexer(options);
500
+ fs.mkdirSync(path.dirname(indexerFilePath), { recursive: true });
501
+ fs.writeFileSync(indexerFilePath, indexerContent);
502
+ await formatFile(indexerFilePath);
503
+ }
504
+ async function updatePackageJson({
505
+ cwd,
506
+ chain,
507
+ storage,
508
+ language
509
+ }) {
510
+ const packageJsonPath = path.join(cwd, "package.json");
511
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
512
+ if (chain === "ethereum") {
513
+ packageJson.dependencies["@apibara/evm"] = packageVersions["@apibara/evm"];
514
+ } else if (chain === "beaconchain") {
515
+ packageJson.dependencies["@apibara/beaconchain"] = packageVersions["@apibara/beaconchain"];
516
+ } else if (chain === "starknet") {
517
+ packageJson.dependencies["@apibara/starknet"] = packageVersions["@apibara/starknet"];
518
+ }
519
+ if (storage === "postgres") {
520
+ packageJson.scripts["drizzle:generate"] = "drizzle-kit generate";
521
+ packageJson.scripts["drizzle:migrate"] = "drizzle-kit migrate";
522
+ packageJson.dependencies["@apibara/plugin-drizzle"] = packageVersions["@apibara/plugin-drizzle"];
523
+ packageJson.dependencies["drizzle-orm"] = packageVersions["drizzle-orm"];
524
+ packageJson.dependencies["@electric-sql/pglite"] = packageVersions["@electric-sql/pglite"];
525
+ packageJson.dependencies["drizzle-kit"] = packageVersions["drizzle-kit"];
526
+ packageJson.dependencies["pg"] = packageVersions["pg"];
527
+ if (language === "typescript") {
528
+ packageJson.devDependencies["@types/pg"] = packageVersions["@types/pg"];
529
+ }
530
+ }
531
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
532
+ await formatFile(packageJsonPath);
533
+ }
534
+ async function updateApibaraConfigFile({
535
+ indexerId,
536
+ cwd,
537
+ chain,
538
+ storage,
539
+ language,
540
+ network,
541
+ dnaUrl
542
+ }) {
543
+ const pathToConfig = path.join(
544
+ cwd,
545
+ `apibara.config.${language === "typescript" ? "ts" : "js"}`
546
+ );
547
+ const runtimeConfigString = `{
548
+ startingBlock: 0,
549
+ streamUrl: "${dnaUrl ?? getDnaUrl(chain, network)}"${storage === "postgres" ? `,
550
+ postgresConnectionString: process.env["POSTGRES_CONNECTION_STRING"] ?? "memory://${indexerId}"` : ""}}`;
551
+ const project = new Project();
552
+ const sourceFile = project.addSourceFileAtPath(pathToConfig);
553
+ const defineConfigCall = sourceFile.getFirstDescendantByKind(
554
+ SyntaxKind.CallExpression
555
+ );
556
+ if (!defineConfigCall)
557
+ return;
558
+ const configObjectExpression = defineConfigCall.getArguments()[0];
559
+ const runtimeConfigObject = configObjectExpression.getProperty("runtimeConfig");
560
+ if (!runtimeConfigObject) {
561
+ configObjectExpression.addPropertyAssignment({
562
+ name: "runtimeConfig",
563
+ initializer: `{
564
+ "${indexerId}": ${runtimeConfigString}
565
+ }`
566
+ });
567
+ } else {
568
+ const runtimeConfigProp = runtimeConfigObject.asKindOrThrow(
569
+ SyntaxKind.PropertyAssignment
570
+ );
571
+ const runtimeConfigObj = runtimeConfigProp.getInitializerOrThrow().asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
572
+ runtimeConfigObj.addPropertyAssignment({
573
+ name: `"${indexerId}"`,
574
+ initializer: runtimeConfigString
575
+ });
576
+ }
577
+ sourceFile.saveSync();
578
+ await formatFile(pathToConfig);
579
+ }
580
+ async function createDrizzleStorageFiles(options) {
581
+ const { cwd, language, storage, indexerId } = 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.${fileExtension}",
596
+ out: "./drizzle",
597
+ dialect: "postgresql",
598
+ dbCredentials: {
599
+ url: process.env["POSTGRES_CONNECTION_STRING"] ?? "memory://${indexerId}",
600
+ },
601
+ }${language === "typescript" ? " satisfies Config" : ""};`;
602
+ fs.writeFileSync(drizzleConfigPath, drizzleConfigContent);
603
+ await formatFile(drizzleConfigPath);
604
+ consola.success(`Created ${cyan(drizzleConfigFileName)}`);
605
+ }
606
+ const schemaFileName = `schema.${fileExtension}`;
607
+ const schemaPath = path.join(cwd, "lib", schemaFileName);
608
+ const { exists: schemaExists, overwrite: schemaOverwrite } = await checkFileExists(schemaPath, {
609
+ askPrompt: true,
610
+ allowIgnore: true,
611
+ fileName: `lib/${schemaFileName}`
612
+ });
613
+ if (!schemaExists || schemaOverwrite) {
614
+ const schemaContent = `// --- Add your pg table schemas here ----
615
+
616
+ // import { bigint, pgTable, text, uuid } from "drizzle-orm/pg-core";
617
+
618
+ // export const cursorTable = pgTable("cursor_table", {
619
+ // id: uuid("id").primaryKey().defaultRandom(),
620
+ // endCursor: bigint("end_cursor", { mode: "number" }),
621
+ // uniqueKey: text("unique_key"),
622
+ // });
623
+
624
+ export {};
625
+ `;
626
+ fs.mkdirSync(path.dirname(schemaPath), { recursive: true });
627
+ fs.writeFileSync(schemaPath, schemaContent);
628
+ await formatFile(schemaPath);
629
+ consola.success(`Created ${cyan("lib/schema.ts")}`);
630
+ }
631
+ console.log("\n");
632
+ if (!schemaExists || schemaOverwrite) {
633
+ consola.info(
634
+ `Make sure to export your pgTables in ${cyan(`lib/${schemaFileName}`)}`
635
+ );
636
+ console.log();
637
+ consola.info(`${magenta("Example:")}
638
+
639
+ ${yellow(`
640
+ \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
641
+ \u2502 lib/schema.ts \u2502
642
+ \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
643
+
644
+ import { bigint, pgTable, text, uuid } from "drizzle-orm/pg-core";
645
+
646
+ export const cursorTable = pgTable("cursor_table", {
647
+ id: uuid("id").primaryKey().defaultRandom(),
648
+ endCursor: bigint("end_cursor", { mode: "number" }),
649
+ uniqueKey: text("unique_key"),
650
+ });`)}`);
651
+ console.log("\n");
652
+ }
653
+ consola.info(
654
+ `Run ${green(`${options.packageManager}${options.packageManager === "npm" ? " run" : ""} drizzle:generate`)} & ${green(`${options.packageManager}${options.packageManager === "npm" ? " run" : ""} drizzle:migrate`)} to generate and apply migrations.`
655
+ );
656
+ }
657
+ async function createStorageRelatedFiles(options) {
658
+ const { storage } = options;
659
+ if (storage === "postgres") {
660
+ await createDrizzleStorageFiles(options);
661
+ }
662
+ }
663
+ const gitIgnoreItems = [
664
+ {
665
+ isRecommended: false,
666
+ value: "node_modules"
667
+ },
668
+ {
669
+ isRecommended: false,
670
+ value: "dist"
671
+ },
672
+ {
673
+ isRecommended: true,
674
+ description: "build and dev files of apibara",
675
+ value: ".apibara"
676
+ },
677
+ {
678
+ isRecommended: false,
679
+ value: ".env"
680
+ },
681
+ {
682
+ isRecommended: false,
683
+ description: "for mac users",
684
+ value: ".DS_Store"
685
+ }
686
+ ];
687
+ async function createGitIgnoreFile(cwd) {
688
+ const gitIgnorePath = path.join(cwd, ".gitignore");
689
+ if (fs.existsSync(gitIgnorePath)) {
690
+ const result = await prompts([
691
+ {
692
+ type: "select",
693
+ name: "overwrite",
694
+ message: `${cyan(".gitignore")} already exists. Please choose how to proceed:`,
695
+ initial: 0,
696
+ choices: [
697
+ {
698
+ title: "Choose items to append in your .gitignore",
699
+ value: "append"
700
+ },
701
+ {
702
+ title: "Keep original",
703
+ value: "ignore"
704
+ },
705
+ {
706
+ title: "Overwrite",
707
+ value: "overwrite"
708
+ }
709
+ ]
710
+ },
711
+ {
712
+ type: (overwrite2) => overwrite2 === "append" ? "multiselect" : null,
713
+ name: "ignoreItems",
714
+ message: "Choose items to append in your .gitignore",
715
+ choices: gitIgnoreItems.map((item) => ({
716
+ title: `${yellow(item.value)}${item.description ? ` - ${item.description}` : ""}${item.isRecommended ? ` ${green("(recommended)")}` : ""}`,
717
+ value: item.value
718
+ }))
719
+ }
720
+ ]);
721
+ const { overwrite, ignoreItems } = result;
722
+ if (overwrite === "append" && ignoreItems.length > 0) {
723
+ const gitIgnoreContent = fs.readFileSync(gitIgnorePath, "utf8");
724
+ fs.writeFileSync(
725
+ gitIgnorePath,
726
+ `${gitIgnoreContent}
727
+ ${result.ignoreItems.join("\n")}`
728
+ );
729
+ consola.success(`Updated ${cyan(".gitignore")}`);
730
+ return;
731
+ }
732
+ if (overwrite === "overwrite") {
733
+ fs.writeFileSync(
734
+ gitIgnorePath,
735
+ gitIgnoreItems.map((item) => item.value).join("\n")
736
+ );
737
+ consola.success(`Updated ${cyan(".gitignore")}`);
738
+ return;
739
+ }
740
+ }
741
+ fs.writeFileSync(
742
+ gitIgnorePath,
743
+ gitIgnoreItems.map((item) => item.value).join("\n")
744
+ );
745
+ consola.success(`Created ${cyan(".gitignore")}`);
746
+ }
747
+
748
+ async function initializeProject({
749
+ argTargetDir,
750
+ argLanguage,
751
+ argNoCreateIndexer
752
+ }) {
753
+ const cwd = process.cwd();
754
+ validateLanguage(argLanguage, true);
755
+ console.log();
756
+ const result = await prompts(
757
+ [
758
+ {
759
+ type: () => argTargetDir && (!fs.existsSync(argTargetDir) || isEmpty(argTargetDir)) ? null : "select",
760
+ name: "overwrite",
761
+ message: () => (argTargetDir === "." ? "Current directory" : `Target directory "${argTargetDir}"`) + " is not empty. Please choose how to proceed:",
762
+ initial: 0,
763
+ choices: [
764
+ {
765
+ title: "Cancel operation",
766
+ value: "no"
767
+ },
768
+ {
769
+ title: "Remove existing files and continue",
770
+ value: "yes"
771
+ },
772
+ {
773
+ title: "Ignore files and continue",
774
+ value: "ignore"
775
+ }
776
+ ],
777
+ hint: "\nCurrent Working Directory: " + cwd
778
+ },
779
+ {
780
+ type: (_, { overwrite: overwrite2 }) => {
781
+ if (overwrite2 === "no") {
782
+ cancelOperation();
783
+ }
784
+ return null;
785
+ },
786
+ name: "overwriteChecker"
787
+ },
788
+ {
789
+ type: argLanguage ? null : "select",
790
+ name: "prompt_language",
791
+ message: "Select a language:",
792
+ choices: [
793
+ {
794
+ title: "Typescript",
795
+ value: "typescript"
796
+ },
797
+ {
798
+ title: "Javascript",
799
+ value: "javascript"
800
+ }
801
+ ]
802
+ }
803
+ ],
804
+ {
805
+ onCancel: () => {
806
+ cancelOperation();
807
+ }
808
+ }
809
+ );
810
+ const { overwrite, prompt_language } = result;
811
+ const root = path.join(cwd, argTargetDir);
812
+ if (overwrite === "yes") {
813
+ emptyDir(root);
814
+ } else if (!fs.existsSync(root)) {
815
+ fs.mkdirSync(root, { recursive: true });
816
+ }
817
+ const lang = argLanguage ? getLanguageFromAlias(argLanguage) : prompt_language;
818
+ const isTs = lang === "typescript";
819
+ const configExt = isTs ? "ts" : "js";
820
+ console.log("\n");
821
+ consola$1.info(`Initializing project in ${argTargetDir}
822
+
823
+ `);
824
+ const packageJsonPath = path.join(root, "package.json");
825
+ const packageJson = generatePackageJson(isTs);
826
+ fs.writeFileSync(
827
+ packageJsonPath,
828
+ JSON.stringify(packageJson, null, 2) + "\n"
829
+ );
830
+ await formatFile(packageJsonPath);
831
+ consola$1.success("Created", cyan("package.json"));
832
+ if (isTs) {
833
+ const tsConfigPath = path.join(root, "tsconfig.json");
834
+ const tsConfig = generateTsConfig();
835
+ fs.writeFileSync(tsConfigPath, JSON.stringify(tsConfig, null, 2) + "\n");
836
+ await formatFile(tsConfigPath);
837
+ consola$1.success("Created", cyan("tsconfig.json"));
838
+ }
839
+ const apibaraConfigPath = path.join(root, `apibara.config.${configExt}`);
840
+ const apibaraConfig = generateApibaraConfig();
841
+ fs.writeFileSync(apibaraConfigPath, apibaraConfig);
842
+ await formatFile(apibaraConfigPath);
843
+ consola$1.success("Created", cyan(`apibara.config.${configExt}`));
844
+ const indexersDir = path.join(root, "indexers");
845
+ if (!fs.existsSync(indexersDir)) {
846
+ fs.mkdirSync(indexersDir, { recursive: true });
847
+ consola$1.success(`Created ${cyan("indexers")} directory`);
848
+ }
849
+ await createGitIgnoreFile(root);
850
+ console.log("\n");
851
+ consola$1.ready(green("Project initialized successfully"));
852
+ console.log();
853
+ if (!argNoCreateIndexer) {
854
+ consola$1.info("Let's create an indexer\n");
855
+ await addIndexer({ argRootDir: argTargetDir });
856
+ } else {
857
+ const pkgManager = getPackageManager();
858
+ consola$1.info(
859
+ "Run ",
860
+ green(`${pkgManager.name} install`),
861
+ " to install all dependencies"
862
+ );
863
+ }
864
+ }
865
+
866
+ async function addIndexer({
867
+ argIndexerId,
868
+ argChain,
869
+ argNetwork,
870
+ argStorage,
871
+ argDnaUrl,
872
+ argRootDir
873
+ }) {
874
+ const cwd = path.join(process.cwd(), argRootDir ?? ".");
875
+ const configExists = hasApibaraConfig(cwd);
876
+ if (!configExists) {
877
+ consola$1.error("No apibara.config found in the current directory.");
878
+ const prompt_initialize = await prompts({
879
+ type: "confirm",
880
+ name: "prompt_initialize",
881
+ message: reset(
882
+ "Do you want to initialize a apibara project here before adding an indexer?"
883
+ )
884
+ });
885
+ if (prompt_initialize.prompt_initialize) {
886
+ await initializeProject({
887
+ argTargetDir: process.cwd(),
888
+ argNoCreateIndexer: true
889
+ });
890
+ } else {
891
+ consola$1.info(
892
+ `Initialize a project with ${cyan("apibara init")} before adding an indexer`
893
+ );
894
+ throw new Error(
895
+ red("\u2716") + " Operation cancelled: No apibara.config found"
896
+ );
897
+ }
898
+ }
899
+ const language = getApibaraConfigLanguage(cwd);
900
+ validateIndexerId(argIndexerId, true);
901
+ validateChain(argChain, true);
902
+ validateNetwork(argChain, argNetwork, true);
903
+ validateStorage(argStorage, true);
904
+ validateDnaUrl(argDnaUrl, true);
905
+ const result = await prompts(
906
+ [
907
+ {
908
+ type: argIndexerId ? null : "text",
909
+ name: "prompt_indexerId",
910
+ message: reset("Indexer ID:"),
911
+ initial: argIndexerId ?? "my-indexer",
912
+ validate: (id) => validateIndexerId(id) ? checkFileExists(
913
+ path.join(
914
+ cwd,
915
+ "indexers",
916
+ `${id}.indexer.${language === "typescript" ? "ts" : "js"}`
917
+ )
918
+ ).then(
919
+ ({ exists }) => exists ? `Indexer ${cyan(`${id}.indexer.${language === "typescript" ? "ts" : "js"}`)} already exists` : true
920
+ ) : "Invalid indexer ID, it cannot be empty and must be in kebab-case format"
921
+ },
922
+ {
923
+ type: argChain ? null : "select",
924
+ name: "prompt_chain",
925
+ message: reset("Select a chain:"),
926
+ choices: chains.map((chain) => ({
927
+ title: chain.color(chain.display),
928
+ value: chain
929
+ }))
930
+ },
931
+ {
932
+ type: argNetwork ? null : "select",
933
+ name: "prompt_network",
934
+ message: reset("Select a network:"),
935
+ choices: (chain) => [
936
+ ...(chain?.networks ?? chains.find((c) => c.name === argChain)?.networks ?? []).map((network) => ({
937
+ title: network.color(network.display),
938
+ value: network
939
+ })),
940
+ {
941
+ title: cyan("Other"),
942
+ value: {
943
+ color: cyan,
944
+ display: "Other",
945
+ name: "other"
946
+ }
947
+ }
948
+ ]
949
+ },
950
+ {
951
+ type: (network) => {
952
+ if (network || argNetwork) {
953
+ return network?.name === "other" || argNetwork === "other" ? "text" : null;
954
+ }
955
+ return null;
956
+ },
957
+ name: "prompt_dnaUrl",
958
+ message: reset("Enter a DNA URL:"),
959
+ validate: (url) => validateDnaUrl(url) || "Provide a valid DNA Url"
960
+ },
961
+ {
962
+ type: argStorage ? null : "select",
963
+ name: "prompt_storage",
964
+ message: reset("Select a storage:"),
965
+ choices: storages.map((storage) => ({
966
+ title: storage.color(storage.display),
967
+ value: storage
968
+ }))
969
+ }
970
+ ],
971
+ {
972
+ onCancel: () => {
973
+ cancelOperation();
974
+ }
975
+ }
976
+ );
977
+ const {
978
+ prompt_indexerId,
979
+ prompt_chain,
980
+ prompt_network,
981
+ prompt_storage,
982
+ prompt_dnaUrl
983
+ } = result;
984
+ if (!argIndexerId && !prompt_indexerId) {
985
+ throw new Error(red("\u2716") + " Indexer ID is required");
986
+ }
987
+ if (!argChain && !prompt_chain) {
988
+ throw new Error(red("\u2716") + " Chain is required");
989
+ }
990
+ if (!argNetwork && !prompt_network) {
991
+ throw new Error(red("\u2716") + " Network is required");
992
+ }
993
+ const indexerFileId = argIndexerId ?? prompt_indexerId;
994
+ const pkgManager = getPackageManager();
995
+ const options = {
996
+ cwd,
997
+ indexerFileId,
998
+ indexerId: convertKebabToCamelCase(indexerFileId),
999
+ chain: argChain ?? prompt_chain?.name,
1000
+ network: argNetwork ?? prompt_network?.name,
1001
+ storage: argStorage ?? prompt_storage?.name,
1002
+ dnaUrl: argDnaUrl ?? prompt_dnaUrl,
1003
+ language,
1004
+ packageManager: pkgManager.name
1005
+ };
1006
+ await updateApibaraConfigFile(options);
1007
+ consola$1.success(
1008
+ `Updated ${cyan("apibara.config." + (language === "typescript" ? "ts" : "js"))}`
1009
+ );
1010
+ await updatePackageJson(options);
1011
+ consola$1.success(`Updated ${cyan("package.json")}`);
1012
+ await createIndexerFile(options);
1013
+ consola$1.success(
1014
+ `Created ${cyan(`${indexerFileId}.indexer.${language === "typescript" ? "ts" : "js"}`)}`
1015
+ );
1016
+ await createStorageRelatedFiles(options);
1017
+ console.log();
1018
+ const baseCommand = `${options.packageManager} install`;
1019
+ const tsCommand = `${baseCommand} && ${options.packageManager} run prepare`;
1020
+ consola$1.info(
1021
+ `Before running the indexer, run ${cyan(language === "typescript" ? tsCommand : baseCommand)}`
1022
+ );
1023
+ }
1024
+
1025
+ export { addIndexer, initializeProject };