Skip to content

Commit 180cf75

Browse files
authored
feat(cli): add message when all files compile successfully in nango dev (#3908)
<!-- Describe the problem and your solution --> ### Description We currently have no feedback for when all files compile successfully, leading to a confusing DX when the last thing you see is the last error you got. This PR adds this: <img width="803" alt="image" src="https://github.com/user-attachments/assets/4424d5e5-548c-49ff-ab7d-6cf8d3d42c3c" /> > Feel free to suggest another message To achieve this, I had to keep track of failed files and tweak how things were done. I believe I've fixed a couple bugs as a tangent. I'll add comments to the relevant sections with more details. <!-- Testing instructions (skip if just adding/editing providers) --> ### Testing Please checkout to the branch and run `nango dev` in a nango project. Break and fix typescript files and the config file, in mixed order, to see if the DX is looking good. The `No compilation errors.` message should only show up when everything is looking good, both with TS files as well as `nango.yaml`.
1 parent 5a7b811 commit 180cf75

File tree

8 files changed

+160
-71
lines changed

8 files changed

+160
-71
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"test:unit": "vitest",
4444
"test:integration": "vitest --config ./vite.integration.config.ts",
4545
"test:cli": "vitest --config ./vite.cli.config.ts",
46+
"test:cli:update-snapshots": "vitest --config ./vite.cli.config.ts --update",
4647
"test:openapi": "npx @apidevtools/swagger-cli validate docs-v2/spec.yaml",
4748
"test:providers": "npx tsx scripts/validation/providers/validate.ts",
4849
"docs": "tsx scripts/docs-gen-snippets.ts && cd ./docs-v2 && npx mintlify dev --port 3033",

packages/cli/lib/cli.ts

Lines changed: 85 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { getLayoutMode } from './utils/layoutMode.js';
1616
import { getNangoRootPath, printDebug } from './utils.js';
1717
import { NANGO_VERSION } from './version.js';
1818

19+
import type { NangoYamlParsed } from '@nangohq/types';
20+
1921
const __filename = fileURLToPath(import.meta.url);
2022
const __dirname = dirname(__filename);
2123

@@ -127,14 +129,19 @@ export function generate({ fullPath, debug = false }: { fullPath: string; debug?
127129
}
128130
}
129131

130-
export function tscWatch({ fullPath, debug = false }: { fullPath: string; debug?: boolean }) {
131-
const tsconfig = fs.readFileSync(path.resolve(getNangoRootPath(), 'tsconfig.dev.json'), 'utf8');
132-
const parsed = loadYamlAndGenerate({ fullPath, debug });
133-
if (!parsed) {
134-
return;
132+
function showCompilationMessage(failedFiles: Set<string>) {
133+
if (failedFiles.size === 0) {
134+
console.log(chalk.green('Compilation success! Watching files…'));
135135
}
136+
}
136137

137-
const watchPath = ['./**/*.ts', `./${nangoConfigFile}`];
138+
export function tscWatch({ fullPath, debug = false, watchConfigFile }: { fullPath: string; debug?: boolean; watchConfigFile: boolean }) {
139+
const tsconfig = fs.readFileSync(path.resolve(getNangoRootPath(), 'tsconfig.dev.json'), 'utf8');
140+
141+
const watchPath = ['./**/*.ts'];
142+
if (watchConfigFile) {
143+
watchPath.push(`./${nangoConfigFile}`);
144+
}
138145

139146
if (debug) {
140147
printDebug(`Watching ${watchPath.join(', ')}`);
@@ -144,7 +151,7 @@ export function tscWatch({ fullPath, debug = false }: { fullPath: string; debug?
144151
ignoreInitial: false,
145152
ignored: (filePath: string) => {
146153
const relativePath = path.relative(__dirname, filePath);
147-
return relativePath.includes('node_modules') || path.basename(filePath) === TYPES_FILE_NAME;
154+
return relativePath.includes('node_modules') || path.basename(filePath) === TYPES_FILE_NAME || relativePath.includes('.nango');
148155
}
149156
});
150157

@@ -157,46 +164,92 @@ export function tscWatch({ fullPath, debug = false }: { fullPath: string; debug?
157164
fs.mkdirSync(distDir);
158165
}
159166

160-
watcher.on('add', async (filePath: string) => {
161-
if (filePath === nangoConfigFile) {
162-
return;
167+
// First parsing of the config file
168+
let parsed: NangoYamlParsed | null = loadYamlAndGenerate({ fullPath, debug });
169+
170+
const failedFiles = new Set<string>();
171+
172+
watcher.on('add', (filePath: string) => {
173+
async function onAdd() {
174+
if (debug) {
175+
printDebug(`Added ${filePath}`);
176+
}
177+
if (filePath === nangoConfigFile || !parsed) {
178+
return;
179+
}
180+
const success = await compileSingleFile({
181+
fullPath,
182+
file: getFileToCompile({ fullPath, filePath }),
183+
tsconfig,
184+
parsed,
185+
debug
186+
});
187+
if (success) {
188+
failedFiles.delete(filePath);
189+
} else {
190+
failedFiles.add(filePath);
191+
}
192+
showCompilationMessage(failedFiles);
193+
}
194+
195+
void onAdd();
196+
});
197+
198+
watcher.on('change', (filePath: string) => {
199+
async function onChange() {
200+
if (debug) {
201+
printDebug(`Changed ${filePath}`);
202+
}
203+
if (filePath === nangoConfigFile) {
204+
parsed = loadYamlAndGenerate({ fullPath, debug });
205+
206+
if (!parsed) {
207+
return;
208+
}
209+
210+
const { failedFiles: newFailedFiles } = await compileAllFiles({ fullPath, debug });
211+
failedFiles.clear();
212+
for (const file of newFailedFiles) {
213+
failedFiles.add(file);
214+
}
215+
showCompilationMessage(failedFiles);
216+
return;
217+
}
218+
219+
if (!parsed) {
220+
return;
221+
}
222+
223+
const success = await compileSingleFile({ fullPath, file: getFileToCompile({ fullPath, filePath }), parsed, debug });
224+
if (success) {
225+
failedFiles.delete(filePath);
226+
} else {
227+
failedFiles.add(filePath);
228+
}
229+
showCompilationMessage(failedFiles);
163230
}
164-
await compileSingleFile({ fullPath, file: getFileToCompile({ fullPath, filePath }), tsconfig, parsed, debug });
231+
232+
void onChange();
165233
});
166234

167235
watcher.on('unlink', (filePath: string) => {
168-
if (filePath === nangoConfigFile) {
236+
if (debug) {
237+
printDebug(`Unlinked ${filePath}`);
238+
}
239+
if (filePath === nangoConfigFile || !parsed) {
169240
return;
170241
}
171242
const providerConfiguration = getProviderConfigurationFromPath({ filePath, parsed });
172243
const baseName = path.basename(filePath, '.ts');
173244
const fileName = providerConfiguration ? `${baseName}-${providerConfiguration.providerConfigKey}.js` : `${baseName}.js`;
174245
const jsFilePath = `./dist/${fileName}`;
175246

247+
failedFiles.delete(filePath);
248+
176249
try {
177250
fs.unlinkSync(jsFilePath);
178251
} catch {
179252
console.log(chalk.red(`Error deleting ${jsFilePath}`));
180253
}
181254
});
182-
183-
watcher.on('change', async (filePath: string) => {
184-
if (filePath === nangoConfigFile) {
185-
await compileAllFiles({ fullPath, debug });
186-
return;
187-
}
188-
await compileSingleFile({ fullPath, file: getFileToCompile({ fullPath, filePath }), parsed, debug });
189-
});
190-
}
191-
192-
export function configWatch({ fullPath, debug = false }: { fullPath: string; debug?: boolean }) {
193-
const watchPath = path.join(fullPath, nangoConfigFile);
194-
if (debug) {
195-
printDebug(`Watching ${watchPath}`);
196-
}
197-
const watcher = chokidar.watch(watchPath, { ignoreInitial: true });
198-
199-
watcher.on('change', () => {
200-
loadYamlAndGenerate({ fullPath, debug });
201-
});
202255
}

packages/cli/lib/index.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import figlet from 'figlet';
1414

1515
import { nangoConfigFile } from '@nangohq/nango-yaml';
1616

17-
import { configWatch, generate, getVersionOutput, tscWatch } from './cli.js';
17+
import { generate, getVersionOutput, tscWatch } from './cli.js';
1818
import { compileAllFiles } from './services/compile.service.js';
1919
import { parse } from './services/config.service.js';
2020
import deployService from './services/deploy.service.js';
@@ -168,11 +168,7 @@ program
168168
const fullPath = process.cwd();
169169
await verificationService.necessaryFilesExist({ fullPath, autoConfirm, debug, checkDist: false });
170170

171-
if (compileInterfaces) {
172-
configWatch({ fullPath, debug });
173-
}
174-
175-
tscWatch({ fullPath, debug });
171+
tscWatch({ fullPath, debug, watchConfigFile: compileInterfaces });
176172
});
177173

178174
program
@@ -267,7 +263,7 @@ program
267263
return;
268264
}
269265

270-
const success = await compileAllFiles({ fullPath, debug });
266+
const { success } = await compileAllFiles({ fullPath, debug });
271267
if (!success) {
272268
console.log(chalk.red('Compilation was not fully successful. Please make sure all files compile before deploying'));
273269
process.exitCode = 1;

packages/cli/lib/services/compile.service.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ function getCachedParser({ fullPath, debug }: { fullPath: string; debug: boolean
3333
};
3434
}
3535

36+
interface CompileAllFilesResult {
37+
success: boolean;
38+
failedFiles: string[];
39+
}
40+
3641
export async function compileAllFiles({
3742
debug,
3843
fullPath,
@@ -45,7 +50,7 @@ export async function compileAllFiles({
4550
scriptName?: string;
4651
providerConfigKey?: string;
4752
type?: ScriptFileType;
48-
}): Promise<boolean> {
53+
}): Promise<CompileAllFilesResult> {
4954
const tsconfig = fs.readFileSync(path.join(getNangoRootPath(), 'tsconfig.dev.json'), 'utf8');
5055

5156
const distDir = path.join(fullPath, 'dist');
@@ -59,7 +64,7 @@ export async function compileAllFiles({
5964
const cachedParser = getCachedParser({ fullPath, debug });
6065
const parsed = cachedParser();
6166
if (!parsed) {
62-
return false;
67+
return { success: false, failedFiles: [] };
6368
}
6469

6570
const compilerOptions = (JSON.parse(tsconfig) as { compilerOptions: Record<string, any> }).compilerOptions;
@@ -80,20 +85,23 @@ export async function compileAllFiles({
8085

8186
const integrationFiles = listFilesToCompile({ scriptName, fullPath, scriptDirectory, parsed, debug, providerConfigKey });
8287
let allSuccess = true;
88+
const failedFiles: string[] = [];
8389
const compilationErrors: string[] = [];
8490

8591
for (const file of integrationFiles) {
8692
try {
8793
const completed = await compile({ fullPath, file, compiler, debug, cachedParser });
8894
if (completed === false) {
8995
allSuccess = false;
96+
failedFiles.push(file.inputPath);
9097
compilationErrors.push(`Failed to compile ${file.inputPath}`);
9198
continue;
9299
}
93100
} catch (err) {
94101
console.log(chalk.red(`Error compiling "${file.inputPath}":`));
95102
console.error(err);
96103
allSuccess = false;
104+
failedFiles.push(file.inputPath);
97105
compilationErrors.push(`Error compiling ${file.inputPath}: ${err instanceof Error ? err.message : String(err)}`);
98106
}
99107
}
@@ -107,13 +115,14 @@ export async function compileAllFiles({
107115
console.log(chalk.green('Successfully compiled all files present in the Nango YAML config file.'));
108116
}
109117

110-
return allSuccess;
118+
return { success: allSuccess, failedFiles };
111119
}
112120

113121
export async function compileSingleFile({
114122
fullPath,
115123
file,
116124
tsconfig,
125+
parsed,
117126
debug = false
118127
}: {
119128
fullPath: string;
@@ -124,7 +133,7 @@ export async function compileSingleFile({
124133
}) {
125134
const resolvedTsconfig = tsconfig ?? fs.readFileSync(path.join(getNangoRootPath(), 'tsconfig.dev.json'), 'utf8');
126135

127-
const cachedParser = getCachedParser({ fullPath, debug });
136+
const cachedParser = parsed ? () => parsed : getCachedParser({ fullPath, debug });
128137

129138
try {
130139
const compiler = tsNode.create({
@@ -140,9 +149,9 @@ export async function compileSingleFile({
140149
debug
141150
});
142151

143-
return result === true;
152+
return result === true || result === null;
144153
} catch (err) {
145-
console.error(`Error compiling ${file.inputPath}:`);
154+
console.error(chalk.red(`Error compiling ${file.inputPath}:`));
146155
console.error(err);
147156
return false;
148157
}

packages/cli/lib/services/deploy.service.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ class DeployService {
4949
printDebug(`Environment is set to ${environmentName}`);
5050
}
5151

52-
const successfulCompile = await compileAllFiles({ fullPath, debug });
52+
const { success } = await compileAllFiles({ fullPath, debug });
5353

54-
if (!successfulCompile) {
54+
if (!success) {
5555
console.log(chalk.red('Compilation was not fully successful. Please make sure all files compile before deploying'));
5656
process.exit(1);
5757
}
@@ -199,14 +199,16 @@ class DeployService {
199199
}
200200
} else if (integrationIdMode) {
201201
// Only compile files for the specified integration
202-
successfulCompile = await compileAllFiles({
202+
const { success } = await compileAllFiles({
203203
fullPath,
204204
debug,
205205
providerConfigKey: integrationId!
206206
});
207+
successfulCompile = success;
207208
} else {
208209
// Compile all files
209-
successfulCompile = await compileAllFiles({ fullPath, debug });
210+
const { success } = await compileAllFiles({ fullPath, debug });
211+
successfulCompile = success;
210212
}
211213

212214
if (!successfulCompile) {
@@ -408,14 +410,16 @@ class DeployService {
408410
let successfulCompile: boolean = false;
409411
if (integration) {
410412
// Only compile files for the specified integration
411-
successfulCompile = await compileAllFiles({
413+
const { success } = await compileAllFiles({
412414
fullPath,
413415
debug,
414416
providerConfigKey: integration
415417
});
418+
successfulCompile = success;
416419
} else {
417420
// Compile all files
418-
successfulCompile = await compileAllFiles({ fullPath, debug });
421+
const { success } = await compileAllFiles({ fullPath, debug });
422+
successfulCompile = success;
419423
}
420424

421425
if (!successfulCompile) {

packages/cli/lib/services/deploy.service.unit.cli-test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import fs from 'node:fs';
2-
import { describe, expect, it, beforeAll } from 'vitest';
2+
3+
import { beforeAll, describe, expect, it } from 'vitest';
4+
5+
import { compileAllFiles } from './compile.service';
36
import { parse } from './config.service.js';
47
import deployService from './deploy.service';
58
import { copyDirectoryAndContents, fixturesPath, getTestDirectory, removeVersion } from '../tests/helpers.js';
6-
import { compileAllFiles } from './compile.service';
79

810
describe('package', () => {
911
let dir: string;
@@ -19,8 +21,11 @@ describe('package', () => {
1921
await fs.promises.copyFile(`${fixturesPath}/nango-yaml/v2/nested-integrations/nango.yaml`, `${dir}/nango.yaml`);
2022

2123
// Compile only once
22-
const success = await compileAllFiles({ fullPath: dir, debug: false });
23-
expect(success).toBe(true);
24+
const result = await compileAllFiles({ fullPath: dir, debug: false });
25+
expect(result).toEqual({
26+
success: true,
27+
failedFiles: []
28+
});
2429
});
2530

2631
it('should package correctly', () => {

packages/cli/lib/services/dryrun.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ export class DryRunService {
247247
type = 'on-events';
248248
}
249249

250-
const result = await compileAllFiles({ fullPath: process.cwd(), debug, scriptName: syncName, providerConfigKey, type });
250+
const { success } = await compileAllFiles({ fullPath: process.cwd(), debug, scriptName: syncName, providerConfigKey, type });
251251

252-
if (!result) {
252+
if (!success) {
253253
console.log(chalk.red('The sync/action did not compile successfully. Exiting'));
254254
return;
255255
}

0 commit comments

Comments
 (0)