Skip to content

Commit 0eeb430

Browse files
authored
Merge pull request #14 from simple-frontend-dev/amazing-feedback
improve CLI tool based on user feedback
2 parents 836be24 + b718742 commit 0eeb430

10 files changed

Lines changed: 148 additions & 46 deletions

File tree

.changeset/afraid-facts-decide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"simplefrontend": minor
3+
---
4+
5+
Improve edge cases such as no package managers found

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# simplefrontend, the [Simple Frontend](<[https://](https://www.simplefrontend.dev/)>) CLI
22

3+
A simple and straightforward CLI to help you setup and automate best practices and patterns for your frontend projects (formatting, linting, type checking) with end-to-end recipies (pre-push hook, CI/CD integration).
4+
5+
As the CLI will auto-detect your package manager, the only requirement for it to run is to be within an existing project root folder already installed (where you should have a `package.json` and an existing lock file).
6+
37
## Setup a new frontend project with patterns:
48

59
```bash

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"node": "22.x"
2828
},
2929
"scripts": {
30-
"dev": "pnpm run build && node dist/cli.js",
30+
"dev": "pnpm run build:dev && node dist/cli.js",
31+
"build:dev": "rm -rf dist && tsc --noCheck",
3132
"build": "rm -rf dist && tsc",
3233
"test": "vitest run",
3334
"check:exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",

src/commands/setup.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Argument, type Command } from "commander";
22
import { isCancel, cancel, log, multiselect } from "@clack/prompts";
3+
import { packageManagerError } from "../utils/package-manager.js";
34
import {
45
installSolutions,
56
type Solutions,
@@ -38,6 +39,12 @@ export function setupCommand({ program }: { program: Command }) {
3839
]),
3940
)
4041
.action(async (pattern) => {
42+
// This is a nicer user experience than throwing an exception for the CLI
43+
if (packageManagerError) {
44+
log.error(packageManagerError);
45+
return;
46+
}
47+
4148
if (pattern === "pre-push") {
4249
await installPattern({ pattern: "pre-push" });
4350
} else if (pattern === "github-actions") {

src/solutions/github-actions.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
1-
import { mkdirSync, writeFileSync } from "node:fs";
1+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
22
import { resolve } from "node:path";
33
import { log } from "@clack/prompts";
44
import { type Solutions } from "./install-solutions.js";
5-
import { getGithubActionsTemplate } from "../templates/github-actions.js";
5+
import { appendToWorkflowFile } from "../templates/github-actions.js";
66
import { packageManager } from "../utils/package-manager.js";
77

88
export function setupGithubActions({ solutions }: { solutions: Solutions }) {
99
try {
10-
const template = getGithubActionsTemplate({
11-
agent: packageManager.name,
10+
// step 1: if the folder does not exist, create it
11+
if (!existsSync(resolve("./.github/workflows"))) {
12+
mkdirSync(resolve("./.github/workflows"), { recursive: true });
13+
}
14+
15+
// step 2: if the file already exists, read it to not override the existing configuration
16+
let existingWorkflow = "";
17+
if (existsSync(resolve("./.github/workflows/quality-checks.yml"))) {
18+
existingWorkflow = readFileSync(
19+
resolve("./.github/workflows/quality-checks.yml"),
20+
"utf-8",
21+
);
22+
}
23+
24+
const finalWorkflowConfig = appendToWorkflowFile({
25+
existingWorkflow,
1226
solutions,
27+
agent: packageManager.name,
1328
});
14-
mkdirSync(resolve("./.github/workflows"), { recursive: true });
29+
30+
// step 3: write the new workflow configuration to the file by merging the existing configuration with the template one
1531
writeFileSync(
1632
resolve("./.github/workflows/quality-checks.yml"),
17-
template,
33+
finalWorkflowConfig,
1834
"utf-8",
1935
);
2036
} catch (error: unknown) {

src/solutions/install-solutions.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ export async function installSolutions({
4646

4747
if (prePushHookConfirm) {
4848
setupPrePushHook({ solutions });
49-
log.success("Sucessfully Setup pre-push hook solution: lefthook");
49+
log.success(
50+
"Sucessfully Setup pre-push hook solution: lefthook and created lefthook.yml",
51+
);
5052
}
5153

5254
const githubActionsConfirm = await confirm({
@@ -58,6 +60,8 @@ export async function installSolutions({
5860

5961
if (githubActionsConfirm) {
6062
setupGithubActions({ solutions });
61-
log.success("Successfully setup GitHub Actions workflow");
63+
log.success(
64+
"Successfully setup GitHub Actions workflow and created .github/workflows/quality-checks.yml",
65+
);
6266
}
6367
}

src/solutions/prettier.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function setupPrettier() {
2727
// );
2828
// }
2929

30-
log.success("Successfully setup Prettier");
30+
log.success("Successfully setup Prettier and created .prettierrc");
3131
} catch (error: unknown) {
3232
log.error(
3333
`Failed to install formatting solution: ${PACKAGE} - error: ${error}`,

src/solutions/typescript.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { existsSync, writeFileSync } from "node:fs";
22
import { resolve } from "node:path";
33
import open from "open";
4-
import { cancel, isCancel, log, select } from "@clack/prompts";
4+
import { cancel, confirm, isCancel, log, select } from "@clack/prompts";
55
import { packageManager, installPackage } from "../utils/package-manager.js";
66
import {
77
typeScriptConfigurationLibrary,
@@ -35,15 +35,21 @@ export async function setupTypescript() {
3535
});
3636

3737
if (isCancel(environment)) {
38-
cancel("typescript setup cancelled");
38+
cancel("TypeScript setup cancelled");
3939
return;
4040
}
4141

4242
if (environment === "browser") {
43-
log.info(
44-
"Browser environment has a lot of branching possibilities, I recommend you follow the recommended configurations from Vite starters at https://vite.dev/guide/#trying-vite-online",
45-
);
46-
await open("https://vite.dev/guide/#trying-vite-online");
43+
const openViteStarter = await confirm({
44+
message:
45+
"For browser environments, I recommend you follow the recommended configurations from Vite starters at https://vite.dev/guide/#trying-vite-online. Do you want to open it now?",
46+
});
47+
if (isCancel(openViteStarter)) {
48+
cancel("TypeScript setup cancelled");
49+
return;
50+
} else if (openViteStarter) {
51+
await open("https://vite.dev/guide/#trying-vite-online");
52+
}
4753
} else if (environment === "node") {
4854
// step 4: select the build context
4955
const buildContext = await select({
@@ -56,7 +62,7 @@ export async function setupTypescript() {
5662
});
5763

5864
if (isCancel(buildContext)) {
59-
cancel("typescript setup cancelled");
65+
cancel("TypeScript setup cancelled");
6066
return;
6167
}
6268

@@ -91,9 +97,8 @@ export async function setupTypescript() {
9197
JSON.stringify(typeScriptConfigurationServer, null, 2),
9298
);
9399
}
94-
95-
log.success("Successfully setup TypeScript");
96100
}
101+
log.success("Successfully setup TypeScript and created tsconfig.json");
97102
} catch (error: unknown) {
98103
log.error(`Failed to install typescript - error: ${error}`);
99104
}

src/templates/github-actions.ts

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,42 @@ function getAgentActionSetup(agent: AgentName) {
3636
];
3737
}
3838
}
39-
export function getGithubActionsTemplate({
39+
40+
function getSetupSteps(agent: AgentName) {
41+
return [
42+
{
43+
uses: "actions/checkout@v4",
44+
},
45+
...getAgentActionSetup(agent),
46+
{
47+
name: "Install dependencies",
48+
run: `${agent} install`,
49+
},
50+
];
51+
}
52+
53+
function getPrettierActionSetup(agent: AgentName) {
54+
return {
55+
name: "Run code quality format check",
56+
run: `${getExecCommand(agent)} prettier --check --ignore-unknown src`,
57+
};
58+
}
59+
60+
function getEslintActionSetup(agent: AgentName) {
61+
return {
62+
name: "Run code quality lint check",
63+
run: `${getExecCommand(agent)} eslint --no-warn-ignored src`,
64+
};
65+
}
66+
67+
function getGithubActionsTemplate({
4068
agent,
4169
solutions,
4270
}: {
4371
agent: AgentName;
4472
solutions: Solutions;
4573
}) {
46-
return yaml.stringify({
74+
return {
4775
name: "Continuous Integration",
4876
on: {
4977
pull_request: {
@@ -58,32 +86,62 @@ export function getGithubActionsTemplate({
5886
"quality-checks": {
5987
"runs-on": "ubuntu-latest",
6088
steps: [
61-
{
62-
uses: "actions/checkout@v4",
63-
},
64-
...getAgentActionSetup(agent),
65-
{
66-
name: "Install dependencies",
67-
run: `${agent} install`,
68-
},
89+
...getSetupSteps(agent),
6990
...(solutions.includes("prettier")
70-
? [
71-
{
72-
name: "Run code quality format check",
73-
run: `${getExecCommand(agent)} prettier --check --ignore-unknown src`,
74-
},
75-
]
91+
? [getPrettierActionSetup(agent)]
7692
: []),
7793
...(solutions.includes("eslint")
78-
? [
79-
{
80-
name: "Run code quality lint check",
81-
run: `${getExecCommand(agent)} eslint --no-warn-ignored src`,
82-
},
83-
]
94+
? [getEslintActionSetup(agent)]
8495
: []),
8596
],
8697
},
8798
},
99+
};
100+
}
101+
102+
export function appendToWorkflowFile({
103+
existingWorkflow,
104+
solutions,
105+
agent,
106+
}: {
107+
existingWorkflow: string;
108+
solutions: Solutions;
109+
agent: AgentName;
110+
}) {
111+
// If the workflow file is empty, return the template directly
112+
if (!existingWorkflow) {
113+
return yaml.stringify(getGithubActionsTemplate({ agent, solutions }));
114+
}
115+
116+
// extend existing steps with prettier and/or eslint
117+
const config = yaml.parse(existingWorkflow) ?? {};
118+
const template = getGithubActionsTemplate({ agent, solutions });
119+
120+
const existingSteps: Array<{ name: string; run: string }> =
121+
config.jobs?.["quality-checks"]?.steps ?? [];
122+
123+
const steps = [
124+
...existingSteps,
125+
...(solutions.includes("prettier") &&
126+
!existingSteps.find((step) => step.name === "Run code quality format check")
127+
? [getPrettierActionSetup(agent)]
128+
: []),
129+
...(solutions.includes("eslint") &&
130+
!existingSteps.find((step) => step.name === "Run code quality lint check")
131+
? [getEslintActionSetup(agent)]
132+
: []),
133+
];
134+
135+
return yaml.stringify({
136+
...template,
137+
...config,
138+
jobs: {
139+
...config.jobs,
140+
"quality-checks": {
141+
"runs-on": "ubuntu-latest",
142+
...(config.jobs?.["quality-checks"] ?? {}),
143+
steps,
144+
},
145+
},
88146
});
89147
}

src/utils/package-manager.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@ import { execSync } from "node:child_process";
55

66
class PackageManagerDetector {
77
public packageManager: DetectResult;
8+
public packageManagerError: string | null = null;
89

910
constructor() {
1011
const packageManager = detectSync();
1112
if (!packageManager) {
12-
throw new Error(
13-
"Not able to detect your package manager, make sure you have a lock file on disk first.",
14-
);
13+
this.packageManagerError =
14+
"Unable to detect your package manager: simplefrontend CLI needs to run in an existing project root folder which is already installed (where you should have a `package.json` and an existing lock file).";
1515
}
16-
this.packageManager = packageManager;
16+
// this is unsafe but we stop on errors within commands
17+
this.packageManager = packageManager as DetectResult;
1718
}
1819
}
1920

20-
export const packageManager = new PackageManagerDetector().packageManager;
21+
export const { packageManager, packageManagerError } =
22+
new PackageManagerDetector();
2123

2224
function getDevDependencyArg(agent: AgentName) {
2325
switch (agent) {

0 commit comments

Comments
 (0)