Skip to content

Commit ed07e12

Browse files
authored
feat(vscode): include create command (#767)
1 parent b71baeb commit ed07e12

File tree

6 files changed

+337
-7
lines changed

6 files changed

+337
-7
lines changed

extensions/vscode/README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ Dart Frog can be installed from the [VS Code Marketplace](https://marketplace.vi
1111

1212
## Commands
1313

14-
| Command | Description |
15-
| --------------------------- | -------------------------- |
16-
| `Dart Frog: Install CLI` | Installs Dart Frog CLI |
17-
| `Dart Frog: Update CLI` | Updates Dart Frog CLI |
18-
| `Dart Frog: New Route` | Generates a new route |
19-
| `Dart Frog: New Middleware` | Generates a new middleware |
14+
| Command | Description |
15+
| --------------------------- | --------------------------- |
16+
| `Dart Frog: Create` | Creates a new Dart Frog app |
17+
| `Dart Frog: Install CLI` | Installs Dart Frog CLI |
18+
| `Dart Frog: Update CLI` | Updates Dart Frog CLI |
19+
| `Dart Frog: New Route` | Generates a new route |
20+
| `Dart Frog: New Middleware` | Generates a new middleware |
2021

2122
You can activate the commands by launching the command palette (View -> Command Palette) and entering the command name or you can right click on the directory or file in which you'd like to create the route and select the command from the context menu.
2223

extensions/vscode/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
"main": "./out/extension.js",
3232
"contributes": {
3333
"commands": [
34+
{
35+
"command": "extension.create",
36+
"title": "Dart Frog: Create Project"
37+
},
3438
{
3539
"command": "extension.install-cli",
3640
"title": "Dart Frog: Install CLI"
@@ -59,6 +63,11 @@
5963
"command": "extension.new-middleware",
6064
"group": "dartFrogGroup@1",
6165
"when": "resourceDirname =~ /routes/ && (explorerResourceIsFolder || resourceExtname == .dart)"
66+
},
67+
{
68+
"command": "extension.create",
69+
"group": "dartFrogGroup@2",
70+
"when": "!(resourceDirname =~ /routes/)"
6271
}
6372
]
6473
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
const cp = require("child_process");
2+
const path = require("node:path");
3+
4+
import {
5+
Uri,
6+
window,
7+
InputBoxOptions,
8+
ProgressOptions,
9+
OpenDialogOptions,
10+
} from "vscode";
11+
12+
/**
13+
* Creates a new Dart Frog project.
14+
*
15+
* This command is available from the command palette and the context menu.
16+
*
17+
* When launching the command from the command palette, the Uri is undefined
18+
* and the user is prompted to select a valid directory or file to create the
19+
* project in.
20+
*
21+
* When launching the command from the context menu, the Uri corresponds to the
22+
* selected file or directory. Only those directories that are not Dart Frog
23+
* projects already show the command in the context menu.
24+
*
25+
* The user will always be prompted for a project name, which is pre-filled with
26+
* the name of the directory the project will be created in. Mimicking the
27+
* Dart Frog CLI create command implementation.
28+
*
29+
* All the logic associated with creating a new project is handled by the
30+
* `dart_frog create` command, from the Dart Frog CLI.
31+
*
32+
* @param {Uri | undefined} uri
33+
* @see {@link https://github.com/VeryGoodOpenSource/dart_frog/blob/main/packages/dart_frog_cli/lib/src/commands/create/create.dart Dart Frog CLI `create` command implementation.}
34+
*/
35+
export const create = async (uri: Uri | undefined): Promise<void> => {
36+
let outputDirectory =
37+
uri === undefined ? await promptForTargetDirectory() : uri.fsPath;
38+
39+
if (outputDirectory === undefined) {
40+
return;
41+
}
42+
43+
let projectName = path.basename(path.normalize(outputDirectory));
44+
projectName = await promptProjectName(projectName);
45+
46+
if (projectName === undefined || projectName.trim() === "") {
47+
window.showErrorMessage("Please enter a project name");
48+
return;
49+
}
50+
51+
const options: ProgressOptions = {
52+
location: 15,
53+
title: `Creating ${projectName} Dart Frog Project...`,
54+
};
55+
window.withProgress(options, async function () {
56+
executeDartFrogCreateCommand(outputDirectory!, projectName);
57+
});
58+
};
59+
60+
/**
61+
* Shows an open dialog to the user and returns a Promise that resolves
62+
* to a string when the user selects a folder or file.
63+
*
64+
* This is used when the user activates the command from the command palette
65+
* instead of the context menu.
66+
*
67+
* @returns The path to the selected folder or file or undefined if the user
68+
* canceled.
69+
*/
70+
async function promptForTargetDirectory(): Promise<string | undefined> {
71+
const options: OpenDialogOptions = {
72+
canSelectMany: false,
73+
openLabel: "Select a folder or file to create the project in",
74+
canSelectFolders: true,
75+
canSelectFiles: true,
76+
};
77+
return window.showOpenDialog(options).then((uri) => {
78+
if (Array.isArray(uri) && uri.length > 0) {
79+
return uri[0].fsPath;
80+
}
81+
82+
return undefined;
83+
});
84+
}
85+
86+
/**
87+
* Shows an input box to the user and returns a Thenable that resolves to
88+
* a string the user provided.
89+
*
90+
* @param {string} value The default value to show in the input box.
91+
* @returns The route name the user provided or undefined if the user canceled.
92+
*/
93+
function promptProjectName(value: string): Thenable<string | undefined> {
94+
const inputBoxOptions: InputBoxOptions = {
95+
prompt: "Project name",
96+
value: value,
97+
};
98+
return window.showInputBox(inputBoxOptions);
99+
}
100+
101+
async function executeDartFrogCreateCommand(
102+
outputDirectory: String,
103+
projectName: string
104+
): Promise<void> {
105+
return cp.exec(
106+
`dart_frog create '${projectName}'`,
107+
{
108+
cwd: outputDirectory,
109+
},
110+
function (error: Error, stdout: String, stderr: String) {
111+
if (error) {
112+
window.showErrorMessage(error.message);
113+
}
114+
}
115+
);
116+
}

extensions/vscode/src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./create";
12
export * from "./install-cli";
23
export * from "./update-cli";
34
export * from "./new-route";

extensions/vscode/src/extension.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import * as vscode from "vscode";
2-
import { installCLI, newRoute, newMiddleware, updateCLI } from "./commands";
2+
import {
3+
installCLI,
4+
newRoute,
5+
newMiddleware,
6+
updateCLI,
7+
create,
8+
} from "./commands";
39
import {
410
readDartFrogCLIVersion,
511
isCompatibleDartFrogCLIVersion,
@@ -26,6 +32,7 @@ export function activate(
2632
}
2733

2834
context.subscriptions.push(
35+
vscode.commands.registerCommand("extension.create", create),
2936
vscode.commands.registerCommand("extension.install-cli", installCLI),
3037
vscode.commands.registerCommand("extension.update-cli", updateCLI),
3138
vscode.commands.registerCommand("extension.new-route", newRoute),
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
const sinon = require("sinon");
2+
var proxyquire = require("proxyquire");
3+
4+
import { afterEach, beforeEach } from "mocha";
5+
6+
suite("create command", () => {
7+
const targetUri = { fsPath: "/home/dart_frog" };
8+
9+
let vscodeStub: any;
10+
let childProcessStub: any;
11+
let command: any;
12+
13+
beforeEach(() => {
14+
vscodeStub = {
15+
window: {
16+
showErrorMessage: sinon.stub(),
17+
showInputBox: sinon.stub(),
18+
showOpenDialog: sinon.stub(),
19+
withProgress: sinon.stub(),
20+
},
21+
};
22+
childProcessStub = {
23+
exec: sinon.stub(),
24+
};
25+
26+
command = proxyquire("../../../commands/create", {
27+
vscode: vscodeStub,
28+
// eslint-disable-next-line @typescript-eslint/naming-convention
29+
child_process: childProcessStub,
30+
});
31+
});
32+
33+
afterEach(() => {
34+
sinon.restore();
35+
});
36+
37+
test("project input box is shown with directory name as value", async () => {
38+
await command.create(targetUri);
39+
40+
sinon.assert.calledOnceWithExactly(vscodeStub.window.showInputBox, {
41+
prompt: "Project name",
42+
value: "dart_frog",
43+
});
44+
});
45+
46+
suite("file open dialog", () => {
47+
test("is shown when Uri is undefined", async () => {
48+
vscodeStub.window.showOpenDialog.returns(Promise.resolve(undefined));
49+
50+
await command.create();
51+
52+
sinon.assert.calledOnceWithExactly(vscodeStub.window.showOpenDialog, {
53+
canSelectMany: false,
54+
openLabel: "Select a folder or file to create the project in",
55+
canSelectFolders: true,
56+
canSelectFiles: true,
57+
});
58+
});
59+
60+
test("is not shown when Uri is defined", async () => {
61+
await command.create(targetUri);
62+
63+
sinon.assert.notCalled(vscodeStub.window.showOpenDialog);
64+
});
65+
});
66+
67+
suite("error message", () => {
68+
test("is shown when prompt is undefined", async () => {
69+
vscodeStub.window.showInputBox.returns(undefined);
70+
71+
await command.create(targetUri);
72+
73+
sinon.assert.calledOnceWithExactly(
74+
vscodeStub.window.showErrorMessage,
75+
"Please enter a project name"
76+
);
77+
});
78+
79+
test("is shown when prompt is empty", async () => {
80+
vscodeStub.window.showInputBox.returns("");
81+
82+
await command.create(targetUri);
83+
84+
sinon.assert.calledOnceWithExactly(
85+
vscodeStub.window.showErrorMessage,
86+
"Please enter a project name"
87+
);
88+
});
89+
90+
test("is shown when prompt is white spaced", async () => {
91+
vscodeStub.window.showInputBox.returns(" ");
92+
93+
await command.create(targetUri);
94+
95+
sinon.assert.calledOnceWithExactly(
96+
vscodeStub.window.showErrorMessage,
97+
"Please enter a project name"
98+
);
99+
});
100+
101+
test("is not shown when prompt is valid", async () => {
102+
vscodeStub.window.showInputBox.returns("my_project");
103+
104+
await command.create(targetUri);
105+
106+
sinon.assert.neverCalledWith(
107+
vscodeStub.window.showErrorMessage,
108+
"Please enter a project name"
109+
);
110+
});
111+
});
112+
113+
suite("progress", () => {
114+
test("is shown when prompt is valid", async () => {
115+
const projectName = "my_project";
116+
vscodeStub.window.showInputBox.returns(projectName);
117+
118+
await command.create(targetUri);
119+
120+
sinon.assert.calledOnceWithMatch(vscodeStub.window.withProgress, {
121+
location: 15,
122+
title: `Creating ${projectName} Dart Frog Project...`,
123+
});
124+
});
125+
126+
test("is not shown when prompt is undefined", async () => {
127+
vscodeStub.window.showInputBox.returns(undefined);
128+
129+
await command.create(targetUri);
130+
131+
sinon.assert.notCalled(vscodeStub.window.withProgress);
132+
});
133+
134+
test("is not shown when prompt is empty", async () => {
135+
vscodeStub.window.showInputBox.returns("");
136+
137+
await command.create(targetUri);
138+
139+
sinon.assert.notCalled(vscodeStub.window.withProgress);
140+
});
141+
142+
test("is not shown when prompt is white spaced", async () => {
143+
vscodeStub.window.showInputBox.returns(" ");
144+
145+
await command.create(targetUri);
146+
147+
sinon.assert.notCalled(vscodeStub.window.withProgress);
148+
});
149+
});
150+
151+
test("runs `dart_frog create` command when project name is valid and uri is defined", async () => {
152+
vscodeStub.window.showInputBox.returns("my_project");
153+
154+
await command.create(targetUri);
155+
156+
const progressFunction = vscodeStub.window.withProgress.getCall(0).args[1];
157+
await progressFunction();
158+
159+
sinon.assert.calledOnceWithMatch(
160+
childProcessStub.exec,
161+
"dart_frog create 'my_project'",
162+
{ cwd: targetUri.fsPath }
163+
);
164+
});
165+
166+
test("runs `dart_frog create` command when project name is valid and uri not defined", async () => {
167+
vscodeStub.window.showOpenDialog.returns(Promise.resolve([targetUri]));
168+
vscodeStub.window.showInputBox.returns("my_project");
169+
170+
await command.create();
171+
172+
const progressFunction = vscodeStub.window.withProgress.getCall(0).args[1];
173+
await progressFunction();
174+
175+
sinon.assert.calledOnceWithMatch(
176+
childProcessStub.exec,
177+
"dart_frog create 'my_project'",
178+
{ cwd: targetUri.fsPath }
179+
);
180+
});
181+
182+
test("shows error when `dart_frog create` command fails", async () => {
183+
const error = new Error("Command failed");
184+
const createCommand = "dart_frog create 'my_project'";
185+
186+
vscodeStub.window.showInputBox.returns("my_project");
187+
childProcessStub.exec.withArgs(createCommand).yields(error);
188+
189+
await command.create(targetUri);
190+
191+
const progressFunction = vscodeStub.window.withProgress.getCall(0).args[1];
192+
await progressFunction();
193+
194+
sinon.assert.calledWith(vscodeStub.window.showErrorMessage, error.message);
195+
});
196+
});

0 commit comments

Comments
 (0)