@tinybirdco/sdk 0.0.30 → 0.0.31

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 (76) hide show
  1. package/README.md +29 -4
  2. package/dist/cli/commands/branch.js +5 -5
  3. package/dist/cli/commands/branch.js.map +1 -1
  4. package/dist/cli/commands/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.js +2 -2
  6. package/dist/cli/commands/build.js.map +1 -1
  7. package/dist/cli/commands/build.test.d.ts +2 -0
  8. package/dist/cli/commands/build.test.d.ts.map +1 -0
  9. package/dist/cli/commands/build.test.js +266 -0
  10. package/dist/cli/commands/build.test.js.map +1 -0
  11. package/dist/cli/commands/clear.d.ts.map +1 -1
  12. package/dist/cli/commands/clear.js +2 -2
  13. package/dist/cli/commands/clear.js.map +1 -1
  14. package/dist/cli/commands/deploy.js +2 -2
  15. package/dist/cli/commands/deploy.js.map +1 -1
  16. package/dist/cli/commands/dev.js +12 -12
  17. package/dist/cli/commands/dev.js.map +1 -1
  18. package/dist/cli/commands/info.js +2 -2
  19. package/dist/cli/commands/info.js.map +1 -1
  20. package/dist/cli/commands/info.test.js +11 -13
  21. package/dist/cli/commands/info.test.js.map +1 -1
  22. package/dist/cli/commands/init.d.ts.map +1 -1
  23. package/dist/cli/commands/init.js +44 -26
  24. package/dist/cli/commands/init.js.map +1 -1
  25. package/dist/cli/commands/init.test.js +44 -25
  26. package/dist/cli/commands/init.test.js.map +1 -1
  27. package/dist/cli/commands/login.d.ts.map +1 -1
  28. package/dist/cli/commands/login.js +7 -6
  29. package/dist/cli/commands/login.js.map +1 -1
  30. package/dist/cli/commands/login.test.js +1 -1
  31. package/dist/cli/commands/login.test.js.map +1 -1
  32. package/dist/cli/commands/preview.d.ts.map +1 -1
  33. package/dist/cli/commands/preview.js +2 -2
  34. package/dist/cli/commands/preview.js.map +1 -1
  35. package/dist/cli/config-loader.d.ts +18 -0
  36. package/dist/cli/config-loader.d.ts.map +1 -0
  37. package/dist/cli/config-loader.js +57 -0
  38. package/dist/cli/config-loader.js.map +1 -0
  39. package/dist/cli/config-types.d.ts +28 -0
  40. package/dist/cli/config-types.d.ts.map +1 -0
  41. package/dist/cli/config-types.js +8 -0
  42. package/dist/cli/config-types.js.map +1 -0
  43. package/dist/cli/config.d.ts +63 -29
  44. package/dist/cli/config.d.ts.map +1 -1
  45. package/dist/cli/config.js +139 -43
  46. package/dist/cli/config.js.map +1 -1
  47. package/dist/cli/config.test.js +73 -9
  48. package/dist/cli/config.test.js.map +1 -1
  49. package/dist/cli/index.js +4 -0
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/client/base.d.ts.map +1 -1
  52. package/dist/client/base.js +21 -9
  53. package/dist/client/base.js.map +1 -1
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/package.json +1 -1
  57. package/src/cli/commands/branch.ts +5 -5
  58. package/src/cli/commands/build.test.ts +310 -0
  59. package/src/cli/commands/build.ts +2 -2
  60. package/src/cli/commands/clear.ts +2 -2
  61. package/src/cli/commands/deploy.ts +2 -2
  62. package/src/cli/commands/dev.ts +12 -12
  63. package/src/cli/commands/info.test.ts +11 -13
  64. package/src/cli/commands/info.ts +2 -2
  65. package/src/cli/commands/init.test.ts +53 -37
  66. package/src/cli/commands/init.ts +49 -30
  67. package/src/cli/commands/login.test.ts +1 -1
  68. package/src/cli/commands/login.ts +7 -6
  69. package/src/cli/commands/preview.ts +2 -2
  70. package/src/cli/config-loader.ts +87 -0
  71. package/src/cli/config-types.ts +29 -0
  72. package/src/cli/config.test.ts +95 -8
  73. package/src/cli/config.ts +179 -70
  74. package/src/cli/index.ts +3 -0
  75. package/src/client/base.ts +33 -16
  76. package/src/index.ts +4 -0
