Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove reliance on .sln file #12

Merged
merged 4 commits into from
Dec 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
},
"cSpell.language": "en-GB",
"cSpell.words": [
"ASPNETCORE",
"color",
"dbcontext",
"efcore",
Expand Down
32 changes: 18 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,23 @@ A VS Code extension to manage Entity Framework migrations.

## Features

![treeview](images/treeview-screenshot.png)
![Entity Framework Migrations](images/treeview-screenshot.png)

- List dbContexts for all projects within a solution
- List migrations by [DbContext](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext)
- Add/remove/run/undo migrations
- Export [DbContext](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) as SQL script

## Requirements

- [dotnet sdk](https://dotnet.microsoft.com/download)
- [efcore tools](https://learn.microsoft.com/en-us/ef/core/cli/dotnet)
- A solution (`.sln`) file with projects
- [Microsoft.EntityFrameworkCore.Design](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Design) must be installed in one of the projects

## Extension Settings

This extension contributes the following settings:

- `entityframework.env`: Custom environment vars, for example:
```json
{
"entityframework.env": {
"ASPNETCORE_ENVIRONMENT": "LocalDev",
"TenantId": "12345"
}
}
```
- `entityframework.commands`: Custom commands, for example:

```json
{
"entityframework.commands": {
Expand Down Expand Up @@ -87,10 +77,24 @@ This extension contributes the following settings:
}
}
```
- `entityframework.env`: Custom environment variables, for example:
```json
{
"entityframework.env": {
"ASPNETCORE_ENVIRONMENT": "LocalDev",
"TenantId": "12345"
}
}
```

## Performance

The EF tools execute application code at design time to get information about the project, thus performance can be slow on large projects.
The EF tools execute application code at design time to get information about the project, thus performance on large projects can be slow.

## Support

- 👉 [Submit a bug report](https://github.com/badsyntax/vscode-entity-framework/issues/new?assignees=badsyntax&labels=bug&template=bug_report.md&title=)
- 👉 [Submit a feature request](https://github.com/badsyntax/vscode-entity-framework/issues/new?assignees=badsyntax&labels=enhancement&template=feature_request.md&title=)

## License

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"Other"
],
"activationEvents": [
"workspaceContains:**/*.sln"
"workspaceContains:**/*.csproj"
],
"license": "SEE LICENSE IN LICENSE.md",
"bugs": {
Expand Down
4 changes: 2 additions & 2 deletions src/actions/GenerateScriptAction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as vscode from 'vscode';

import { extractDataFromStdOut } from '../cli/ef';
import { getDataFromStdOut } from '../cli/ef';
import { getCommandsConfig } from '../config/config';
import type { TerminalProvider } from '../terminal/TerminalProvider';
import { TerminalAction } from './TerminalAction';
Expand All @@ -24,7 +24,7 @@ export class GenerateScriptAction extends TerminalAction {
}

public async run() {
const output = extractDataFromStdOut(await super.run());
const output = getDataFromStdOut(await super.run());
const uri = vscode.Uri.parse('ef-script:' + output);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.languages.setTextDocumentLanguage(doc, 'sql');
Expand Down
30 changes: 21 additions & 9 deletions src/cli/ef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,32 @@ import { promisify } from 'node:util';
import { getEnvConfig } from '../config/config';

const execAsync = promisify(exec);
const NEWLINE_SEPARATOR = /\r\n|\r|\n/;
const STDOUT_PREFIX = /^[a-z]+: /;

export function extractDataFromStdOut(output: string): string {
export function removePrefixFromStdOut(output: string): string {
return output
.split(/\r\n|\r|\n/)
.filter(line => line.startsWith('data: '))
.map(line => line.replace('data: ', ''))
.split(NEWLINE_SEPARATOR)
.map(line => line.replace(STDOUT_PREFIX, ''))
.join('\n');
}

export function removePrefixFromStdOut(output: string): string {
return output
.split(/\r\n|\r|\n/)
.map(line => line.replace(/^[a-z]+: /, ''))
.join('\n');
export function getDataFromStdOut(output: string): string {
return removePrefixFromStdOut(
output
.split(NEWLINE_SEPARATOR)
.filter(line => line.startsWith('data:'))
.join('\n'),
);
}

export function getErrorsFromStdOut(output: string): string {
return removePrefixFromStdOut(
output
.split(NEWLINE_SEPARATOR)
.filter(line => line.startsWith('error:'))
.join('\n'),
);
}

export async function execEF(
Expand Down
2 changes: 1 addition & 1 deletion src/commands/AddMigrationCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class AddMigrationCommand extends Command {
}
return new AddMigrationAction(
this.terminalProvider,
this.item.solutionFile.workspaceRoot,
this.item.workspaceRoot,
this.item.label,
this.item.project,
).run();
Expand Down
2 changes: 1 addition & 1 deletion src/commands/GenerateScriptCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class GenerateScriptCommand extends Command {
}
return new GenerateScriptAction(
this.terminalProvider,
this.item.solutionFile.workspaceRoot,
this.item.workspaceRoot,
this.item.label,
this.item.project,
).run();
Expand Down
2 changes: 1 addition & 1 deletion src/commands/RemoveMigrationCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class RemoveMigrationCommand extends Command {
}
return new RemoveMigrationAction(
this.terminalProvider,
this.item.solutionFile.workspaceRoot,
this.item.workspaceRoot,
this.item.dbContext,
this.item.project,
).run();
Expand Down
2 changes: 1 addition & 1 deletion src/commands/RunMigrationCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class RunMigrationCommand extends Command {
}
return new RunMigrationAction(
this.terminalProvider,
this.item.solutionFile.workspaceRoot,
this.item.workspaceRoot,
this.item.dbContext,
this.item.project,
this.item.migration.id,
Expand Down
4 changes: 2 additions & 2 deletions src/commands/UndoMigrationCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class UndoMigrationCommand extends Command {
return;
}
const cacheId = DbContextTreeItem.getCacheId(
this.item.solutionFile.workspaceRoot,
this.item.workspaceRoot,
this.item.project,
this.item.dbContext,
);
Expand All @@ -33,7 +33,7 @@ export class UndoMigrationCommand extends Command {
index === 0 ? '0' : migrations[index - 1].migration.id;
return new RunMigrationAction(
this.terminalProvider,
this.item.solutionFile.workspaceRoot,
this.item.workspaceRoot,
this.item.dbContext,
this.item.project,
migrationId,
Expand Down
4 changes: 2 additions & 2 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const DB_CONTEXT_MODEL_SNAPSHOT_SUFFIX = 'ModelSnapshot.cs';

export const EXTENSION_NAMESPACE = 'entityframework';

export const TERMINAL_NAME = 'ef-migrations';
6 changes: 3 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import type * as vscode from 'vscode';
import { TreeDataProvider } from './treeView/TreeDataProvider';
import { CommandProvider } from './commands/CommandProvider';
import { MigrationTreeItemDecorationProvider } from './treeView/MigrationTreeItemDecorationProvider';
import { SolutionFinder } from './solution/SolutionProvider';
import { Terminal } from './terminal/Terminal';
import { TerminalProvider } from './terminal/TerminalProvider';
import { ScriptFileProvider } from './util/ScriptFileProvider';
import { ProjectFilesProvider } from './solution/ProjectFilesProvider';

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

export async function activate(_context: vscode.ExtensionContext) {
const solutionFiles = await SolutionFinder.getSolutionFiles();
const projectFiles = await ProjectFilesProvider.getProjectFiles();
const scriptFileProvider = new ScriptFileProvider();
const migrationTreeItemDecorationProvider =
new MigrationTreeItemDecorationProvider();
const treeDataProvider = new TreeDataProvider(solutionFiles);
const treeDataProvider = new TreeDataProvider(projectFiles);
const terminalProvider = new TerminalProvider(new Terminal());
const commandProvider = new CommandProvider(
treeDataProvider,
Expand Down
36 changes: 36 additions & 0 deletions src/solution/ProjectFilesProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as vscode from 'vscode';
import path from 'node:path';
import { parseProject } from 'vs-parse';
import type { ProjectFile } from '../types/ProjectFile';

export class ProjectFilesProvider {
public static async getProjectFiles() {
const workspaceRoots = vscode.workspace.workspaceFolders || [];

const projects: ProjectFile[] = [];

for (const workspaceRoot of workspaceRoots) {
const projectFiles = await vscode.workspace.findFiles(
new vscode.RelativePattern(workspaceRoot, '**/*.csproj'),
);
for (const projectFile of projectFiles) {
const project = await parseProject(projectFile.fsPath);
const hasEFDesignPackage =
(project.packages || []).find(
pkg => pkg.name === 'Microsoft.EntityFrameworkCore.Design',
) !== undefined;
if (hasEFDesignPackage) {
const projectName = path.basename(path.dirname(projectFile.fsPath));
projects.push({
name: projectName,
project,
workspaceRoot: workspaceRoot.uri.fsPath,
path: projectFile.fsPath,
});
}
}
}

return projects;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { parseSolution } from 'vs-parse';
import path from 'node:path';
import type { SolutionFile } from '../types/SolutionFile';

export class SolutionFinder {
export class SolutionFilesProvider {
public static async getSolutionFiles() {
const workspaceRoots =
vscode.workspace.workspaceFolders?.map(w => w.uri.fsPath) || [];
Expand Down
17 changes: 11 additions & 6 deletions src/terminal/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';

import { EventWaiter } from '../util/EventWaiter';
import { getEnvConfig } from '../config/config';
import { removePrefixFromStdOut } from '../cli/ef';
import { getErrorsFromStdOut, removePrefixFromStdOut } from '../cli/ef';

const NL = '\n';
const CR = '\r';
Expand Down Expand Up @@ -50,7 +50,9 @@ export class Terminal implements vscode.Pseudoterminal {
});

return new Promise(res => {
this.write(this.cmdArgs.join(' ') + '\n');
// --prefix-output is an internal flag that is added to all commands
const argsWithoutPrefixOutput = this.cmdArgs.slice(0, -1);
this.write(argsWithoutPrefixOutput.join(' ') + '\n');

this.cmd?.stdout.on('data', data => {
const dataString = data.toString();
Expand All @@ -64,10 +66,13 @@ export class Terminal implements vscode.Pseudoterminal {
this.write(removePrefixFromStdOut(dataString));
});

this.cmd?.on('exit', code => {
this.cmd?.on('exit', async _code => {
this.cmd = undefined;
this.write(`Exited with code ${code}\n\n`);
res(stderr || stdout);
const error = stderr || getErrorsFromStdOut(stdout);
if (error) {
await vscode.window.showErrorMessage(error);
}
res(stdout);
});
});
}
Expand All @@ -80,7 +85,7 @@ export class Terminal implements vscode.Pseudoterminal {
}

public write(message: string): void {
// We need NLCR to move down and left
// NLCR is required to move down and left
const sanitisedMessage = message.replace(nlRegExp, `${NL + CR}$1`);
this.writeEmitter.fire(sanitisedMessage);
}
Expand Down
5 changes: 2 additions & 3 deletions src/terminal/TerminalProvider.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import * as vscode from 'vscode';
import { TERMINAL_NAME } from '../constants/constants';
import { Disposable } from '../util/Disposable';
import type { Terminal } from './Terminal';

const TERMINAL_NAME = 'ef-migrations';

export class TerminalProvider extends Disposable {
constructor(private readonly terminal: Terminal) {
super();
}

public provideTerminal(): Terminal {
let existingTerminal = vscode.window.terminals.find(
t => t.name === TERMINAL_NAME,
({ name }) => name === TERMINAL_NAME,
);
if (!existingTerminal) {
existingTerminal = vscode.window.createTerminal({
Expand Down
18 changes: 8 additions & 10 deletions src/treeView/DbContextTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { Migration } from '../types/Migration';
import { getIconPath } from './iconProvider';
import { MigrationTreeItem } from './MigrationTreeItem';
import { TreeItem } from './TreeItem';
import { execEF, extractDataFromStdOut } from '../cli/ef';
import { execEF, getDataFromStdOut } from '../cli/ef';
import { TreeItemCache } from './TreeItemCache';
import type { SolutionFile } from '../types/SolutionFile';
import { ContextValues } from './ContextValues';
import type { ProjectFile } from '../types/ProjectFile';

export const dbContextsCache = new TreeItemCache<MigrationTreeItem[]>();

Expand All @@ -15,16 +15,16 @@ export class DbContextTreeItem extends TreeItem {

constructor(
public readonly label: string,
solutionFile: SolutionFile,
private readonly projectFile: ProjectFile,
public readonly project: string,
collapsibleState: vscode.TreeItemCollapsibleState = vscode
.TreeItemCollapsibleState.Collapsed,
) {
super(label, solutionFile, collapsibleState);
super(label, projectFile.workspaceRoot, collapsibleState);
this.iconPath = getIconPath('database_light.svg', 'database_dark.svg');
this.contextValue = ContextValues.dbContext;
this.cacheId = DbContextTreeItem.getCacheId(
solutionFile.workspaceRoot,
projectFile.workspaceRoot,
this.project,
this.label,
);
Expand All @@ -48,16 +48,14 @@ export class DbContextTreeItem extends TreeItem {
try {
const output = await execEF(
`migrations list --context ${this.label} --project ${this.project} --no-color --json --prefix-output`,
this.solutionFile.workspaceRoot,
this.projectFile.workspaceRoot,
);
const migrations = JSON.parse(
extractDataFromStdOut(output),
) as Migration[];
const migrations = JSON.parse(getDataFromStdOut(output)) as Migration[];
const children = migrations.map(
(migration, index) =>
new MigrationTreeItem(
migration.name,
this.solutionFile,
this.projectFile.workspaceRoot,
this.label,
this.project,
migration,
Expand Down
Loading