Skip to content

Commit 6810208

Browse files
authored
Merge pull request #13 from badsyntax/cli-improvements
Improve cli, add logger
2 parents 854936c + ef5ff58 commit 6810208

File tree

14 files changed

+240
-117
lines changed

14 files changed

+240
-117
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
A VS Code extension to manage Entity Framework migrations.
66

7-
## Features
8-
97
![Entity Framework Migrations](images/treeview-screenshot.png)
108

11-
- List migrations by [DbContext](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext)
9+
## Features
10+
11+
- List migrations by project [DbContext](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext)
1212
- Add/remove/run/undo migrations
13-
- Export [DbContext](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) as SQL script
13+
- Export project [DbContext](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) as SQL script
1414

1515
## Requirements
1616

package.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,31 @@
241241
"\"$project\"",
242242
"--context",
243243
"\"$dbContext\""
244+
],
245+
"listDbContexts": [
246+
"dotnet",
247+
"ef",
248+
"dbcontext",
249+
"list",
250+
"--project",
251+
"\"$project\"",
252+
"--no-color",
253+
"--json"
254+
],
255+
"listMigrations": [
256+
"dotnet",
257+
"ef",
258+
"migrations",
259+
"list",
260+
"--context",
261+
"\"$context\"",
262+
"--project",
263+
"\"$project\"",
264+
"--no-color",
265+
"--json"
244266
]
245267
},
246-
"description": "Environment vars when interacting with Entity Framework."
268+
"description": "Environment variables when interacting with Entity Framework."
247269
}
248270
}
249271
}

src/actions/GenerateScriptAction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode';
2+
import { CLI } from '../cli/CLI';
23

3-
import { getDataFromStdOut } from '../cli/ef';
44
import { getCommandsConfig } from '../config/config';
55
import type { TerminalProvider } from '../terminal/TerminalProvider';
66
import { TerminalAction } from './TerminalAction';
@@ -24,7 +24,7 @@ export class GenerateScriptAction extends TerminalAction {
2424
}
2525

2626
public async run() {
27-
const output = getDataFromStdOut(await super.run());
27+
const output = CLI.getDataFromStdOut(await super.run());
2828
const uri = vscode.Uri.parse('ef-script:' + output);
2929
const doc = await vscode.workspace.openTextDocument(uri);
3030
await vscode.languages.setTextDocumentLanguage(doc, 'sql');

src/actions/TerminalAction.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CLI } from '../cli/CLI';
12
import type { TerminalProvider } from '../terminal/TerminalProvider';
23
import type { IAction } from './IAction';
34

@@ -11,15 +12,7 @@ export abstract class TerminalAction implements IAction {
1112

1213
public async run(params = this.params): Promise<string> {
1314
const terminal = this.terminalProvider.provideTerminal();
14-
terminal.setCmdARgs(
15-
this.getInterpolatedArgs(params).concat(['--prefix-output']),
16-
);
15+
terminal.setCmdArgs(CLI.getInterpolatedArgs(this.args, params));
1716
return await terminal.exec(this.workingFolder);
1817
}
19-
20-
private getInterpolatedArgs(params = this.params) {
21-
return this.args.map(arg =>
22-
arg.replace(/\$[\w]+/, a => params[a.slice(1)] || a),
23-
);
24-
}
2518
}