@@ -3,7 +3,7 @@ import { runInfo } from "./info.js";
3
3
 
4
4
  // Mock the config module
5
5
  vi.mock("../config.js", () => ({
6
- loadConfig: vi.fn(),
6
+ loadConfigAsync: vi.fn(),
7
7
  LOCAL_BASE_URL: "http://localhost:7181",
8
8
  }));
9
9
 
@@ -31,7 +31,7 @@ vi.mock("../../api/local.js", () => ({
31
31
  }));
32
32
 
33
33
  // Import mocked functions
34
- import { loadConfig } from "../config.js";
34
+ import { loadConfigAsync } from "../config.js";
35
35
  import { getWorkspace } from "../../api/workspaces.js";
36
36
  import { listBranches, getBranch } from "../../api/branches.js";
37
37
  import { getDashboardUrl, getBranchDashboardUrl, getLocalDashboardUrl } from "../../api/dashboard.js";
@@ -42,7 +42,7 @@ import {
42
42
  getLocalWorkspaceName,
43
43
  } from "../../api/local.js";
44
44
 
45
- const mockedLoadConfig = vi.mocked(loadConfig);
45
+ const mockedLoadConfigAsync = vi.mocked(loadConfigAsync);
46
46
  const mockedGetWorkspace = vi.mocked(getWorkspace);
47
47
  const mockedListBranches = vi.mocked(listBranches);
48
48
  const mockedGetBranch = vi.mocked(getBranch);
