letsrunit 0.0.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +315 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +301 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -3
- package/src/bin.ts +16 -0
- package/src/detect.ts +47 -0
- package/src/index.ts +2 -0
- package/src/init.ts +113 -0
- package/src/setup/cli.ts +24 -0
- package/src/setup/cucumber.ts +114 -0
- package/src/setup/github-actions.ts +69 -0
- package/src/setup/playwright.ts +28 -0
- package/.editorconfig +0 -8
- package/.gitattributes +0 -4
- package/README.md +0 -1
package/dist/bin.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/init.ts
|
|
4
|
+
import { confirm, intro, log, note, outro, spinner } from "@clack/prompts";
|
|
5
|
+
|
|
6
|
+
// src/detect.ts
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
function detectEnvironment() {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
13
|
+
let packageManager = "npm";
|
|
14
|
+
if (existsSync(join(cwd, "yarn.lock"))) packageManager = "yarn";
|
|
15
|
+
else if (existsSync(join(cwd, "pnpm-lock.yaml"))) packageManager = "pnpm";
|
|
16
|
+
else if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) packageManager = "bun";
|
|
17
|
+
const nodeVersion = parseInt(process.version.slice(1), 10);
|
|
18
|
+
const hasCucumber = existsSync(join(cwd, "node_modules", "@cucumber", "cucumber", "package.json"));
|
|
19
|
+
return { isInteractive, packageManager, nodeVersion, hasCucumber, cwd };
|
|
20
|
+
}
|
|
21
|
+
function pmCmd(pm, args2) {
|
|
22
|
+
if (pm === "yarn") return `yarn ${args2.yarn}`;
|
|
23
|
+
if (pm === "pnpm") return `pnpm ${args2.pnpm}`;
|
|
24
|
+
if (pm === "bun") return `bun ${args2.bun}`;
|
|
25
|
+
return `npm ${args2.npm}`;
|
|
26
|
+
}
|
|
27
|
+
function execPm(env, args2) {
|
|
28
|
+
const cmd = pmCmd(env.packageManager, args2);
|
|
29
|
+
return execSync(cmd, { stdio: "inherit", cwd: env.cwd });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/setup/cli.ts
|
|
33
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
34
|
+
import { join as join2 } from "path";
|
|
35
|
+
function isCliInstalled({ cwd }) {
|
|
36
|
+
const pkgPath = join2(cwd, "package.json");
|
|
37
|
+
if (!existsSync2(pkgPath)) return false;
|
|
38
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
39
|
+
return "@letsrunit/cli" in (pkg.devDependencies ?? {}) || "@letsrunit/cli" in (pkg.dependencies ?? {});
|
|
40
|
+
}
|
|
41
|
+
function installCli(env) {
|
|
42
|
+
execPm(env, {
|
|
43
|
+
npm: "install --save-dev @letsrunit/cli",
|
|
44
|
+
yarn: "add --dev @letsrunit/cli",
|
|
45
|
+
pnpm: "add -D @letsrunit/cli",
|
|
46
|
+
bun: "add -d @letsrunit/cli"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/setup/cucumber.ts
|
|
51
|
+
import { existsSync as existsSync3, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
52
|
+
import { join as join3 } from "path";
|
|
53
|
+
var BDD_IMPORT = "@letsrunit/bdd/define";
|
|
54
|
+
var CUCUMBER_CONFIG = `export default {
|
|
55
|
+
worldParameters: {
|
|
56
|
+
baseURL: 'http://localhost:3000',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
`;
|
|
60
|
+
var SUPPORT_FILE = `import { setDefaultTimeout } from '@cucumber/cucumber';
|
|
61
|
+
import '${BDD_IMPORT}';
|
|
62
|
+
|
|
63
|
+
setDefaultTimeout(30_000);
|
|
64
|
+
`;
|
|
65
|
+
var EXAMPLE_FEATURE = `Feature: Example
|
|
66
|
+
Scenario: Homepage loads
|
|
67
|
+
Given I'm on the homepage
|
|
68
|
+
Then The page contains heading "Welcome"
|
|
69
|
+
`;
|
|
70
|
+
function installCucumber(env) {
|
|
71
|
+
execPm(env, {
|
|
72
|
+
npm: "install --save-dev @cucumber/cucumber",
|
|
73
|
+
yarn: "add --dev @cucumber/cucumber",
|
|
74
|
+
pnpm: "add -D @cucumber/cucumber",
|
|
75
|
+
bun: "add -d @cucumber/cucumber"
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function installBdd(env) {
|
|
79
|
+
const pkgPath = join3(env.cwd, "package.json");
|
|
80
|
+
if (!existsSync3(pkgPath)) return false;
|
|
81
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
82
|
+
const alreadyInstalled = "@letsrunit/bdd" in (pkg.devDependencies ?? {}) || "@letsrunit/bdd" in (pkg.dependencies ?? {});
|
|
83
|
+
if (alreadyInstalled) return false;
|
|
84
|
+
execPm(env, {
|
|
85
|
+
npm: "install --save-dev @letsrunit/bdd",
|
|
86
|
+
yarn: "add --dev @letsrunit/bdd",
|
|
87
|
+
pnpm: "add -D @letsrunit/bdd",
|
|
88
|
+
bun: "add -d @letsrunit/bdd"
|
|
89
|
+
});
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function setupCucumberConfig({ cwd }) {
|
|
93
|
+
const supportDir = join3(cwd, "features", "support");
|
|
94
|
+
const supportPath = join3(supportDir, "world.js");
|
|
95
|
+
if (existsSync3(supportPath)) {
|
|
96
|
+
const content = readFileSync2(supportPath, "utf-8");
|
|
97
|
+
if (content.includes(BDD_IMPORT)) return "skipped";
|
|
98
|
+
return "needs-manual-update";
|
|
99
|
+
}
|
|
100
|
+
const configPath = join3(cwd, "cucumber.js");
|
|
101
|
+
if (!existsSync3(configPath)) {
|
|
102
|
+
writeFileSync(configPath, CUCUMBER_CONFIG, "utf-8");
|
|
103
|
+
}
|
|
104
|
+
mkdirSync(supportDir, { recursive: true });
|
|
105
|
+
writeFileSync(supportPath, SUPPORT_FILE, "utf-8");
|
|
106
|
+
return "created";
|
|
107
|
+
}
|
|
108
|
+
function setupFeaturesDir({ cwd }) {
|
|
109
|
+
if (existsSync3(join3(cwd, "features"))) {
|
|
110
|
+
try {
|
|
111
|
+
const hasFeatureFiles = readdirSync(join3(cwd, "features")).some((f) => f.endsWith(".feature"));
|
|
112
|
+
if (hasFeatureFiles) return false;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const hasFeatureAtRoot = readdirSync(cwd).some((f) => f.endsWith(".feature"));
|
|
119
|
+
if (hasFeatureAtRoot) return false;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
mkdirSync(join3(cwd, "features"), { recursive: true });
|
|
124
|
+
writeFileSync(join3(cwd, "features", "example.feature"), EXAMPLE_FEATURE, "utf-8");
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
function setupCucumber(env) {
|
|
128
|
+
const bddInstalled = installBdd(env);
|
|
129
|
+
const configResult = setupCucumberConfig(env);
|
|
130
|
+
const featuresCreated = setupFeaturesDir(env);
|
|
131
|
+
return { bddInstalled, configResult, featuresCreated };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/setup/github-actions.ts
|
|
135
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
136
|
+
import { join as join4 } from "path";
|
|
137
|
+
function setupStepsFor({ packageManager, nodeVersion }) {
|
|
138
|
+
if (packageManager === "yarn") return ` - name: Enable Corepack
|
|
139
|
+
run: corepack enable
|
|
140
|
+
- uses: actions/setup-node@v4
|
|
141
|
+
with:
|
|
142
|
+
node-version: ${nodeVersion}
|
|
143
|
+
cache: yarn
|
|
144
|
+
- name: Install dependencies
|
|
145
|
+
run: yarn install --immutable`;
|
|
146
|
+
if (packageManager === "pnpm") return ` - uses: pnpm/action-setup@v4
|
|
147
|
+
- uses: actions/setup-node@v4
|
|
148
|
+
with:
|
|
149
|
+
node-version: ${nodeVersion}
|
|
150
|
+
cache: pnpm
|
|
151
|
+
- name: Install dependencies
|
|
152
|
+
run: pnpm install --frozen-lockfile`;
|
|
153
|
+
if (packageManager === "bun") return ` - uses: oven-sh/setup-bun@v2
|
|
154
|
+
- name: Install dependencies
|
|
155
|
+
run: bun install --frozen-lockfile`;
|
|
156
|
+
return ` - uses: actions/setup-node@v4
|
|
157
|
+
with:
|
|
158
|
+
node-version: ${nodeVersion}
|
|
159
|
+
cache: npm
|
|
160
|
+
- name: Install dependencies
|
|
161
|
+
run: npm ci`;
|
|
162
|
+
}
|
|
163
|
+
function workflowYaml(env) {
|
|
164
|
+
const setupSteps = setupStepsFor(env);
|
|
165
|
+
return `name: Features
|
|
166
|
+
on:
|
|
167
|
+
push:
|
|
168
|
+
branches: [main]
|
|
169
|
+
pull_request:
|
|
170
|
+
branches: [main]
|
|
171
|
+
jobs:
|
|
172
|
+
features:
|
|
173
|
+
runs-on: ubuntu-latest
|
|
174
|
+
steps:
|
|
175
|
+
- uses: actions/checkout@v4
|
|
176
|
+
${setupSteps}
|
|
177
|
+
- name: Install Playwright browsers
|
|
178
|
+
run: npx playwright install chromium --with-deps
|
|
179
|
+
- name: Run features
|
|
180
|
+
run: npx cucumber-js
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
function installGithubAction(env) {
|
|
184
|
+
const workflowDir = join4(env.cwd, ".github", "workflows");
|
|
185
|
+
const workflowPath = join4(workflowDir, "letsrunit.yml");
|
|
186
|
+
if (existsSync4(workflowPath)) return "skipped";
|
|
187
|
+
mkdirSync2(workflowDir, { recursive: true });
|
|
188
|
+
writeFileSync2(workflowPath, workflowYaml(env), "utf-8");
|
|
189
|
+
return "created";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/setup/playwright.ts
|
|
193
|
+
import { execSync as execSync2 } from "child_process";
|
|
194
|
+
import { existsSync as existsSync5 } from "fs";
|
|
195
|
+
import { join as join5 } from "path";
|
|
196
|
+
function hasPlaywrightBrowsers({ cwd }) {
|
|
197
|
+
if (!existsSync5(join5(cwd, "node_modules", "playwright-core", "package.json"))) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const execPath = execSync2(
|
|
202
|
+
`node -e "console.log(require('playwright-core').chromium.executablePath())"`,
|
|
203
|
+
{ cwd, stdio: "pipe", encoding: "utf-8" }
|
|
204
|
+
).trim();
|
|
205
|
+
return existsSync5(execPath);
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function installPlaywrightBrowsers(env) {
|
|
211
|
+
execPm(env, {
|
|
212
|
+
npm: "exec playwright install chromium",
|
|
213
|
+
yarn: "exec playwright install chromium",
|
|
214
|
+
pnpm: "exec playwright install chromium",
|
|
215
|
+
bun: "x playwright install chromium"
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/init.ts
|
|
220
|
+
var BDD_IMPORT2 = "@letsrunit/bdd/define";
|
|
221
|
+
async function stepInstallCli(env) {
|
|
222
|
+
if (isCliInstalled(env)) {
|
|
223
|
+
log.success("@letsrunit/cli already installed");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const s = spinner();
|
|
227
|
+
s.start("Installing @letsrunit/cli\u2026");
|
|
228
|
+
installCli(env);
|
|
229
|
+
s.stop("@letsrunit/cli installed");
|
|
230
|
+
}
|
|
231
|
+
async function stepEnsureCucumber(env, { yes: yes2 }) {
|
|
232
|
+
if (env.hasCucumber) return true;
|
|
233
|
+
if (!yes2 && !env.isInteractive) {
|
|
234
|
+
log.warn("@cucumber/cucumber not found. Install it to use letsrunit with Cucumber:");
|
|
235
|
+
note("npm install --save-dev @cucumber/cucumber\nThen run: npx letsrunit init", "Setup Cucumber");
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
if (!yes2) {
|
|
239
|
+
const install = await confirm({ message: "@cucumber/cucumber not found. Install it now?" });
|
|
240
|
+
if (install !== true) return false;
|
|
241
|
+
}
|
|
242
|
+
const s = spinner();
|
|
243
|
+
s.start("Installing @cucumber/cucumber\u2026");
|
|
244
|
+
installCucumber(env);
|
|
245
|
+
s.stop("@cucumber/cucumber installed");
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
function stepSetupCucumber(env) {
|
|
249
|
+
const result = setupCucumber(env);
|
|
250
|
+
if (result.bddInstalled) log.success("@letsrunit/bdd installed");
|
|
251
|
+
if (result.configResult === "created") {
|
|
252
|
+
log.success("features/support/world.js created");
|
|
253
|
+
} else if (result.configResult === "needs-manual-update") {
|
|
254
|
+
log.warn("features/support/world.js exists but does not import @letsrunit/bdd.");
|
|
255
|
+
note(`Add "import '${BDD_IMPORT2}';" to features/support/world.js`, "Action required");
|
|
256
|
+
}
|
|
257
|
+
if (result.featuresCreated) log.success("features/ directory created with example.feature");
|
|
258
|
+
}
|
|
259
|
+
async function stepCheckPlaywrightBrowsers(env, { yes: yes2 }) {
|
|
260
|
+
if (hasPlaywrightBrowsers(env)) return;
|
|
261
|
+
if (!yes2 && !env.isInteractive) {
|
|
262
|
+
log.warn("Playwright Chromium browser not found.");
|
|
263
|
+
note("npx playwright install chromium", "Run to install browsers");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!yes2) {
|
|
267
|
+
const install = await confirm({ message: "Playwright Chromium browser not found. Install it now?" });
|
|
268
|
+
if (install !== true) return;
|
|
269
|
+
}
|
|
270
|
+
const s = spinner();
|
|
271
|
+
s.start("Installing Playwright Chromium\u2026");
|
|
272
|
+
installPlaywrightBrowsers(env);
|
|
273
|
+
s.stop("Playwright Chromium installed");
|
|
274
|
+
}
|
|
275
|
+
async function stepAddGithubAction(env, { yes: yes2 }) {
|
|
276
|
+
if (!yes2 && !env.isInteractive) return;
|
|
277
|
+
if (!yes2) {
|
|
278
|
+
const addAction = await confirm({ message: "Add a GitHub Action to run features on push?" });
|
|
279
|
+
if (addAction !== true) return;
|
|
280
|
+
}
|
|
281
|
+
const result = installGithubAction(env);
|
|
282
|
+
if (result === "created") {
|
|
283
|
+
log.success(".github/workflows/letsrunit.yml created");
|
|
284
|
+
} else {
|
|
285
|
+
log.info(".github/workflows/letsrunit.yml already exists, skipped");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function init(options = {}) {
|
|
289
|
+
intro("letsrunit init");
|
|
290
|
+
const env = detectEnvironment();
|
|
291
|
+
await stepInstallCli(env);
|
|
292
|
+
const hasCucumber = await stepEnsureCucumber(env, options);
|
|
293
|
+
if (hasCucumber) {
|
|
294
|
+
stepSetupCucumber(env);
|
|
295
|
+
await stepCheckPlaywrightBrowsers(env, options);
|
|
296
|
+
await stepAddGithubAction(env, options);
|
|
297
|
+
}
|
|
298
|
+
outro("All done! Run npx letsrunit --help to get started.");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/bin.ts
|
|
302
|
+
var args = process.argv.slice(2);
|
|
303
|
+
var command = args.find((a) => !a.startsWith("-")) ?? "init";
|
|
304
|
+
var yes = args.includes("--yes") || args.includes("-y");
|
|
305
|
+
if (command === "init") {
|
|
306
|
+
init({ yes }).catch((err) => {
|
|
307
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
308
|
+
process.exit(1);
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
console.error(`Unknown command: ${command}`);
|
|
312
|
+
console.error("Usage: letsrunit init [--yes]");
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/init.ts","../src/detect.ts","../src/setup/cli.ts","../src/setup/cucumber.ts","../src/setup/github-actions.ts","../src/setup/playwright.ts","../src/bin.ts"],"sourcesContent":["import { confirm, intro, log, note, outro, spinner } from '@clack/prompts';\nimport { detectEnvironment, type Environment } from './detect.js';\nimport { installCli, isCliInstalled } from './setup/cli.js';\nimport { installCucumber, setupCucumber } from './setup/cucumber.js';\nimport { installGithubAction } from './setup/github-actions.js';\nimport { hasPlaywrightBrowsers, installPlaywrightBrowsers } from './setup/playwright.js';\n\nconst BDD_IMPORT = '@letsrunit/bdd/define';\n\nexport interface InitOptions {\n yes?: boolean;\n}\n\nasync function stepInstallCli(env: Environment): Promise<void> {\n if (isCliInstalled(env)) {\n log.success('@letsrunit/cli already installed');\n return;\n }\n\n const s = spinner();\n s.start('Installing @letsrunit/cli…');\n installCli(env);\n s.stop('@letsrunit/cli installed');\n}\n\nasync function stepEnsureCucumber(env: Environment, { yes }: InitOptions): Promise<boolean> {\n if (env.hasCucumber) return true;\n\n if (!yes && !env.isInteractive) {\n log.warn('@cucumber/cucumber not found. Install it to use letsrunit with Cucumber:');\n note('npm install --save-dev @cucumber/cucumber\\nThen run: npx letsrunit init', 'Setup Cucumber');\n return false;\n }\n\n if (!yes) {\n const install = await confirm({ message: '@cucumber/cucumber not found. Install it now?' });\n if (install !== true) return false;\n }\n\n const s = spinner();\n s.start('Installing @cucumber/cucumber…');\n installCucumber(env);\n s.stop('@cucumber/cucumber installed');\n return true;\n}\n\nfunction stepSetupCucumber(env: Environment): void {\n const result = setupCucumber(env);\n\n if (result.bddInstalled) log.success('@letsrunit/bdd installed');\n\n if (result.configResult === 'created') {\n log.success('features/support/world.js created');\n } else if (result.configResult === 'needs-manual-update') {\n log.warn('features/support/world.js exists but does not import @letsrunit/bdd.');\n note(`Add \"import '${BDD_IMPORT}';\" to features/support/world.js`, 'Action required');\n }\n\n if (result.featuresCreated) log.success('features/ directory created with example.feature');\n}\n\nasync function stepCheckPlaywrightBrowsers(env: Environment, { yes }: InitOptions): Promise<void> {\n if (hasPlaywrightBrowsers(env)) return;\n\n if (!yes && !env.isInteractive) {\n log.warn('Playwright Chromium browser not found.');\n note('npx playwright install chromium', 'Run to install browsers');\n return;\n }\n\n if (!yes) {\n const install = await confirm({ message: 'Playwright Chromium browser not found. Install it now?' });\n if (install !== true) return;\n }\n\n const s = spinner();\n s.start('Installing Playwright Chromium…');\n installPlaywrightBrowsers(env);\n s.stop('Playwright Chromium installed');\n}\n\nasync function stepAddGithubAction(env: Environment, { yes }: InitOptions): Promise<void> {\n if (!yes && !env.isInteractive) return;\n\n if (!yes) {\n const addAction = await confirm({ message: 'Add a GitHub Action to run features on push?' });\n if (addAction !== true) return;\n }\n\n const result = installGithubAction(env);\n if (result === 'created') {\n log.success('.github/workflows/letsrunit.yml created');\n } else {\n log.info('.github/workflows/letsrunit.yml already exists, skipped');\n }\n}\n\nexport async function init(options: InitOptions = {}): Promise<void> {\n intro('letsrunit init');\n\n const env = detectEnvironment();\n\n await stepInstallCli(env);\n\n const hasCucumber = await stepEnsureCucumber(env, options);\n if (hasCucumber) {\n stepSetupCucumber(env);\n await stepCheckPlaywrightBrowsers(env, options);\n await stepAddGithubAction(env, options);\n }\n\n outro('All done! Run npx letsrunit --help to get started.');\n}\n","import { execSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport type PackageManager = 'yarn' | 'pnpm' | 'bun' | 'npm';\n\nexport interface Environment {\n isInteractive: boolean;\n packageManager: PackageManager;\n nodeVersion: number;\n hasCucumber: boolean;\n cwd: string;\n}\n\nexport interface PackageManagerArgs {\n npm: string;\n yarn: string;\n pnpm: string;\n bun: string;\n}\n\nexport function detectEnvironment(): Environment {\n const cwd = process.cwd();\n const isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY);\n\n let packageManager: PackageManager = 'npm';\n if (existsSync(join(cwd, 'yarn.lock'))) packageManager = 'yarn';\n else if (existsSync(join(cwd, 'pnpm-lock.yaml'))) packageManager = 'pnpm';\n else if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) packageManager = 'bun';\n\n const nodeVersion = parseInt(process.version.slice(1), 10);\n const hasCucumber = existsSync(join(cwd, 'node_modules', '@cucumber', 'cucumber', 'package.json'));\n\n return { isInteractive, packageManager, nodeVersion, hasCucumber, cwd };\n}\n\nfunction pmCmd(pm: string, args: PackageManagerArgs): string {\n if (pm === 'yarn') return `yarn ${args.yarn}`;\n if (pm === 'pnpm') return `pnpm ${args.pnpm}`;\n if (pm === 'bun') return `bun ${args.bun}`;\n return `npm ${args.npm}`;\n}\n\nexport function execPm(env: Pick<Environment, 'packageManager' | 'cwd'>, args: PackageManagerArgs) {\n const cmd = pmCmd(env.packageManager, args);\n return execSync(cmd, { stdio: 'inherit', cwd: env.cwd });\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nexport function isCliInstalled({ cwd }: Pick<Environment, 'cwd'>): boolean {\n const pkgPath = join(cwd, 'package.json');\n if (!existsSync(pkgPath)) return false;\n\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n };\n\n return '@letsrunit/cli' in (pkg.devDependencies ?? {}) || '@letsrunit/cli' in (pkg.dependencies ?? {});\n}\n\nexport function installCli(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'install --save-dev @letsrunit/cli',\n yarn: 'add --dev @letsrunit/cli',\n pnpm: 'add -D @letsrunit/cli',\n bun: 'add -d @letsrunit/cli',\n });\n}\n","import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nconst BDD_IMPORT = '@letsrunit/bdd/define';\n\nconst CUCUMBER_CONFIG = `export default {\n worldParameters: {\n baseURL: 'http://localhost:3000',\n },\n};\n`;\n\nconst SUPPORT_FILE = `import { setDefaultTimeout } from '@cucumber/cucumber';\nimport '${BDD_IMPORT}';\n\nsetDefaultTimeout(30_000);\n`;\n\nconst EXAMPLE_FEATURE = `Feature: Example\n Scenario: Homepage loads\n Given I'm on the homepage\n Then The page contains heading \"Welcome\"\n`;\n\nexport type CucumberConfigResult = 'created' | 'skipped' | 'needs-manual-update';\n\nexport interface CucumberSetupResult {\n bddInstalled: boolean;\n configResult: CucumberConfigResult;\n featuresCreated: boolean;\n}\n\nexport function installCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'install --save-dev @cucumber/cucumber',\n yarn: 'add --dev @cucumber/cucumber',\n pnpm: 'add -D @cucumber/cucumber',\n bun: 'add -d @cucumber/cucumber',\n });\n}\n\nfunction installBdd(env: Pick<Environment, 'packageManager' | 'cwd'>): boolean {\n const pkgPath = join(env.cwd, 'package.json');\n if (!existsSync(pkgPath)) return false;\n\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n };\n\n const alreadyInstalled =\n '@letsrunit/bdd' in (pkg.devDependencies ?? {}) || '@letsrunit/bdd' in (pkg.dependencies ?? {});\n\n if (alreadyInstalled) return false;\n\n execPm(env, {\n npm: 'install --save-dev @letsrunit/bdd',\n yarn: 'add --dev @letsrunit/bdd',\n pnpm: 'add -D @letsrunit/bdd',\n bun: 'add -d @letsrunit/bdd',\n });\n\n return true;\n}\n\nfunction setupCucumberConfig({ cwd }: Pick<Environment, 'cwd'>): CucumberConfigResult {\n const supportDir = join(cwd, 'features', 'support');\n const supportPath = join(supportDir, 'world.js');\n\n if (existsSync(supportPath)) {\n const content = readFileSync(supportPath, 'utf-8');\n if (content.includes(BDD_IMPORT)) return 'skipped';\n return 'needs-manual-update';\n }\n\n const configPath = join(cwd, 'cucumber.js');\n if (!existsSync(configPath)) {\n writeFileSync(configPath, CUCUMBER_CONFIG, 'utf-8');\n }\n\n mkdirSync(supportDir, { recursive: true });\n writeFileSync(supportPath, SUPPORT_FILE, 'utf-8');\n return 'created';\n}\n\nfunction setupFeaturesDir({ cwd }: Pick<Environment, 'cwd'>): boolean {\n if (existsSync(join(cwd, 'features'))) {\n try {\n const hasFeatureFiles = readdirSync(join(cwd, 'features')).some((f) => f.endsWith('.feature'));\n if (hasFeatureFiles) return false;\n } catch {\n return false;\n }\n }\n\n try {\n const hasFeatureAtRoot = readdirSync(cwd).some((f) => f.endsWith('.feature'));\n if (hasFeatureAtRoot) return false;\n } catch {\n return false;\n }\n\n mkdirSync(join(cwd, 'features'), { recursive: true });\n writeFileSync(join(cwd, 'features', 'example.feature'), EXAMPLE_FEATURE, 'utf-8');\n return true;\n}\n\nexport function setupCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): CucumberSetupResult {\n const bddInstalled = installBdd(env);\n const configResult = setupCucumberConfig(env);\n const featuresCreated = setupFeaturesDir(env);\n return { bddInstalled, configResult, featuresCreated };\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Environment } from '../detect.js';\n\nexport type GithubActionsResult = 'created' | 'skipped';\n\nfunction setupStepsFor({ packageManager, nodeVersion }: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {\n if (packageManager === 'yarn') return `\\\n - name: Enable Corepack\n run: corepack enable\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: yarn\n - name: Install dependencies\n run: yarn install --immutable`;\n if (packageManager === 'pnpm') return `\\\n - uses: pnpm/action-setup@v4\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: pnpm\n - name: Install dependencies\n run: pnpm install --frozen-lockfile`;\n if (packageManager === 'bun') return `\\\n - uses: oven-sh/setup-bun@v2\n - name: Install dependencies\n run: bun install --frozen-lockfile`;\n return `\\\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: npm\n - name: Install dependencies\n run: npm ci`;\n}\n\nfunction workflowYaml(env: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {\n const setupSteps = setupStepsFor(env);\n\n return `name: Features\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\njobs:\n features:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n${setupSteps}\n - name: Install Playwright browsers\n run: npx playwright install chromium --with-deps\n - name: Run features\n run: npx cucumber-js\n`;\n}\n\nexport function installGithubAction(env: Pick<Environment, 'packageManager' | 'nodeVersion' | 'cwd'>): GithubActionsResult {\n const workflowDir = join(env.cwd, '.github', 'workflows');\n const workflowPath = join(workflowDir, 'letsrunit.yml');\n\n if (existsSync(workflowPath)) return 'skipped';\n\n mkdirSync(workflowDir, { recursive: true });\n writeFileSync(workflowPath, workflowYaml(env), 'utf-8');\n return 'created';\n}\n","import { execSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nexport function hasPlaywrightBrowsers({ cwd }: Pick<Environment, 'cwd'>): boolean {\n if (!existsSync(join(cwd, 'node_modules', 'playwright-core', 'package.json'))) {\n return false;\n }\n try {\n const execPath = execSync(\n `node -e \"console.log(require('playwright-core').chromium.executablePath())\"`,\n { cwd, stdio: 'pipe', encoding: 'utf-8' },\n ).trim();\n return existsSync(execPath);\n } catch {\n return false;\n }\n}\n\nexport function installPlaywrightBrowsers(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'exec playwright install chromium',\n yarn: 'exec playwright install chromium',\n pnpm: 'exec playwright install chromium',\n bun: 'x playwright install chromium',\n });\n}\n","import { init } from './init.js';\n\nconst args = process.argv.slice(2);\nconst command = args.find((a) => !a.startsWith('-')) ?? 'init';\nconst yes = args.includes('--yes') || args.includes('-y');\n\nif (command === 'init') {\n init({ yes }).catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n });\n} else {\n console.error(`Unknown command: ${command}`);\n console.error('Usage: letsrunit init [--yes]');\n process.exit(1);\n}\n"],"mappings":"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","names":["args","existsSync","join","join","existsSync","existsSync","readFileSync","join","join","existsSync","readFileSync","existsSync","mkdirSync","writeFileSync","join","execSync","existsSync","join","existsSync","join","execSync","BDD_IMPORT","yes"]}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// src/init.ts
|
|
2
|
+
import { confirm, intro, log, note, outro, spinner } from "@clack/prompts";
|
|
3
|
+
|
|
4
|
+
// src/detect.ts
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
function detectEnvironment() {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
11
|
+
let packageManager = "npm";
|
|
12
|
+
if (existsSync(join(cwd, "yarn.lock"))) packageManager = "yarn";
|
|
13
|
+
else if (existsSync(join(cwd, "pnpm-lock.yaml"))) packageManager = "pnpm";
|
|
14
|
+
else if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) packageManager = "bun";
|
|
15
|
+
const nodeVersion = parseInt(process.version.slice(1), 10);
|
|
16
|
+
const hasCucumber = existsSync(join(cwd, "node_modules", "@cucumber", "cucumber", "package.json"));
|
|
17
|
+
return { isInteractive, packageManager, nodeVersion, hasCucumber, cwd };
|
|
18
|
+
}
|
|
19
|
+
function pmCmd(pm, args) {
|
|
20
|
+
if (pm === "yarn") return `yarn ${args.yarn}`;
|
|
21
|
+
if (pm === "pnpm") return `pnpm ${args.pnpm}`;
|
|
22
|
+
if (pm === "bun") return `bun ${args.bun}`;
|
|
23
|
+
return `npm ${args.npm}`;
|
|
24
|
+
}
|
|
25
|
+
function execPm(env, args) {
|
|
26
|
+
const cmd = pmCmd(env.packageManager, args);
|
|
27
|
+
return execSync(cmd, { stdio: "inherit", cwd: env.cwd });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/setup/cli.ts
|
|
31
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
32
|
+
import { join as join2 } from "path";
|
|
33
|
+
function isCliInstalled({ cwd }) {
|
|
34
|
+
const pkgPath = join2(cwd, "package.json");
|
|
35
|
+
if (!existsSync2(pkgPath)) return false;
|
|
36
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
37
|
+
return "@letsrunit/cli" in (pkg.devDependencies ?? {}) || "@letsrunit/cli" in (pkg.dependencies ?? {});
|
|
38
|
+
}
|
|
39
|
+
function installCli(env) {
|
|
40
|
+
execPm(env, {
|
|
41
|
+
npm: "install --save-dev @letsrunit/cli",
|
|
42
|
+
yarn: "add --dev @letsrunit/cli",
|
|
43
|
+
pnpm: "add -D @letsrunit/cli",
|
|
44
|
+
bun: "add -d @letsrunit/cli"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/setup/cucumber.ts
|
|
49
|
+
import { existsSync as existsSync3, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
50
|
+
import { join as join3 } from "path";
|
|
51
|
+
var BDD_IMPORT = "@letsrunit/bdd/define";
|
|
52
|
+
var CUCUMBER_CONFIG = `export default {
|
|
53
|
+
worldParameters: {
|
|
54
|
+
baseURL: 'http://localhost:3000',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
`;
|
|
58
|
+
var SUPPORT_FILE = `import { setDefaultTimeout } from '@cucumber/cucumber';
|
|
59
|
+
import '${BDD_IMPORT}';
|
|
60
|
+
|
|
61
|
+
setDefaultTimeout(30_000);
|
|
62
|
+
`;
|
|
63
|
+
var EXAMPLE_FEATURE = `Feature: Example
|
|
64
|
+
Scenario: Homepage loads
|
|
65
|
+
Given I'm on the homepage
|
|
66
|
+
Then The page contains heading "Welcome"
|
|
67
|
+
`;
|
|
68
|
+
function installCucumber(env) {
|
|
69
|
+
execPm(env, {
|
|
70
|
+
npm: "install --save-dev @cucumber/cucumber",
|
|
71
|
+
yarn: "add --dev @cucumber/cucumber",
|
|
72
|
+
pnpm: "add -D @cucumber/cucumber",
|
|
73
|
+
bun: "add -d @cucumber/cucumber"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function installBdd(env) {
|
|
77
|
+
const pkgPath = join3(env.cwd, "package.json");
|
|
78
|
+
if (!existsSync3(pkgPath)) return false;
|
|
79
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
80
|
+
const alreadyInstalled = "@letsrunit/bdd" in (pkg.devDependencies ?? {}) || "@letsrunit/bdd" in (pkg.dependencies ?? {});
|
|
81
|
+
if (alreadyInstalled) return false;
|
|
82
|
+
execPm(env, {
|
|
83
|
+
npm: "install --save-dev @letsrunit/bdd",
|
|
84
|
+
yarn: "add --dev @letsrunit/bdd",
|
|
85
|
+
pnpm: "add -D @letsrunit/bdd",
|
|
86
|
+
bun: "add -d @letsrunit/bdd"
|
|
87
|
+
});
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function setupCucumberConfig({ cwd }) {
|
|
91
|
+
const supportDir = join3(cwd, "features", "support");
|
|
92
|
+
const supportPath = join3(supportDir, "world.js");
|
|
93
|
+
if (existsSync3(supportPath)) {
|
|
94
|
+
const content = readFileSync2(supportPath, "utf-8");
|
|
95
|
+
if (content.includes(BDD_IMPORT)) return "skipped";
|
|
96
|
+
return "needs-manual-update";
|
|
97
|
+
}
|
|
98
|
+
const configPath = join3(cwd, "cucumber.js");
|
|
99
|
+
if (!existsSync3(configPath)) {
|
|
100
|
+
writeFileSync(configPath, CUCUMBER_CONFIG, "utf-8");
|
|
101
|
+
}
|
|
102
|
+
mkdirSync(supportDir, { recursive: true });
|
|
103
|
+
writeFileSync(supportPath, SUPPORT_FILE, "utf-8");
|
|
104
|
+
return "created";
|
|
105
|
+
}
|
|
106
|
+
function setupFeaturesDir({ cwd }) {
|
|
107
|
+
if (existsSync3(join3(cwd, "features"))) {
|
|
108
|
+
try {
|
|
109
|
+
const hasFeatureFiles = readdirSync(join3(cwd, "features")).some((f) => f.endsWith(".feature"));
|
|
110
|
+
if (hasFeatureFiles) return false;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const hasFeatureAtRoot = readdirSync(cwd).some((f) => f.endsWith(".feature"));
|
|
117
|
+
if (hasFeatureAtRoot) return false;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
mkdirSync(join3(cwd, "features"), { recursive: true });
|
|
122
|
+
writeFileSync(join3(cwd, "features", "example.feature"), EXAMPLE_FEATURE, "utf-8");
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
function setupCucumber(env) {
|
|
126
|
+
const bddInstalled = installBdd(env);
|
|
127
|
+
const configResult = setupCucumberConfig(env);
|
|
128
|
+
const featuresCreated = setupFeaturesDir(env);
|
|
129
|
+
return { bddInstalled, configResult, featuresCreated };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/setup/github-actions.ts
|
|
133
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
134
|
+
import { join as join4 } from "path";
|
|
135
|
+
function setupStepsFor({ packageManager, nodeVersion }) {
|
|
136
|
+
if (packageManager === "yarn") return ` - name: Enable Corepack
|
|
137
|
+
run: corepack enable
|
|
138
|
+
- uses: actions/setup-node@v4
|
|
139
|
+
with:
|
|
140
|
+
node-version: ${nodeVersion}
|
|
141
|
+
cache: yarn
|
|
142
|
+
- name: Install dependencies
|
|
143
|
+
run: yarn install --immutable`;
|
|
144
|
+
if (packageManager === "pnpm") return ` - uses: pnpm/action-setup@v4
|
|
145
|
+
- uses: actions/setup-node@v4
|
|
146
|
+
with:
|
|
147
|
+
node-version: ${nodeVersion}
|
|
148
|
+
cache: pnpm
|
|
149
|
+
- name: Install dependencies
|
|
150
|
+
run: pnpm install --frozen-lockfile`;
|
|
151
|
+
if (packageManager === "bun") return ` - uses: oven-sh/setup-bun@v2
|
|
152
|
+
- name: Install dependencies
|
|
153
|
+
run: bun install --frozen-lockfile`;
|
|
154
|
+
return ` - uses: actions/setup-node@v4
|
|
155
|
+
with:
|
|
156
|
+
node-version: ${nodeVersion}
|
|
157
|
+
cache: npm
|
|
158
|
+
- name: Install dependencies
|
|
159
|
+
run: npm ci`;
|
|
160
|
+
}
|
|
161
|
+
function workflowYaml(env) {
|
|
162
|
+
const setupSteps = setupStepsFor(env);
|
|
163
|
+
return `name: Features
|
|
164
|
+
on:
|
|
165
|
+
push:
|
|
166
|
+
branches: [main]
|
|
167
|
+
pull_request:
|
|
168
|
+
branches: [main]
|
|
169
|
+
jobs:
|
|
170
|
+
features:
|
|
171
|
+
runs-on: ubuntu-latest
|
|
172
|
+
steps:
|
|
173
|
+
- uses: actions/checkout@v4
|
|
174
|
+
${setupSteps}
|
|
175
|
+
- name: Install Playwright browsers
|
|
176
|
+
run: npx playwright install chromium --with-deps
|
|
177
|
+
- name: Run features
|
|
178
|
+
run: npx cucumber-js
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
function installGithubAction(env) {
|
|
182
|
+
const workflowDir = join4(env.cwd, ".github", "workflows");
|
|
183
|
+
const workflowPath = join4(workflowDir, "letsrunit.yml");
|
|
184
|
+
if (existsSync4(workflowPath)) return "skipped";
|
|
185
|
+
mkdirSync2(workflowDir, { recursive: true });
|
|
186
|
+
writeFileSync2(workflowPath, workflowYaml(env), "utf-8");
|
|
187
|
+
return "created";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/setup/playwright.ts
|
|
191
|
+
import { execSync as execSync2 } from "child_process";
|
|
192
|
+
import { existsSync as existsSync5 } from "fs";
|
|
193
|
+
import { join as join5 } from "path";
|
|
194
|
+
function hasPlaywrightBrowsers({ cwd }) {
|
|
195
|
+
if (!existsSync5(join5(cwd, "node_modules", "playwright-core", "package.json"))) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const execPath = execSync2(
|
|
200
|
+
`node -e "console.log(require('playwright-core').chromium.executablePath())"`,
|
|
201
|
+
{ cwd, stdio: "pipe", encoding: "utf-8" }
|
|
202
|
+
).trim();
|
|
203
|
+
return existsSync5(execPath);
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function installPlaywrightBrowsers(env) {
|
|
209
|
+
execPm(env, {
|
|
210
|
+
npm: "exec playwright install chromium",
|
|
211
|
+
yarn: "exec playwright install chromium",
|
|
212
|
+
pnpm: "exec playwright install chromium",
|
|
213
|
+
bun: "x playwright install chromium"
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/init.ts
|
|
218
|
+
var BDD_IMPORT2 = "@letsrunit/bdd/define";
|
|
219
|
+
async function stepInstallCli(env) {
|
|
220
|
+
if (isCliInstalled(env)) {
|
|
221
|
+
log.success("@letsrunit/cli already installed");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const s = spinner();
|
|
225
|
+
s.start("Installing @letsrunit/cli\u2026");
|
|
226
|
+
installCli(env);
|
|
227
|
+
s.stop("@letsrunit/cli installed");
|
|
228
|
+
}
|
|
229
|
+
async function stepEnsureCucumber(env, { yes }) {
|
|
230
|
+
if (env.hasCucumber) return true;
|
|
231
|
+
if (!yes && !env.isInteractive) {
|
|
232
|
+
log.warn("@cucumber/cucumber not found. Install it to use letsrunit with Cucumber:");
|
|
233
|
+
note("npm install --save-dev @cucumber/cucumber\nThen run: npx letsrunit init", "Setup Cucumber");
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (!yes) {
|
|
237
|
+
const install = await confirm({ message: "@cucumber/cucumber not found. Install it now?" });
|
|
238
|
+
if (install !== true) return false;
|
|
239
|
+
}
|
|
240
|
+
const s = spinner();
|
|
241
|
+
s.start("Installing @cucumber/cucumber\u2026");
|
|
242
|
+
installCucumber(env);
|
|
243
|
+
s.stop("@cucumber/cucumber installed");
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
function stepSetupCucumber(env) {
|
|
247
|
+
const result = setupCucumber(env);
|
|
248
|
+
if (result.bddInstalled) log.success("@letsrunit/bdd installed");
|
|
249
|
+
if (result.configResult === "created") {
|
|
250
|
+
log.success("features/support/world.js created");
|
|
251
|
+
} else if (result.configResult === "needs-manual-update") {
|
|
252
|
+
log.warn("features/support/world.js exists but does not import @letsrunit/bdd.");
|
|
253
|
+
note(`Add "import '${BDD_IMPORT2}';" to features/support/world.js`, "Action required");
|
|
254
|
+
}
|
|
255
|
+
if (result.featuresCreated) log.success("features/ directory created with example.feature");
|
|
256
|
+
}
|
|
257
|
+
async function stepCheckPlaywrightBrowsers(env, { yes }) {
|
|
258
|
+
if (hasPlaywrightBrowsers(env)) return;
|
|
259
|
+
if (!yes && !env.isInteractive) {
|
|
260
|
+
log.warn("Playwright Chromium browser not found.");
|
|
261
|
+
note("npx playwright install chromium", "Run to install browsers");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!yes) {
|
|
265
|
+
const install = await confirm({ message: "Playwright Chromium browser not found. Install it now?" });
|
|
266
|
+
if (install !== true) return;
|
|
267
|
+
}
|
|
268
|
+
const s = spinner();
|
|
269
|
+
s.start("Installing Playwright Chromium\u2026");
|
|
270
|
+
installPlaywrightBrowsers(env);
|
|
271
|
+
s.stop("Playwright Chromium installed");
|
|
272
|
+
}
|
|
273
|
+
async function stepAddGithubAction(env, { yes }) {
|
|
274
|
+
if (!yes && !env.isInteractive) return;
|
|
275
|
+
if (!yes) {
|
|
276
|
+
const addAction = await confirm({ message: "Add a GitHub Action to run features on push?" });
|
|
277
|
+
if (addAction !== true) return;
|
|
278
|
+
}
|
|
279
|
+
const result = installGithubAction(env);
|
|
280
|
+
if (result === "created") {
|
|
281
|
+
log.success(".github/workflows/letsrunit.yml created");
|
|
282
|
+
} else {
|
|
283
|
+
log.info(".github/workflows/letsrunit.yml already exists, skipped");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function init(options = {}) {
|
|
287
|
+
intro("letsrunit init");
|
|
288
|
+
const env = detectEnvironment();
|
|
289
|
+
await stepInstallCli(env);
|
|
290
|
+
const hasCucumber = await stepEnsureCucumber(env, options);
|
|
291
|
+
if (hasCucumber) {
|
|
292
|
+
stepSetupCucumber(env);
|
|
293
|
+
await stepCheckPlaywrightBrowsers(env, options);
|
|
294
|
+
await stepAddGithubAction(env, options);
|
|
295
|
+
}
|
|
296
|
+
outro("All done! Run npx letsrunit --help to get started.");
|
|
297
|
+
}
|
|
298
|
+
export {
|
|
299
|
+
init
|
|
300
|
+
};
|
|
301
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/init.ts","../src/detect.ts","../src/setup/cli.ts","../src/setup/cucumber.ts","../src/setup/github-actions.ts","../src/setup/playwright.ts"],"sourcesContent":["import { confirm, intro, log, note, outro, spinner } from '@clack/prompts';\nimport { detectEnvironment, type Environment } from './detect.js';\nimport { installCli, isCliInstalled } from './setup/cli.js';\nimport { installCucumber, setupCucumber } from './setup/cucumber.js';\nimport { installGithubAction } from './setup/github-actions.js';\nimport { hasPlaywrightBrowsers, installPlaywrightBrowsers } from './setup/playwright.js';\n\nconst BDD_IMPORT = '@letsrunit/bdd/define';\n\nexport interface InitOptions {\n yes?: boolean;\n}\n\nasync function stepInstallCli(env: Environment): Promise<void> {\n if (isCliInstalled(env)) {\n log.success('@letsrunit/cli already installed');\n return;\n }\n\n const s = spinner();\n s.start('Installing @letsrunit/cli…');\n installCli(env);\n s.stop('@letsrunit/cli installed');\n}\n\nasync function stepEnsureCucumber(env: Environment, { yes }: InitOptions): Promise<boolean> {\n if (env.hasCucumber) return true;\n\n if (!yes && !env.isInteractive) {\n log.warn('@cucumber/cucumber not found. Install it to use letsrunit with Cucumber:');\n note('npm install --save-dev @cucumber/cucumber\\nThen run: npx letsrunit init', 'Setup Cucumber');\n return false;\n }\n\n if (!yes) {\n const install = await confirm({ message: '@cucumber/cucumber not found. Install it now?' });\n if (install !== true) return false;\n }\n\n const s = spinner();\n s.start('Installing @cucumber/cucumber…');\n installCucumber(env);\n s.stop('@cucumber/cucumber installed');\n return true;\n}\n\nfunction stepSetupCucumber(env: Environment): void {\n const result = setupCucumber(env);\n\n if (result.bddInstalled) log.success('@letsrunit/bdd installed');\n\n if (result.configResult === 'created') {\n log.success('features/support/world.js created');\n } else if (result.configResult === 'needs-manual-update') {\n log.warn('features/support/world.js exists but does not import @letsrunit/bdd.');\n note(`Add \"import '${BDD_IMPORT}';\" to features/support/world.js`, 'Action required');\n }\n\n if (result.featuresCreated) log.success('features/ directory created with example.feature');\n}\n\nasync function stepCheckPlaywrightBrowsers(env: Environment, { yes }: InitOptions): Promise<void> {\n if (hasPlaywrightBrowsers(env)) return;\n\n if (!yes && !env.isInteractive) {\n log.warn('Playwright Chromium browser not found.');\n note('npx playwright install chromium', 'Run to install browsers');\n return;\n }\n\n if (!yes) {\n const install = await confirm({ message: 'Playwright Chromium browser not found. Install it now?' });\n if (install !== true) return;\n }\n\n const s = spinner();\n s.start('Installing Playwright Chromium…');\n installPlaywrightBrowsers(env);\n s.stop('Playwright Chromium installed');\n}\n\nasync function stepAddGithubAction(env: Environment, { yes }: InitOptions): Promise<void> {\n if (!yes && !env.isInteractive) return;\n\n if (!yes) {\n const addAction = await confirm({ message: 'Add a GitHub Action to run features on push?' });\n if (addAction !== true) return;\n }\n\n const result = installGithubAction(env);\n if (result === 'created') {\n log.success('.github/workflows/letsrunit.yml created');\n } else {\n log.info('.github/workflows/letsrunit.yml already exists, skipped');\n }\n}\n\nexport async function init(options: InitOptions = {}): Promise<void> {\n intro('letsrunit init');\n\n const env = detectEnvironment();\n\n await stepInstallCli(env);\n\n const hasCucumber = await stepEnsureCucumber(env, options);\n if (hasCucumber) {\n stepSetupCucumber(env);\n await stepCheckPlaywrightBrowsers(env, options);\n await stepAddGithubAction(env, options);\n }\n\n outro('All done! Run npx letsrunit --help to get started.');\n}\n","import { execSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport type PackageManager = 'yarn' | 'pnpm' | 'bun' | 'npm';\n\nexport interface Environment {\n isInteractive: boolean;\n packageManager: PackageManager;\n nodeVersion: number;\n hasCucumber: boolean;\n cwd: string;\n}\n\nexport interface PackageManagerArgs {\n npm: string;\n yarn: string;\n pnpm: string;\n bun: string;\n}\n\nexport function detectEnvironment(): Environment {\n const cwd = process.cwd();\n const isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY);\n\n let packageManager: PackageManager = 'npm';\n if (existsSync(join(cwd, 'yarn.lock'))) packageManager = 'yarn';\n else if (existsSync(join(cwd, 'pnpm-lock.yaml'))) packageManager = 'pnpm';\n else if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) packageManager = 'bun';\n\n const nodeVersion = parseInt(process.version.slice(1), 10);\n const hasCucumber = existsSync(join(cwd, 'node_modules', '@cucumber', 'cucumber', 'package.json'));\n\n return { isInteractive, packageManager, nodeVersion, hasCucumber, cwd };\n}\n\nfunction pmCmd(pm: string, args: PackageManagerArgs): string {\n if (pm === 'yarn') return `yarn ${args.yarn}`;\n if (pm === 'pnpm') return `pnpm ${args.pnpm}`;\n if (pm === 'bun') return `bun ${args.bun}`;\n return `npm ${args.npm}`;\n}\n\nexport function execPm(env: Pick<Environment, 'packageManager' | 'cwd'>, args: PackageManagerArgs) {\n const cmd = pmCmd(env.packageManager, args);\n return execSync(cmd, { stdio: 'inherit', cwd: env.cwd });\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nexport function isCliInstalled({ cwd }: Pick<Environment, 'cwd'>): boolean {\n const pkgPath = join(cwd, 'package.json');\n if (!existsSync(pkgPath)) return false;\n\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n };\n\n return '@letsrunit/cli' in (pkg.devDependencies ?? {}) || '@letsrunit/cli' in (pkg.dependencies ?? {});\n}\n\nexport function installCli(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'install --save-dev @letsrunit/cli',\n yarn: 'add --dev @letsrunit/cli',\n pnpm: 'add -D @letsrunit/cli',\n bun: 'add -d @letsrunit/cli',\n });\n}\n","import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nconst BDD_IMPORT = '@letsrunit/bdd/define';\n\nconst CUCUMBER_CONFIG = `export default {\n worldParameters: {\n baseURL: 'http://localhost:3000',\n },\n};\n`;\n\nconst SUPPORT_FILE = `import { setDefaultTimeout } from '@cucumber/cucumber';\nimport '${BDD_IMPORT}';\n\nsetDefaultTimeout(30_000);\n`;\n\nconst EXAMPLE_FEATURE = `Feature: Example\n Scenario: Homepage loads\n Given I'm on the homepage\n Then The page contains heading \"Welcome\"\n`;\n\nexport type CucumberConfigResult = 'created' | 'skipped' | 'needs-manual-update';\n\nexport interface CucumberSetupResult {\n bddInstalled: boolean;\n configResult: CucumberConfigResult;\n featuresCreated: boolean;\n}\n\nexport function installCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'install --save-dev @cucumber/cucumber',\n yarn: 'add --dev @cucumber/cucumber',\n pnpm: 'add -D @cucumber/cucumber',\n bun: 'add -d @cucumber/cucumber',\n });\n}\n\nfunction installBdd(env: Pick<Environment, 'packageManager' | 'cwd'>): boolean {\n const pkgPath = join(env.cwd, 'package.json');\n if (!existsSync(pkgPath)) return false;\n\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n };\n\n const alreadyInstalled =\n '@letsrunit/bdd' in (pkg.devDependencies ?? {}) || '@letsrunit/bdd' in (pkg.dependencies ?? {});\n\n if (alreadyInstalled) return false;\n\n execPm(env, {\n npm: 'install --save-dev @letsrunit/bdd',\n yarn: 'add --dev @letsrunit/bdd',\n pnpm: 'add -D @letsrunit/bdd',\n bun: 'add -d @letsrunit/bdd',\n });\n\n return true;\n}\n\nfunction setupCucumberConfig({ cwd }: Pick<Environment, 'cwd'>): CucumberConfigResult {\n const supportDir = join(cwd, 'features', 'support');\n const supportPath = join(supportDir, 'world.js');\n\n if (existsSync(supportPath)) {\n const content = readFileSync(supportPath, 'utf-8');\n if (content.includes(BDD_IMPORT)) return 'skipped';\n return 'needs-manual-update';\n }\n\n const configPath = join(cwd, 'cucumber.js');\n if (!existsSync(configPath)) {\n writeFileSync(configPath, CUCUMBER_CONFIG, 'utf-8');\n }\n\n mkdirSync(supportDir, { recursive: true });\n writeFileSync(supportPath, SUPPORT_FILE, 'utf-8');\n return 'created';\n}\n\nfunction setupFeaturesDir({ cwd }: Pick<Environment, 'cwd'>): boolean {\n if (existsSync(join(cwd, 'features'))) {\n try {\n const hasFeatureFiles = readdirSync(join(cwd, 'features')).some((f) => f.endsWith('.feature'));\n if (hasFeatureFiles) return false;\n } catch {\n return false;\n }\n }\n\n try {\n const hasFeatureAtRoot = readdirSync(cwd).some((f) => f.endsWith('.feature'));\n if (hasFeatureAtRoot) return false;\n } catch {\n return false;\n }\n\n mkdirSync(join(cwd, 'features'), { recursive: true });\n writeFileSync(join(cwd, 'features', 'example.feature'), EXAMPLE_FEATURE, 'utf-8');\n return true;\n}\n\nexport function setupCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): CucumberSetupResult {\n const bddInstalled = installBdd(env);\n const configResult = setupCucumberConfig(env);\n const featuresCreated = setupFeaturesDir(env);\n return { bddInstalled, configResult, featuresCreated };\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Environment } from '../detect.js';\n\nexport type GithubActionsResult = 'created' | 'skipped';\n\nfunction setupStepsFor({ packageManager, nodeVersion }: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {\n if (packageManager === 'yarn') return `\\\n - name: Enable Corepack\n run: corepack enable\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: yarn\n - name: Install dependencies\n run: yarn install --immutable`;\n if (packageManager === 'pnpm') return `\\\n - uses: pnpm/action-setup@v4\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: pnpm\n - name: Install dependencies\n run: pnpm install --frozen-lockfile`;\n if (packageManager === 'bun') return `\\\n - uses: oven-sh/setup-bun@v2\n - name: Install dependencies\n run: bun install --frozen-lockfile`;\n return `\\\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: npm\n - name: Install dependencies\n run: npm ci`;\n}\n\nfunction workflowYaml(env: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {\n const setupSteps = setupStepsFor(env);\n\n return `name: Features\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\njobs:\n features:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n${setupSteps}\n - name: Install Playwright browsers\n run: npx playwright install chromium --with-deps\n - name: Run features\n run: npx cucumber-js\n`;\n}\n\nexport function installGithubAction(env: Pick<Environment, 'packageManager' | 'nodeVersion' | 'cwd'>): GithubActionsResult {\n const workflowDir = join(env.cwd, '.github', 'workflows');\n const workflowPath = join(workflowDir, 'letsrunit.yml');\n\n if (existsSync(workflowPath)) return 'skipped';\n\n mkdirSync(workflowDir, { recursive: true });\n writeFileSync(workflowPath, workflowYaml(env), 'utf-8');\n return 'created';\n}\n","import { execSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nexport function hasPlaywrightBrowsers({ cwd }: Pick<Environment, 'cwd'>): boolean {\n if (!existsSync(join(cwd, 'node_modules', 'playwright-core', 'package.json'))) {\n return false;\n }\n try {\n const execPath = execSync(\n `node -e \"console.log(require('playwright-core').chromium.executablePath())\"`,\n { cwd, stdio: 'pipe', encoding: 'utf-8' },\n ).trim();\n return existsSync(execPath);\n } catch {\n return false;\n }\n}\n\nexport function installPlaywrightBrowsers(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'exec playwright install chromium',\n yarn: 'exec playwright install chromium',\n pnpm: 'exec playwright install chromium',\n bun: 'x playwright install chromium',\n });\n}\n"],"mappings":"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","names":["existsSync","join","join","existsSync","existsSync","readFileSync","join","join","existsSync","readFileSync","existsSync","mkdirSync","writeFileSync","join","execSync","existsSync","join","existsSync","join","execSync","BDD_IMPORT"]}
|
package/package.json
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letsrunit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Onboarding tool for letsrunit — run npx letsrunit init to get started",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/letsrunit-hq/letsrunit.git",
|
|
9
|
+
"directory": "packages/letsrunit"
|
|
10
|
+
},
|
|
11
|
+
"bugs": "https://github.com/letsrunit-hq/letsrunit/issues",
|
|
12
|
+
"homepage": "https://github.com/letsrunit-hq/letsrunit#readme",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"letsrunit": "./dist/bin.js"
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"bin": {
|
|
21
|
+
"letsrunit": "./dist/bin.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "../../node_modules/.bin/tsup",
|
|
33
|
+
"test": "echo \"No tests yet\""
|
|
34
|
+
},
|
|
35
|
+
"packageManager": "yarn@4.10.3",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@clack/prompts": "^0.10.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.0.9",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { init } from './init.js';
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
const command = args.find((a) => !a.startsWith('-')) ?? 'init';
|
|
5
|
+
const yes = args.includes('--yes') || args.includes('-y');
|
|
6
|
+
|
|
7
|
+
if (command === 'init') {
|
|
8
|
+
init({ yes }).catch((err: unknown) => {
|
|
9
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
|
12
|
+
} else {
|
|
13
|
+
console.error(`Unknown command: ${command}`);
|
|
14
|
+
console.error('Usage: letsrunit init [--yes]');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
package/src/detect.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export type PackageManager = 'yarn' | 'pnpm' | 'bun' | 'npm';
|
|
6
|
+
|
|
7
|
+
export interface Environment {
|
|
8
|
+
isInteractive: boolean;
|
|
9
|
+
packageManager: PackageManager;
|
|
10
|
+
nodeVersion: number;
|
|
11
|
+
hasCucumber: boolean;
|
|
12
|
+
cwd: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PackageManagerArgs {
|
|
16
|
+
npm: string;
|
|
17
|
+
yarn: string;
|
|
18
|
+
pnpm: string;
|
|
19
|
+
bun: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function detectEnvironment(): Environment {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
25
|
+
|
|
26
|
+
let packageManager: PackageManager = 'npm';
|
|
27
|
+
if (existsSync(join(cwd, 'yarn.lock'))) packageManager = 'yarn';
|
|
28
|
+
else if (existsSync(join(cwd, 'pnpm-lock.yaml'))) packageManager = 'pnpm';
|
|
29
|
+
else if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) packageManager = 'bun';
|
|
30
|
+
|
|
31
|
+
const nodeVersion = parseInt(process.version.slice(1), 10);
|
|
32
|
+
const hasCucumber = existsSync(join(cwd, 'node_modules', '@cucumber', 'cucumber', 'package.json'));
|
|
33
|
+
|
|
34
|
+
return { isInteractive, packageManager, nodeVersion, hasCucumber, cwd };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pmCmd(pm: string, args: PackageManagerArgs): string {
|
|
38
|
+
if (pm === 'yarn') return `yarn ${args.yarn}`;
|
|
39
|
+
if (pm === 'pnpm') return `pnpm ${args.pnpm}`;
|
|
40
|
+
if (pm === 'bun') return `bun ${args.bun}`;
|
|
41
|
+
return `npm ${args.npm}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function execPm(env: Pick<Environment, 'packageManager' | 'cwd'>, args: PackageManagerArgs) {
|
|
45
|
+
const cmd = pmCmd(env.packageManager, args);
|
|
46
|
+
return execSync(cmd, { stdio: 'inherit', cwd: env.cwd });
|
|
47
|
+
}
|
package/src/index.ts
ADDED
package/src/init.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { confirm, intro, log, note, outro, spinner } from '@clack/prompts';
|
|
2
|
+
import { detectEnvironment, type Environment } from './detect.js';
|
|
3
|
+
import { installCli, isCliInstalled } from './setup/cli.js';
|
|
4
|
+
import { installCucumber, setupCucumber } from './setup/cucumber.js';
|
|
5
|
+
import { installGithubAction } from './setup/github-actions.js';
|
|
6
|
+
import { hasPlaywrightBrowsers, installPlaywrightBrowsers } from './setup/playwright.js';
|
|
7
|
+
|
|
8
|
+
const BDD_IMPORT = '@letsrunit/bdd/define';
|
|
9
|
+
|
|
10
|
+
export interface InitOptions {
|
|
11
|
+
yes?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function stepInstallCli(env: Environment): Promise<void> {
|
|
15
|
+
if (isCliInstalled(env)) {
|
|
16
|
+
log.success('@letsrunit/cli already installed');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const s = spinner();
|
|
21
|
+
s.start('Installing @letsrunit/cli…');
|
|
22
|
+
installCli(env);
|
|
23
|
+
s.stop('@letsrunit/cli installed');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function stepEnsureCucumber(env: Environment, { yes }: InitOptions): Promise<boolean> {
|
|
27
|
+
if (env.hasCucumber) return true;
|
|
28
|
+
|
|
29
|
+
if (!yes && !env.isInteractive) {
|
|
30
|
+
log.warn('@cucumber/cucumber not found. Install it to use letsrunit with Cucumber:');
|
|
31
|
+
note('npm install --save-dev @cucumber/cucumber\nThen run: npx letsrunit init', 'Setup Cucumber');
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!yes) {
|
|
36
|
+
const install = await confirm({ message: '@cucumber/cucumber not found. Install it now?' });
|
|
37
|
+
if (install !== true) return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const s = spinner();
|
|
41
|
+
s.start('Installing @cucumber/cucumber…');
|
|
42
|
+
installCucumber(env);
|
|
43
|
+
s.stop('@cucumber/cucumber installed');
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stepSetupCucumber(env: Environment): void {
|
|
48
|
+
const result = setupCucumber(env);
|
|
49
|
+
|
|
50
|
+
if (result.bddInstalled) log.success('@letsrunit/bdd installed');
|
|
51
|
+
|
|
52
|
+
if (result.configResult === 'created') {
|
|
53
|
+
log.success('features/support/world.js created');
|
|
54
|
+
} else if (result.configResult === 'needs-manual-update') {
|
|
55
|
+
log.warn('features/support/world.js exists but does not import @letsrunit/bdd.');
|
|
56
|
+
note(`Add "import '${BDD_IMPORT}';" to features/support/world.js`, 'Action required');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (result.featuresCreated) log.success('features/ directory created with example.feature');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function stepCheckPlaywrightBrowsers(env: Environment, { yes }: InitOptions): Promise<void> {
|
|
63
|
+
if (hasPlaywrightBrowsers(env)) return;
|
|
64
|
+
|
|
65
|
+
if (!yes && !env.isInteractive) {
|
|
66
|
+
log.warn('Playwright Chromium browser not found.');
|
|
67
|
+
note('npx playwright install chromium', 'Run to install browsers');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!yes) {
|
|
72
|
+
const install = await confirm({ message: 'Playwright Chromium browser not found. Install it now?' });
|
|
73
|
+
if (install !== true) return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const s = spinner();
|
|
77
|
+
s.start('Installing Playwright Chromium…');
|
|
78
|
+
installPlaywrightBrowsers(env);
|
|
79
|
+
s.stop('Playwright Chromium installed');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function stepAddGithubAction(env: Environment, { yes }: InitOptions): Promise<void> {
|
|
83
|
+
if (!yes && !env.isInteractive) return;
|
|
84
|
+
|
|
85
|
+
if (!yes) {
|
|
86
|
+
const addAction = await confirm({ message: 'Add a GitHub Action to run features on push?' });
|
|
87
|
+
if (addAction !== true) return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = installGithubAction(env);
|
|
91
|
+
if (result === 'created') {
|
|
92
|
+
log.success('.github/workflows/letsrunit.yml created');
|
|
93
|
+
} else {
|
|
94
|
+
log.info('.github/workflows/letsrunit.yml already exists, skipped');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function init(options: InitOptions = {}): Promise<void> {
|
|
99
|
+
intro('letsrunit init');
|
|
100
|
+
|
|
101
|
+
const env = detectEnvironment();
|
|
102
|
+
|
|
103
|
+
await stepInstallCli(env);
|
|
104
|
+
|
|
105
|
+
const hasCucumber = await stepEnsureCucumber(env, options);
|
|
106
|
+
if (hasCucumber) {
|
|
107
|
+
stepSetupCucumber(env);
|
|
108
|
+
await stepCheckPlaywrightBrowsers(env, options);
|
|
109
|
+
await stepAddGithubAction(env, options);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
outro('All done! Run npx letsrunit --help to get started.');
|
|
113
|
+
}
|
package/src/setup/cli.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { type Environment, execPm } from '../detect.js';
|
|
4
|
+
|
|
5
|
+
export function isCliInstalled({ cwd }: Pick<Environment, 'cwd'>): boolean {
|
|
6
|
+
const pkgPath = join(cwd, 'package.json');
|
|
7
|
+
if (!existsSync(pkgPath)) return false;
|
|
8
|
+
|
|
9
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {
|
|
10
|
+
devDependencies?: Record<string, string>;
|
|
11
|
+
dependencies?: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return '@letsrunit/cli' in (pkg.devDependencies ?? {}) || '@letsrunit/cli' in (pkg.dependencies ?? {});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function installCli(env: Pick<Environment, 'packageManager' | 'cwd'>): void {
|
|
18
|
+
execPm(env, {
|
|
19
|
+
npm: 'install --save-dev @letsrunit/cli',
|
|
20
|
+
yarn: 'add --dev @letsrunit/cli',
|
|
21
|
+
pnpm: 'add -D @letsrunit/cli',
|
|
22
|
+
bun: 'add -d @letsrunit/cli',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { type Environment, execPm } from '../detect.js';
|
|
4
|
+
|
|
5
|
+
const BDD_IMPORT = '@letsrunit/bdd/define';
|
|
6
|
+
|
|
7
|
+
const CUCUMBER_CONFIG = `export default {
|
|
8
|
+
worldParameters: {
|
|
9
|
+
baseURL: 'http://localhost:3000',
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const SUPPORT_FILE = `import { setDefaultTimeout } from '@cucumber/cucumber';
|
|
15
|
+
import '${BDD_IMPORT}';
|
|
16
|
+
|
|
17
|
+
setDefaultTimeout(30_000);
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const EXAMPLE_FEATURE = `Feature: Example
|
|
21
|
+
Scenario: Homepage loads
|
|
22
|
+
Given I'm on the homepage
|
|
23
|
+
Then The page contains heading "Welcome"
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export type CucumberConfigResult = 'created' | 'skipped' | 'needs-manual-update';
|
|
27
|
+
|
|
28
|
+
export interface CucumberSetupResult {
|
|
29
|
+
bddInstalled: boolean;
|
|
30
|
+
configResult: CucumberConfigResult;
|
|
31
|
+
featuresCreated: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function installCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): void {
|
|
35
|
+
execPm(env, {
|
|
36
|
+
npm: 'install --save-dev @cucumber/cucumber',
|
|
37
|
+
yarn: 'add --dev @cucumber/cucumber',
|
|
38
|
+
pnpm: 'add -D @cucumber/cucumber',
|
|
39
|
+
bun: 'add -d @cucumber/cucumber',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function installBdd(env: Pick<Environment, 'packageManager' | 'cwd'>): boolean {
|
|
44
|
+
const pkgPath = join(env.cwd, 'package.json');
|
|
45
|
+
if (!existsSync(pkgPath)) return false;
|
|
46
|
+
|
|
47
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {
|
|
48
|
+
devDependencies?: Record<string, string>;
|
|
49
|
+
dependencies?: Record<string, string>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const alreadyInstalled =
|
|
53
|
+
'@letsrunit/bdd' in (pkg.devDependencies ?? {}) || '@letsrunit/bdd' in (pkg.dependencies ?? {});
|
|
54
|
+
|
|
55
|
+
if (alreadyInstalled) return false;
|
|
56
|
+
|
|
57
|
+
execPm(env, {
|
|
58
|
+
npm: 'install --save-dev @letsrunit/bdd',
|
|
59
|
+
yarn: 'add --dev @letsrunit/bdd',
|
|
60
|
+
pnpm: 'add -D @letsrunit/bdd',
|
|
61
|
+
bun: 'add -d @letsrunit/bdd',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function setupCucumberConfig({ cwd }: Pick<Environment, 'cwd'>): CucumberConfigResult {
|
|
68
|
+
const supportDir = join(cwd, 'features', 'support');
|
|
69
|
+
const supportPath = join(supportDir, 'world.js');
|
|
70
|
+
|
|
71
|
+
if (existsSync(supportPath)) {
|
|
72
|
+
const content = readFileSync(supportPath, 'utf-8');
|
|
73
|
+
if (content.includes(BDD_IMPORT)) return 'skipped';
|
|
74
|
+
return 'needs-manual-update';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const configPath = join(cwd, 'cucumber.js');
|
|
78
|
+
if (!existsSync(configPath)) {
|
|
79
|
+
writeFileSync(configPath, CUCUMBER_CONFIG, 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
mkdirSync(supportDir, { recursive: true });
|
|
83
|
+
writeFileSync(supportPath, SUPPORT_FILE, 'utf-8');
|
|
84
|
+
return 'created';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function setupFeaturesDir({ cwd }: Pick<Environment, 'cwd'>): boolean {
|
|
88
|
+
if (existsSync(join(cwd, 'features'))) {
|
|
89
|
+
try {
|
|
90
|
+
const hasFeatureFiles = readdirSync(join(cwd, 'features')).some((f) => f.endsWith('.feature'));
|
|
91
|
+
if (hasFeatureFiles) return false;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const hasFeatureAtRoot = readdirSync(cwd).some((f) => f.endsWith('.feature'));
|
|
99
|
+
if (hasFeatureAtRoot) return false;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
mkdirSync(join(cwd, 'features'), { recursive: true });
|
|
105
|
+
writeFileSync(join(cwd, 'features', 'example.feature'), EXAMPLE_FEATURE, 'utf-8');
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function setupCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): CucumberSetupResult {
|
|
110
|
+
const bddInstalled = installBdd(env);
|
|
111
|
+
const configResult = setupCucumberConfig(env);
|
|
112
|
+
const featuresCreated = setupFeaturesDir(env);
|
|
113
|
+
return { bddInstalled, configResult, featuresCreated };
|
|
114
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { Environment } from '../detect.js';
|
|
4
|
+
|
|
5
|
+
export type GithubActionsResult = 'created' | 'skipped';
|
|
6
|
+
|
|
7
|
+
function setupStepsFor({ packageManager, nodeVersion }: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {
|
|
8
|
+
if (packageManager === 'yarn') return `\
|
|
9
|
+
- name: Enable Corepack
|
|
10
|
+
run: corepack enable
|
|
11
|
+
- uses: actions/setup-node@v4
|
|
12
|
+
with:
|
|
13
|
+
node-version: ${nodeVersion}
|
|
14
|
+
cache: yarn
|
|
15
|
+
- name: Install dependencies
|
|
16
|
+
run: yarn install --immutable`;
|
|
17
|
+
if (packageManager === 'pnpm') return `\
|
|
18
|
+
- uses: pnpm/action-setup@v4
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: ${nodeVersion}
|
|
22
|
+
cache: pnpm
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: pnpm install --frozen-lockfile`;
|
|
25
|
+
if (packageManager === 'bun') return `\
|
|
26
|
+
- uses: oven-sh/setup-bun@v2
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: bun install --frozen-lockfile`;
|
|
29
|
+
return `\
|
|
30
|
+
- uses: actions/setup-node@v4
|
|
31
|
+
with:
|
|
32
|
+
node-version: ${nodeVersion}
|
|
33
|
+
cache: npm
|
|
34
|
+
- name: Install dependencies
|
|
35
|
+
run: npm ci`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function workflowYaml(env: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {
|
|
39
|
+
const setupSteps = setupStepsFor(env);
|
|
40
|
+
|
|
41
|
+
return `name: Features
|
|
42
|
+
on:
|
|
43
|
+
push:
|
|
44
|
+
branches: [main]
|
|
45
|
+
pull_request:
|
|
46
|
+
branches: [main]
|
|
47
|
+
jobs:
|
|
48
|
+
features:
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v4
|
|
52
|
+
${setupSteps}
|
|
53
|
+
- name: Install Playwright browsers
|
|
54
|
+
run: npx playwright install chromium --with-deps
|
|
55
|
+
- name: Run features
|
|
56
|
+
run: npx cucumber-js
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function installGithubAction(env: Pick<Environment, 'packageManager' | 'nodeVersion' | 'cwd'>): GithubActionsResult {
|
|
61
|
+
const workflowDir = join(env.cwd, '.github', 'workflows');
|
|
62
|
+
const workflowPath = join(workflowDir, 'letsrunit.yml');
|
|
63
|
+
|
|
64
|
+
if (existsSync(workflowPath)) return 'skipped';
|
|
65
|
+
|
|
66
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
67
|
+
writeFileSync(workflowPath, workflowYaml(env), 'utf-8');
|
|
68
|
+
return 'created';
|
|
69
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { type Environment, execPm } from '../detect.js';
|
|
5
|
+
|
|
6
|
+
export function hasPlaywrightBrowsers({ cwd }: Pick<Environment, 'cwd'>): boolean {
|
|
7
|
+
if (!existsSync(join(cwd, 'node_modules', 'playwright-core', 'package.json'))) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const execPath = execSync(
|
|
12
|
+
`node -e "console.log(require('playwright-core').chromium.executablePath())"`,
|
|
13
|
+
{ cwd, stdio: 'pipe', encoding: 'utf-8' },
|
|
14
|
+
).trim();
|
|
15
|
+
return existsSync(execPath);
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function installPlaywrightBrowsers(env: Pick<Environment, 'packageManager' | 'cwd'>): void {
|
|
22
|
+
execPm(env, {
|
|
23
|
+
npm: 'exec playwright install chromium',
|
|
24
|
+
yarn: 'exec playwright install chromium',
|
|
25
|
+
pnpm: 'exec playwright install chromium',
|
|
26
|
+
bun: 'x playwright install chromium',
|
|
27
|
+
});
|
|
28
|
+
}
|
package/.editorconfig
DELETED
package/.gitattributes
DELETED
package/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# letsrunit
|