src/cli/CLI.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2+
import { spawn } from 'node:child_process';
3+
4+
import { getEnvConfig } from '../config/config';
5+
import type { Logger } from '../util/logger';
6+
7+
const NEWLINE_SEPARATOR = /\r\n|\r|\n/;
8+
const STDOUT_PREFIX = /^[a-z]+: /;
9+
10+
export class CLI {
11+
constructor(private readonly logger: Logger) {}
12+
13+
public static getInterpolatedArgs(
14+
args: string[],
15+
params: { [key: string]: string },
16+
) {
17+
return args.map(arg =>
18+
arg.replace(/\$[\w]+/, a => params[a.slice(1)] || a),
19+
);
20+
}
21+
22+
public static getDataFromStdOut(output: string): string {
23+
return this.removePrefixFromStdOut(
24+
output
25+
.split(NEWLINE_SEPARATOR)
26+
.filter(line => line.startsWith('data:'))
27+
.join('\n'),
28+
);
29+
}
30+
31+
public static getErrorsFromStdOut(output: string): string {
32+
return this.removePrefixFromStdOut(
33+
output
34+
.split(NEWLINE_SEPARATOR)
35+
.filter(line => line.startsWith('error:'))
36+
.join('\n'),
37+
);
38+
}
39+
40+
public static removePrefixFromStdOut(output: string): string {
41+
return output
42+
.split(NEWLINE_SEPARATOR)
43+
.map(line => line.replace(STDOUT_PREFIX, ''))
44+
.join('\n');
45+
}
46+
47+
public exec(
48+
cmdArgs: string[],
49+
cwd: string,
50+
handlers?: {
51+
onStdOut?: (buffer: string) => void;
52+
onStdErr?: (buffer: string) => void;
53+
},
54+
): {
55+
cmd: ChildProcessWithoutNullStreams;
56+
output: Promise<string>;
57+
} {
58+
this.logger.info(cmdArgs.join(' '));
59+
60+
const args = cmdArgs.concat(['--prefix-output']);
61+
62+
const cmd = spawn(args[0], args.slice(1), {
63+
cwd,
64+
env: {
65+
...process.env,
66+
...getEnvConfig(),
67+
},
68+
});
69+
70+
let stdout = '';
71+
let stderr = '';
72+
73+
return {
74+
cmd,
75+
output: new Promise((res, rej) => {
76+
cmd?.stdout.on('data', buffer => {
77+
const data = buffer.toString();
78+
stdout += data;
79+
handlers?.onStdOut?.(data);
80+
});
81+
82+
cmd?.stderr.on('data', buffer => {
83+
const data = buffer.toString();
84+
stderr += data;
85+
handlers?.onStdErr?.(data);
86+
});
87+
88+
cmd?.on('exit', async code => {
89+
const error = stderr || CLI.getErrorsFromStdOut(stdout);
90+
if (error || code !== 0) {
91+
const finalError = error || stdout;
92+
this.logger.error(finalError);
93+
rej(new Error(finalError));
94+
} else {
95+
res(stdout);
96+
}
97+
});
98+
}),
99+
};
100+
}
101+
}

src/cli/ef.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.

src/config/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ type CommandsConfig = {
1010
removeMigration: string[];
1111
runMigration: string[];
1212
generateScript: string[];
13+
listDbContexts: string[];
14+
listMigrations: string[];
1315
};
1416

1517
export function getEnvConfig() {
@@ -26,5 +28,7 @@ export function getCommandsConfig() {
2628
removeMigration: [],
2729
runMigration: [],
2830
generateScript: [],
31+
listDbContexts: [],
32+
listMigrations: [],
2933
});
3034
}

src/constants/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export const EXTENSION_NAMESPACE = 'entityframework';
22

33
export const TERMINAL_NAME = 'ef-migrations';
4+
5+
export const OUTPUT_CHANNEL_ID = 'Entity Framework Migrations';

src/extension.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type * as vscode from 'vscode';
1+
import * as vscode from 'vscode';
22

33
import { TreeDataProvider } from './treeView/TreeDataProvider';
44
import { CommandProvider } from './commands/CommandProvider';
@@ -7,16 +7,25 @@ import { Terminal } from './terminal/Terminal';
77
import { TerminalProvider } from './terminal/TerminalProvider';
88
import { ScriptFileProvider } from './util/ScriptFileProvider';
99
import { ProjectFilesProvider } from './solution/ProjectFilesProvider';
10+
import { Logger } from './util/logger';
11+
import { OUTPUT_CHANNEL_ID } from './constants/constants';
12+
import { CLI } from './cli/CLI';
1013

1114
const subscriptions: vscode.Disposable[] = [];
1215