@@ -61,9 +61,7 @@ describe("Info Command", () => {
61
61
 
62
62
  describe("config loading", () => {
63
63
  it("returns error when config loading fails", async () => {
64
- mockedLoadConfig.mockImplementation(() => {
65
- throw new Error("No tinybird.json found");
66
- });
64
+ mockedLoadConfigAsync.mockRejectedValue(new Error("No tinybird.json found"));
67
65
 
68
66
  const result = await runInfo();
69
67
 
@@ -95,7 +93,7 @@ describe("Info Command", () => {
95
93
  };
96
94
 
97
95
  beforeEach(() => {
98
- mockedLoadConfig.mockReturnValue(mockConfig);
96
+ mockedLoadConfigAsync.mockResolvedValue(mockConfig);
99
97
  mockedGetWorkspace.mockResolvedValue(mockWorkspace);
100
98
  mockedListBranches.mockResolvedValue([]);
101
99
  mockedGetDashboardUrl.mockReturnValue("https://cloud.tinybird.co/gcp/europe-west3/test-workspace");
@@ -161,7 +159,7 @@ describe("Info Command", () => {
161
159
  });
162
160
 
163
161
  it("does not return branch info when on main branch", async () => {
164
- mockedLoadConfig.mockReturnValue({
162
+ mockedLoadConfigAsync.mockResolvedValue({
165
163
  ...mockConfig,
166
164
  gitBranch: "main",
167
165
  tinybirdBranch: null,
@@ -212,7 +210,7 @@ describe("Info Command", () => {
212
210
  };
213
211
 
214
212
  beforeEach(() => {
215
- mockedLoadConfig.mockReturnValue(mockConfig);
213
+ mockedLoadConfigAsync.mockResolvedValue(mockConfig);
216
214
  mockedGetWorkspace.mockResolvedValue(mockWorkspace);
217
215
  mockedGetDashboardUrl.mockReturnValue("https://cloud.tinybird.co/gcp/europe-west3/test-workspace");
218
216
  });
@@ -285,7 +283,7 @@ describe("Info Command", () => {
285
283
 
286
284
  describe("error handling", () => {
287
285
  it("returns error when workspace fetch fails", async () => {
288
- mockedLoadConfig.mockReturnValue({
286
+ mockedLoadConfigAsync.mockResolvedValue({
289
287
  cwd: "/test",
290
288
  configPath: "/test/tinybird.json",
291
289
  devMode: "branch" as const,
@@ -306,7 +304,7 @@ describe("Info Command", () => {
306
304
  });
307
305
 
308
306
  it("handles branch fetch error gracefully", async () => {
309
- mockedLoadConfig.mockReturnValue({
307
+ mockedLoadConfigAsync.mockResolvedValue({
310
308
  cwd: "/test",
311
309
  configPath: "/test/tinybird.json",
312
310
  devMode: "branch" as const,
@@ -336,7 +334,7 @@ describe("Info Command", () => {
336
334
  });
337
335
 
338
336
  it("handles branches list fetch error gracefully", async () => {
339
- mockedLoadConfig.mockReturnValue({
337
+ mockedLoadConfigAsync.mockResolvedValue({
340
338
  cwd: "/test",
341
339
  configPath: "/test/tinybird.json",
342
340
  devMode: "branch" as const,
@@ -365,7 +363,7 @@ describe("Info Command", () => {
365
363
  });
366
364
 
367
365
  it("handles local workspace fetch error gracefully", async () => {
368
- mockedLoadConfig.mockReturnValue({
366
+ mockedLoadConfigAsync.mockResolvedValue({
369
367
  cwd: "/test",
370
368
  configPath: "/test/tinybird.json",
371
369
  devMode: "local" as const,
@@ -2,7 +2,7 @@
2
2
  * Info command - shows information about the current project and workspace
3
3
  */
4
4
 
5
- import { loadConfig, LOCAL_BASE_URL, type ResolvedConfig } from "../config.js";
5
+ import { loadConfigAsync, LOCAL_BASE_URL, type ResolvedConfig } from "../config.js";
6
6
  import { getWorkspace, type TinybirdWorkspace } from "../../api/workspaces.js";
7
7
  import { listBranches, getBranch, type TinybirdBranch } from "../../api/branches.js";
8
8
  import { getDashboardUrl, getBranchDashboardUrl, getLocalDashboardUrl } from "../../api/dashboard.js";
@@ -125,7 +125,7 @@ export async function runInfo(
125
125
  // Load config
126
126
  let config: ResolvedConfig;
127
127
  try {
128
- config = loadConfig(cwd);
128
+ config = await loadConfigAsync(cwd);
129
129
  } catch (error) {
130
130
  return {
131
131
  success: false,
@@ -46,45 +46,45 @@ describe("Init Command", () => {
46
46
  ).toBe(true);
47
47
  });
48
48
 
49
- it("creates tinybird.json with correct include path for lib/tinybird.ts", async () => {
49
+ it("creates tinybird.config.json with correct include path for lib/tinybird.ts", async () => {
50
50
  const result = await runInit({ cwd: tempDir, skipLogin: true, devMode: "branch", clientPath: "lib/tinybird.ts" });
51
51
 
52
52
  expect(result.success).toBe(true);
53
- expect(result.created).toContain("tinybird.json");
53
+ expect(result.created).toContain("tinybird.config.json");
54
54
 
55
55
  const config = JSON.parse(
56
- fs.readFileSync(path.join(tempDir, "tinybird.json"), "utf-8")
56
+ fs.readFileSync(path.join(tempDir, "tinybird.config.json"), "utf-8")
57
57
  );
58
- expect(config.include).toEqual(["lib/tinybird.ts"]);
58
+ expect(config.include).toContain("lib/tinybird.ts");
59
+ expect(config.token).toBe("${TINYBIRD_TOKEN}");
59
60
  });
60
61
 
61
- it("creates tinybird.json with correct include path for src/lib/tinybird.ts", async () => {
62
+ it("creates tinybird.config.json with correct include path for src/lib/tinybird.ts", async () => {
62
63
  fs.mkdirSync(path.join(tempDir, "src"));
63
64
 
64
65
  const result = await runInit({ cwd: tempDir, skipLogin: true, devMode: "branch", clientPath: "src/lib/tinybird.ts" });
65
66
 
66
67
  expect(result.success).toBe(true);
67
68
 
68
- const config = JSON.parse(
69
- fs.readFileSync(path.join(tempDir, "tinybird.json"), "utf-8")
70
- );
71
- expect(config.include).toEqual(["src/lib/tinybird.ts"]);
69
+ const content = fs.readFileSync(path.join(tempDir, "tinybird.config.json"), "utf-8");
70
+ expect(content).toContain('"src/lib/tinybird.ts"');
72
71
  });
73
72
  });
74
73
 
75
74
  describe("config file creation", () => {
76
- it("creates tinybird.json with default values", async () => {
75
+ it("creates tinybird.config.json with default values", async () => {
77
76
  await runInit({ cwd: tempDir, skipLogin: true, devMode: "branch", clientPath: "lib/tinybird.ts" });
78
77
 
79
78
  const config = JSON.parse(
80
- fs.readFileSync(path.join(tempDir, "tinybird.json"), "utf-8")
79
+ fs.readFileSync(path.join(tempDir, "tinybird.config.json"), "utf-8")
81
80
  );
82
81
 
83
82
  expect(config.token).toBe("${TINYBIRD_TOKEN}");
84
83
  expect(config.baseUrl).toBe("https://api.tinybird.co");
84
+ expect(config.devMode).toBe("branch");
85
85
  });
86
86
 
87
- it("updates tinybird.json if it already exists", async () => {
87
+ it("updates legacy tinybird.json if it already exists", async () => {
88
88
  const existingConfig = {
89
89
  include: ["custom.ts"],
90
90
  token: "existing",
@@ -109,22 +109,46 @@ describe("Init Command", () => {
109
109
  expect(config.token).toBe("existing");
110
110
  });
111
111
 
112
- it("overwrites tinybird.json with force option", async () => {
113
- const existingConfig = { include: ["custom.ts"], token: "existing" };
112
+ it("updates tinybird.config.json if it already exists", async () => {
113
+ const existingConfig = {
114
+ include: ["custom.ts"],
115
+ token: "existing",
116
+ devMode: "local",
117
+ };
114
118
  fs.writeFileSync(
115
- path.join(tempDir, "tinybird.json"),
119
+ path.join(tempDir, "tinybird.config.json"),
116
120
  JSON.stringify(existingConfig)
117
121
  );
118
122
 
119
- const result = await runInit({ cwd: tempDir, skipLogin: true, force: true, devMode: "branch", clientPath: "lib/tinybird.ts" });
123
+ const result = await runInit({ cwd: tempDir, skipLogin: true, devMode: "branch", clientPath: "lib/tinybird.ts" });
120
124
 
121
125
  expect(result.success).toBe(true);
122
- expect(result.created).toContain("tinybird.json");
126
+ expect(result.created).toContain("tinybird.config.json (updated)");
123
127
 
128
+ // Verify include/devMode updated but token preserved
124
129
  const config = JSON.parse(
125
- fs.readFileSync(path.join(tempDir, "tinybird.json"), "utf-8")
130
+ fs.readFileSync(path.join(tempDir, "tinybird.config.json"), "utf-8")
126
131
  );
127
132
  expect(config.include).toEqual(["lib/tinybird.ts"]);
133
+ expect(config.devMode).toBe("branch");
134
+ expect(config.token).toBe("existing");
135
+ });
136
+
137
+ it("overwrites existing config with force option", async () => {
138
+ const existingConfig = { include: ["custom.ts"], token: "existing" };
139
+ fs.writeFileSync(
140
+ path.join(tempDir, "tinybird.json"),
141
+ JSON.stringify(existingConfig)
142
+ );
143
+
144
+ const result = await runInit({ cwd: tempDir, skipLogin: true, force: true, devMode: "branch", clientPath: "lib/tinybird.ts" });
145
+
146
+ expect(result.success).toBe(true);
147
+ // With force, it creates a new tinybird.config.json
148
+ expect(result.created).toContain("tinybird.config.json");
149
+
150
+ const content = fs.readFileSync(path.join(tempDir, "tinybird.config.json"), "utf-8");
151
+ expect(content).toContain('"lib/tinybird.ts"');
128
152
  });
129
153
  });
130
154
 
@@ -417,11 +441,9 @@ describe("Init Command", () => {
417
441
  expect(result.existingDatafiles).toContain("datasources/events.datasource");
418
442
  expect(result.existingDatafiles).toContain("pipes/top_events.pipe");
419
443
 
420
- const config = JSON.parse(
421
- fs.readFileSync(path.join(tempDir, "tinybird.json"), "utf-8")
422
- );
423
- expect(config.include).toContain("datasources/events.datasource");
424
- expect(config.include).toContain("pipes/top_events.pipe");
444
+ const content = fs.readFileSync(path.join(tempDir, "tinybird.config.json"), "utf-8");
445
+ expect(content).toContain('"datasources/events.datasource"');
446
+ expect(content).toContain('"pipes/top_events.pipe"');
425
447
  });
426
448
 
427
449
  it("does not include existing datafiles when user opts out", async () => {
@@ -441,11 +463,9 @@ describe("Init Command", () => {
441
463
  expect(result.success).toBe(true);
442
464
  expect(result.existingDatafiles).toBeUndefined();
443
465
 
444
- const config = JSON.parse(
445
- fs.readFileSync(path.join(tempDir, "tinybird.json"), "utf-8")
446
- );
447
- expect(config.include).not.toContain("datasources/events.datasource");
448
- expect(config.include).toEqual(["lib/tinybird.ts"]);
466
+ const content = fs.readFileSync(path.join(tempDir, "tinybird.config.json"), "utf-8");
467
+ expect(content).not.toContain("datasources/events.datasource");
468
+ expect(content).toContain('"lib/tinybird.ts"');
449
469
  });
450
470
 
451
471
  it("preserves TypeScript include paths alongside datafiles", async () => {
@@ -464,12 +484,10 @@ describe("Init Command", () => {
464
484
 
465
485
  expect(result.success).toBe(true);
466
486
 
467
- const config = JSON.parse(
468
- fs.readFileSync(path.join(tempDir, "tinybird.json"), "utf-8")
469
- );
487
+ const content = fs.readFileSync(path.join(tempDir, "tinybird.config.json"), "utf-8");
470
488
  // Should have both TypeScript file AND datafiles
471
- expect(config.include).toContain("lib/tinybird.ts");
472
- expect(config.include).toContain("datasources/events.datasource");
489
+ expect(content).toContain('"lib/tinybird.ts"');
490
+ expect(content).toContain('"datasources/events.datasource"');
473
491
  });
474
492
 
475
493
  it("handles projects with no existing datafiles", async () => {
@@ -485,10 +503,8 @@ describe("Init Command", () => {
485
503
  expect(result.success).toBe(true);
486
504
  expect(result.existingDatafiles).toBeUndefined();
487
505
 
488
- const config = JSON.parse(
489
- fs.readFileSync(path.join(tempDir, "tinybird.json"), "utf-8")
490
- );
491
- expect(config.include).toEqual(["lib/tinybird.ts"]);
506
+ const content = fs.readFileSync(path.join(tempDir, "tinybird.config.json"), "utf-8");
507
+ expect(content).toContain('"lib/tinybird.ts"');
492
508
  });
493
509
  });
494
510
  });
@@ -10,8 +10,9 @@ import {
10
10
  hasValidToken,
11
11
  getRelativeTinybirdDir,
12
12
  getConfigPath,
13
+ findExistingConfigPath,
13
14
  updateConfig,
14
- loadConfig,
15
+ loadConfigAsync,
15
16
  type DevMode,
16
17
  } from "../config.js";
17
18
  import { browserLogin } from "../auth.js";
@@ -130,6 +131,7 @@ function generateGithubCiWorkflow(workingDirectory?: string): string {
130
131
  on:
131
132
  pull_request:
132
133
  paths:
134
+ - "${pathPrefix}tinybird.config.*"
133
135
  - "${pathPrefix}tinybird.json"
134
136
  - "${pathPrefix}**/*.ts"
135
137
 
@@ -176,6 +178,7 @@ on:
176
178
  branches:
177
179
  - main
178
180
  paths:
181
+ - "${pathPrefix}tinybird.config.*"
179
182
  - "${pathPrefix}tinybird.json"
180
183
  - "${pathPrefix}**/*.ts"
181
184
 
@@ -218,6 +221,7 @@ tinybird_ci:
218
221
  image: node:22
219
222
  rules:
220
223
  - changes:
224
+ - ${pathPrefix}tinybird.config.*
221
225
  - ${pathPrefix}tinybird.json
222
226
  - ${pathPrefix}**/*.ts
223
227
  script:
@@ -246,6 +250,7 @@ tinybird_cd:
246
250
  rules:
247
251
  - if: '$CI_COMMIT_BRANCH == "main"'
248
252
  changes:
253
+ - ${pathPrefix}tinybird.config.*
249
254
  - ${pathPrefix}tinybird.json
250
255
  - ${pathPrefix}**/*.ts
251
256
  script:
@@ -258,7 +263,7 @@ tinybird_cd:
258
263
  }
259
264
 
260
265
  /**
261
- * Default config content generator
266
+ * Default config content generator (for JSON files)
262
267
  */
263
268
  function createDefaultConfig(
264
269
  tinybirdFilePath: string,
@@ -419,7 +424,7 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
419
424
 
420
425
  if (!options.devMode) {
421
426
  // Show interactive prompt for workflow selection
422
- p.intro(pc.cyan("tinybird.json"));
427
+ p.intro(pc.cyan("tinybird.config.json"));
423
428
 
424
429
  const workflowChoice = await p.select({
425
430
  message: "How do you want to develop with Tinybird?",
@@ -589,43 +594,54 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
589
594
  const tinybirdFilePath = path.join(cwd, relativeTinybirdDir);
590
595
  const tinybirdDir = path.dirname(tinybirdFilePath);
591
596
 
592
- // Create config file (tinybird.json)
593
- const configPath = getConfigPath(cwd);
594
- if (fs.existsSync(configPath) && !force) {
595
- try {
596
- const config = createDefaultConfig(
597
- relativeTinybirdDir,
598
- devMode,
599
- existingDatafiles
600
- );
601
- updateConfig(configPath, {
602
- include: config.include,
603
- devMode: config.devMode,
604
- });
605
- created.push("tinybird.json (updated)");
606
- } catch (error) {
607
- return {
608
- success: false,
609
- created,
610
- skipped,
611
- error: `Failed to update tinybird.json: ${(error as Error).message}`,
612
- };
597
+ // Create or update config file
598
+ // Check for any existing config file first
599
+ const existingConfigPath = findExistingConfigPath(cwd);
600
+ const newConfigPath = getConfigPath(cwd);
601
+
602
+ if (existingConfigPath && !force) {
603
+ // Update existing config file (only if it's JSON)
604
+ const configFileName = path.basename(existingConfigPath);
605
+ if (existingConfigPath.endsWith(".json")) {
606
+ try {
607
+ const config = createDefaultConfig(
608
+ relativeTinybirdDir,
609
+ devMode,
610
+ existingDatafiles
611
+ );
612
+ updateConfig(existingConfigPath, {
613
+ include: config.include,
614
+ devMode: config.devMode,
615
+ });
616
+ created.push(`${configFileName} (updated)`);
617
+ } catch (error) {
618
+ return {
619
+ success: false,
620
+ created,
621
+ skipped,
622
+ error: `Failed to update ${configFileName}: ${(error as Error).message}`,
623
+ };
624
+ }
625
+ } else {
626
+ // JS config file exists - skip and let user update manually
627
+ skipped.push(`${configFileName} (JS config files must be updated manually)`);
613
628
  }
614
629
  } else {
630
+ // Create new config file with JSON format
615
631
  try {
616
632
  const config = createDefaultConfig(
617
633
  relativeTinybirdDir,
618
634
  devMode,
619
635
  existingDatafiles
620
636
  );
621
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
622
- created.push("tinybird.json");
637
+ fs.writeFileSync(newConfigPath, JSON.stringify(config, null, 2) + "\n");
638
+ created.push("tinybird.config.json");
623
639
  } catch (error) {
624
640
  return {
625
641
  success: false,
626
642
  created,
627
643
  skipped,
628
- error: `Failed to create tinybird.json: ${(error as Error).message}`,
644
+ error: `Failed to create tinybird.config.json: ${(error as Error).message}`,
629
645
  };
630
646
  }
631
647
  }
@@ -841,10 +857,13 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
841
857
  created.push(".env.local");
842
858
  }
843
859
 
844
- // If custom base URL, update tinybird.json
860
+ // If custom base URL, update config file
845
861
  const baseUrl = authResult.baseUrl ?? "https://api.tinybird.co";
846
862
  if (baseUrl !== "https://api.tinybird.co") {
847
- updateConfig(configPath, { baseUrl });
863
+ const currentConfigPath = findExistingConfigPath(cwd);
864
+ if (currentConfigPath && currentConfigPath.endsWith(".json")) {
865
+ updateConfig(currentConfigPath, { baseUrl });
866
+ }
848
867
  }
849
868
 
850
869
  // Generate TypeScript from existing Tinybird resources if requested
@@ -916,7 +935,7 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
916
935
  // (when user is already logged in)
917
936
  if (datafileAction === "codegen" && hasValidToken(cwd)) {
918
937
  try {
919
- const config = loadConfig(cwd);
938
+ const config = await loadConfigAsync(cwd);
920
939
  const tinybirdDir = path.join(cwd, relativeTinybirdDir);
921
940
  await runCodegen(
922
941
  config.baseUrl,
@@ -35,7 +35,7 @@ describe("Login Command", () => {
35
35
  const result = await runLogin({ cwd: tempDir });
36
36
 
37
37
  expect(result.success).toBe(false);
38
- expect(result.error).toContain("No tinybird.json found");
38
+ expect(result.error).toContain("No tinybird config found");
39
39
  expect(result.error).toContain("npx tinybird init");
40
40
  });
41
41
 
@@ -45,14 +45,15 @@ export async function runLogin(options: RunLoginOptions = {}): Promise<LoginResu
45
45
  const cwd = options.cwd ?? process.cwd();
46
46
 
47
47
  // Find the actual config file (may be in parent directory)
48
- const configPath = findConfigFile(cwd);
49
- if (!configPath) {
48
+ const configResult = findConfigFile(cwd);
49
+ if (!configResult) {
50
50
  return {
51
51
  success: false,
52
- error: "No tinybird.json found. Run 'npx tinybird init' first.",
52
+ error: "No tinybird config found. Run 'npx tinybird init' first.",
53
53
  };
54
54
  }
55
55
 
56
+ const configPath = configResult.path;
56
57
  // Get the directory containing the config file for .env.local
57
58
  const configDir = path.dirname(configPath);
58
59
 
@@ -72,12 +73,12 @@ export async function runLogin(options: RunLoginOptions = {}): Promise<LoginResu
72
73
  };
73
74
  }
74
75
 
75
- // Save token to .env.local (in same directory as tinybird.json)
76
+ // Save token to .env.local (in same directory as config file)
76
77
  try {
77
78
  saveTinybirdToken(configDir, authResult.token);
78
79
 
79
- // Update baseUrl in tinybird.json if it changed
80
- if (authResult.baseUrl) {
80
+ // Update baseUrl in config file if it changed (only for JSON configs)
81
+ if (authResult.baseUrl && configPath.endsWith(".json")) {
81
82
  updateConfig(configPath, {
82
83
  baseUrl: authResult.baseUrl,
83
84
  });
@@ -2,7 +2,7 @@
2
2
  * Preview command - creates ephemeral preview branch and deploys resources
3
3
  */
4
4
 
5
- import { loadConfig, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js";
5
+ import { loadConfigAsync, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js";
6
6
  import { buildFromInclude, type BuildFromIncludeResult } from "../../generator/index.js";
7
7
  import { createBranch, deleteBranch, getBranch, type TinybirdBranch } from "../../api/branches.js";
8
8
  import { deployToMain } from "../../api/deploy.js";
@@ -90,7 +90,7 @@ export async function runPreview(options: PreviewCommandOptions = {}): Promise<P
90
90
  // Load config
91
91
  let config: ResolvedConfig;
92
92
  try {
93
- config = loadConfig(cwd);
93
+ config = await loadConfigAsync(cwd);
94
94
  } catch (error) {
95
95
  return {
96
96
  success: false,
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Universal config file loader
3
+ * Supports .json, .cjs, and .mjs files
4
+ */
5
+
6
+ import * as fs from "node:fs/promises";
7
+ import * as path from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+
10
+ export type MaybePromise<T> = T | Promise<T>;
11
+
12
+ export type LoadedConfig<T> = {
13
+ config: T;
14
+ filepath: string;
15
+ };
16
+
17
+ export type LoadConfigOptions = {
18
+ cwd?: string;
19
+ };
20
+
21
+ function isObject(value: unknown): value is Record<string, unknown> {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+
25
+ async function readJsonFile<T>(filepath: string): Promise<T> {
26
+ const raw = await fs.readFile(filepath, "utf8");
27
+ return JSON.parse(raw) as T;
28
+ }
29
+
30
+ /**
31
+ * Resolve the config export from a module
32
+ * Supports default export, module.exports, and function configs
33
+ */
34
+ async function resolveConfigExport(mod: unknown): Promise<unknown> {
35
+ const moduleObj = mod as Record<string, unknown>;
36
+ const exported = moduleObj?.default ?? mod;
37
+
38
+ // Allow config as function (sync/async)
39
+ if (typeof exported === "function") {
40
+ return await (exported as () => MaybePromise<unknown>)();
41
+ }
42
+ return exported;
43
+ }
44
+
45
+ /**
46
+ * Load a config file from disk
47
+ * Supports .json, .cjs, and .mjs files
48
+ */
49
+ export async function loadConfigFile<T = unknown>(
50
+ configPath: string,
51
+ opts: LoadConfigOptions = {}
52
+ ): Promise<LoadedConfig<T>> {
53
+ const cwd = opts.cwd ?? process.cwd();
54
+ const filepath = path.isAbsolute(configPath)
55
+ ? configPath
56
+ : path.resolve(cwd, configPath);
57
+
58
+ const ext = path.extname(filepath).toLowerCase();
59
+
60
+ if (ext === ".json") {
61
+ const config = await readJsonFile<T>(filepath);
62
+ return { config, filepath };
63
+ }
64
+
65
+ if (ext === ".mjs" || ext === ".cjs") {
66
+ // Load JS modules via runtime import for bundler compatibility
67
+ const url = pathToFileURL(filepath).href;
68
+ const mod = await import(
69
+ /* webpackIgnore: true */
70
+ /* @vite-ignore */
71
+ url
72
+ );
73
+ const config = await resolveConfigExport(mod);
74
+
75
+ if (!isObject(config)) {
76
+ throw new Error(
77
+ `Config in ${filepath} must export an object (or a function returning an object).`
78
+ );
79
+ }
80
+
81
+ return { config: config as T, filepath };
82
+ }
83
+
84
+ throw new Error(
85
+ `Unsupported config extension "${ext}". Use .json, .mjs, or .cjs`
86
+ );
87
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Configuration types for tinybird.config.{ts,js,json}
3
+ *
4
+ * This file is separate from config.ts to avoid pulling in esbuild
5
+ * when these types are imported by client code.
6
+ */
7
+
8
+ /**
9
+ * Development mode options
10
+ * - "branch": Use Tinybird cloud with branches (default)
11
+ * - "local": Use local Tinybird container at localhost:7181
12
+ */
13
+ export type DevMode = "branch" | "local";
14
+
15
+ /**
16
+ * Tinybird configuration file structure
17
+ */
18
+ export interface TinybirdConfig {
19
+ /** Array of TypeScript files to scan for datasources and pipes */
20
+ include?: string[];
21
+ /** @deprecated Use `include` instead. Path to the TypeScript schema entry point */
22
+ schema?: string;
23
+ /** API token (supports ${ENV_VAR} interpolation) */
24
+ token: string;
25
+ /** Tinybird API base URL (optional, defaults to EU region) */
26
+ baseUrl?: string;
27
+ /** Development mode: "branch" (default) or "local" */
28
+ devMode?: DevMode;
29
+ }