1316
export async function activate(_context: vscode.ExtensionContext) {
17+
const logger = new Logger();
18+
logger.setLoggingChannel(
19+
vscode.window.createOutputChannel(OUTPUT_CHANNEL_ID),
20+
);
21+
22+
const cli = new CLI(logger);
1423
const projectFiles = await ProjectFilesProvider.getProjectFiles();
1524
const scriptFileProvider = new ScriptFileProvider();
1625
const migrationTreeItemDecorationProvider =
1726
new MigrationTreeItemDecorationProvider();
18-
const treeDataProvider = new TreeDataProvider(projectFiles);
19-
const terminalProvider = new TerminalProvider(new Terminal());
27+
const treeDataProvider = new TreeDataProvider(projectFiles, cli);
28+
const terminalProvider = new TerminalProvider(new Terminal(cli));
2029
const commandProvider = new CommandProvider(
2130
treeDataProvider,
2231
terminalProvider,

src/terminal/Terminal.ts

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import * as vscode from 'vscode';
2-
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2+
import { type ChildProcessWithoutNullStreams } from 'node:child_process';
33

44
import { EventWaiter } from '../util/EventWaiter';
5-
import { getEnvConfig } from '../config/config';
6-
import { getErrorsFromStdOut, removePrefixFromStdOut } from '../cli/ef';
5+
import { CLI } from '../cli/CLI';
76

87
const NL = '\n';
98
const CR = '\r';
109
const nlRegExp = new RegExp(`${NL}([^${CR}]|$)`, 'g');
1110

1211
export class Terminal implements vscode.Pseudoterminal {
1312
private cmdArgs: string[] = [];
13+
private cmdParams: { [key: string]: string } = {};
1414
private cmd: ChildProcessWithoutNullStreams | undefined;
1515

1616
private readonly writeEmitter = new vscode.EventEmitter<string>();
@@ -23,58 +23,33 @@ export class Terminal implements vscode.Pseudoterminal {
2323

2424
private readonly waitForOpen = new EventWaiter<undefined>(this.onDidOpen);
2525

26-
constructor() {}
26+
constructor(private readonly cli: CLI) {}
2727

2828
public async open(): Promise<void> {
2929
this.openEmitter.fire(undefined);
3030
}
3131

3232
public async close(): Promise<void> {}
3333

34-
public setCmdARgs(cmdArgs: string[]) {
34+
public setCmdArgs(cmdArgs: string[]) {
3535
this.cmdArgs = cmdArgs;
3636
}
3737

3838
public async exec(cwd: string): Promise<string> {
3939
await this.waitForOpen.wait();
4040

41-
let stdout = '';
42-
let stderr = '';
41+
this.write(this.cmdArgs.join(' ') + '\n');
4342

44-
this.cmd = spawn(this.cmdArgs[0], this.cmdArgs.slice(1), {
45-
cwd,
46-
env: {
47-
...process.env,
48-
...getEnvConfig(),
43+
const { cmd, output } = this.cli.exec(this.cmdArgs, cwd, {
44+
onStdOut: (buffer: string) => {
45+
this.write(CLI.removePrefixFromStdOut(buffer));
46+
},
47+
onStdErr: (buffer: string) => {
48+
this.write(CLI.removePrefixFromStdOut(buffer));
4949
},
5050
});
51-
52-
return new Promise(res => {
53-
// --prefix-output is an internal flag that is added to all commands
54-
const argsWithoutPrefixOutput = this.cmdArgs.slice(0, -1);
55-
this.write(argsWithoutPrefixOutput.join(' ') + '\n');
56-
57-
this.cmd?.stdout.on('data', data => {
58-
const dataString = data.toString();
59-
stdout += dataString;
60-
this.write(removePrefixFromStdOut(dataString));
61-
});
62-
63-
this.cmd?.stderr.on('data', data => {
64-
const dataString = data.toString();
65-
stderr += dataString;
66-
this.write(removePrefixFromStdOut(dataString));
67-
});
68-
69-
this.cmd?.on('exit', async _code => {
70-
this.cmd = undefined;
71-
const error = stderr || getErrorsFromStdOut(stdout);
72-
if (error) {
73-
await vscode.window.showErrorMessage(error);
74-
}
75-
res(stdout);
76-
});
77-
});
51+
this.cmd = cmd;
52+
return await output;
7853
}
7954

8055
public async handleInput(data: string): Promise<void> {

0 commit comments

Comments
 (0)