executable-stories-init 0.1.0
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/README.md +119 -0
- package/dist/cli/index.js +621 -0
- package/dist/templates/index.ts +22 -0
- package/dist/templates/playwright-config.ts.tmpl +13 -0
- package/dist/templates/playwright-sample.story.spec.ts.tmpl +12 -0
- package/dist/templates/tsconfig.json.tmpl +10 -0
- package/dist/templates/vitest-config.ts.tmpl +20 -0
- package/dist/templates/vitest-sample.story.test.ts.tmpl +18 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# executable-stories-init
|
|
2
|
+
|
|
3
|
+
Bootstrap [executable-stories](https://github.com/jagreehal/executable-stories) (Vitest and/or Playwright) into a TypeScript repo from zero. Detects your framework, package manager and monorepo layout, installs the right adapter, writes config, drops a sample story, and patches `package.json` scripts.
|
|
4
|
+
|
|
5
|
+
## Install and run
|
|
6
|
+
|
|
7
|
+
Run from your project root, no install required:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx executable-stories-init@latest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm dlx executable-stories-init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
yarn dlx executable-stories-init
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The wizard is interactive by default. Pass `--yes` (or `--json`) to run non-interactively.
|
|
22
|
+
|
|
23
|
+
## What it does
|
|
24
|
+
|
|
25
|
+
For each selected target (repo root, or one or more workspace packages):
|
|
26
|
+
|
|
27
|
+
1. **Detects** existing Vitest / Playwright installs, an existing config, and TypeScript.
|
|
28
|
+
2. **Installs** `executable-stories-vitest` / `executable-stories-playwright` and `executable-stories-formatters` using your repo's package manager (`pnpm`, `yarn`, or `npm`).
|
|
29
|
+
3. **Writes** `vitest.config.ts` and/or `playwright.config.ts` pre-wired with `StoryReporter` (Markdown + HTML output to `reports/`).
|
|
30
|
+
4. **Writes a sample story**: `tests/sample.story.test.ts` for Vitest, `tests/sample.story.spec.ts` for Playwright.
|
|
31
|
+
5. **Patches `package.json` scripts**: adds `test` (Vitest) and/or `test:e2e` (Playwright).
|
|
32
|
+
6. **Optionally writes `tsconfig.json`** when missing and `--ts` is passed.
|
|
33
|
+
|
|
34
|
+
Anything already present is left alone (or skipped with a reason). Use `--force` to overwrite differing files.
|
|
35
|
+
|
|
36
|
+
## Flags
|
|
37
|
+
|
|
38
|
+
| Flag | Description |
|
|
39
|
+
| ---- | ----------- |
|
|
40
|
+
| `--vitest` | Set up Vitest. |
|
|
41
|
+
| `--playwright` | Set up Playwright. |
|
|
42
|
+
| `--both` | Set up both Vitest and Playwright. |
|
|
43
|
+
| `--target <pkg...>` | Workspace package(s) to set up. Use `root` for the repo root. Repeatable. |
|
|
44
|
+
| `--ts` / `--no-ts` | Write a minimal `tsconfig.json` if missing. |
|
|
45
|
+
| `-y`, `--yes` | Non-interactive; accept defaults. |
|
|
46
|
+
| `--interactive` | Force prompts even when piped. |
|
|
47
|
+
| `--dry-run` | Print the plan; do not write or install. |
|
|
48
|
+
| `--force` | Overwrite differing existing files. |
|
|
49
|
+
| `--json` | Machine-readable output. Implies `--yes`. Requires `--vitest` / `--playwright` / `--both`. |
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
Single-package repo, Vitest only:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx executable-stories-init@latest --vitest --yes
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Monorepo, Vitest in two packages and Playwright at the root:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx executable-stories-init@latest --vitest --target packages/api --target packages/web
|
|
63
|
+
npx executable-stories-init@latest --playwright --target root
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Preview without changes:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx executable-stories-init@latest --both --dry-run
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
CI / scripting:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx executable-stories-init@latest --both --target root --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## After it runs
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cd <target> # repeat per selected target
|
|
82
|
+
<pm> install # if you ran with --dry-run, or just to be sure
|
|
83
|
+
<pm> run test # Vitest stories
|
|
84
|
+
<pm> run test:e2e # Playwright stories (if installed)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Open the generated report:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# macOS
|
|
91
|
+
open reports/executable-stories.html
|
|
92
|
+
# Linux
|
|
93
|
+
xdg-open reports/executable-stories.html
|
|
94
|
+
# Windows
|
|
95
|
+
start reports\executable-stories.html
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Playwright also needs browsers on first use:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pnpm exec playwright install # pnpm
|
|
102
|
+
npm exec playwright install # npm
|
|
103
|
+
yarn playwright install # yarn
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Detection rules
|
|
107
|
+
|
|
108
|
+
- **Package manager** is taken from `packageManager` in the root `package.json`, then `pnpm-lock.yaml`, then `yarn.lock`, falling back to `npm`.
|
|
109
|
+
- **Monorepo** is detected from `pnpm-workspace.yaml` (`packages:` entries) or `workspaces` in `package.json` (array or `{ packages: [...] }`).
|
|
110
|
+
- **Already set up?** If the framework is already a dependency, install is skipped; if a `vitest.config.*` / `playwright.config.*` exists, the config write is skipped unless `--force` is passed.
|
|
111
|
+
|
|
112
|
+
## Exit codes
|
|
113
|
+
|
|
114
|
+
| Code | Meaning |
|
|
115
|
+
| ---- | ------- |
|
|
116
|
+
| `0` | Success. |
|
|
117
|
+
| `1` | Plan applied with failures (e.g. install failed). |
|
|
118
|
+
| `2` | Invalid CLI usage (e.g. `--json` without a framework). |
|
|
119
|
+
| `130` | Cancelled by user (prompt aborted or no targets selected). |
|
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import process3 from "process";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
import * as p3 from "@clack/prompts";
|
|
8
|
+
|
|
9
|
+
// src/factory.ts
|
|
10
|
+
import process from "process";
|
|
11
|
+
import { mkdir, readFile, writeFile, access } from "fs/promises";
|
|
12
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
13
|
+
function buildOpts(flags) {
|
|
14
|
+
const isTty = process.stdin.isTTY ?? false;
|
|
15
|
+
const json = Boolean(flags.json);
|
|
16
|
+
const yes = Boolean(flags.yes);
|
|
17
|
+
const interactive = !yes && !json && (Boolean(flags.interactive) || isTty);
|
|
18
|
+
return { json, interactive };
|
|
19
|
+
}
|
|
20
|
+
function buildDeps(opts, cwd = process.cwd()) {
|
|
21
|
+
return {
|
|
22
|
+
cwd,
|
|
23
|
+
opts,
|
|
24
|
+
fs: {
|
|
25
|
+
readFile: (p4) => readFile(p4, "utf8"),
|
|
26
|
+
writeFile: (p4, c) => writeFile(p4, c),
|
|
27
|
+
exists: async (p4) => {
|
|
28
|
+
try {
|
|
29
|
+
await access(p4);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
mkdir: async (p4) => {
|
|
36
|
+
await mkdir(p4, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
spawn: (cmd, args, { cwd: cwd2 }) => new Promise((resolve2) => {
|
|
40
|
+
const child = nodeSpawn(cmd, args, { cwd: cwd2, stdio: ["ignore", "pipe", "pipe"] });
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
child.stdout.on("data", (d) => {
|
|
44
|
+
stdout += d.toString();
|
|
45
|
+
});
|
|
46
|
+
child.stderr.on("data", (d) => {
|
|
47
|
+
stderr += d.toString();
|
|
48
|
+
});
|
|
49
|
+
child.on("close", (code) => resolve2({ code: code ?? 0, stdout, stderr }));
|
|
50
|
+
})
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/detect.ts
|
|
55
|
+
import { readFile as readFile2, access as access2 } from "fs/promises";
|
|
56
|
+
import { join } from "path";
|
|
57
|
+
import fg from "fast-glob";
|
|
58
|
+
async function exists(p4) {
|
|
59
|
+
try {
|
|
60
|
+
await access2(p4);
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function readJson(path) {
|
|
67
|
+
return JSON.parse(await readFile2(path, "utf8"));
|
|
68
|
+
}
|
|
69
|
+
var EMPTY_PKG = {};
|
|
70
|
+
async function detectPackageManager(cwd) {
|
|
71
|
+
const root = await readJson(join(cwd, "package.json")).catch(() => EMPTY_PKG);
|
|
72
|
+
if (typeof root.packageManager === "string") {
|
|
73
|
+
if (root.packageManager.startsWith("pnpm")) return "pnpm";
|
|
74
|
+
if (root.packageManager.startsWith("yarn")) return "yarn";
|
|
75
|
+
if (root.packageManager.startsWith("npm")) return "npm";
|
|
76
|
+
}
|
|
77
|
+
if (await exists(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
78
|
+
if (await exists(join(cwd, "yarn.lock"))) return "yarn";
|
|
79
|
+
return "npm";
|
|
80
|
+
}
|
|
81
|
+
function depPresent(pkg, name) {
|
|
82
|
+
const deps = pkg?.dependencies;
|
|
83
|
+
const devDeps = pkg?.devDependencies;
|
|
84
|
+
return Boolean(deps?.[name] || devDeps?.[name]);
|
|
85
|
+
}
|
|
86
|
+
async function statSet(target) {
|
|
87
|
+
const flags = /* @__PURE__ */ new Set();
|
|
88
|
+
if (await exists(join(target, "tsconfig.json"))) flags.add("ts");
|
|
89
|
+
for (const f of ["vitest.config.ts", "vitest.config.js", "vitest.config.mjs"]) {
|
|
90
|
+
if (await exists(join(target, f))) {
|
|
91
|
+
flags.add("vitest-config");
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const f of ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"]) {
|
|
96
|
+
if (await exists(join(target, f))) {
|
|
97
|
+
flags.add("playwright-config");
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return flags;
|
|
102
|
+
}
|
|
103
|
+
async function readWorkspaceGlobs(cwd) {
|
|
104
|
+
const pw = join(cwd, "pnpm-workspace.yaml");
|
|
105
|
+
if (await exists(pw)) {
|
|
106
|
+
const text = await readFile2(pw, "utf8");
|
|
107
|
+
const lines = text.split("\n");
|
|
108
|
+
const globs = [];
|
|
109
|
+
let inPackages = false;
|
|
110
|
+
for (const raw of lines) {
|
|
111
|
+
const line = raw.trim();
|
|
112
|
+
if (line.startsWith("packages:")) {
|
|
113
|
+
inPackages = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (inPackages) {
|
|
117
|
+
if (line.startsWith("- ")) {
|
|
118
|
+
globs.push(line.slice(2).replace(/^["']|["']$/g, ""));
|
|
119
|
+
} else if (line && !line.startsWith("#")) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return globs;
|
|
125
|
+
}
|
|
126
|
+
const rootPkg = await readJson(join(cwd, "package.json")).catch(() => EMPTY_PKG);
|
|
127
|
+
const workspaces = rootPkg.workspaces;
|
|
128
|
+
if (Array.isArray(workspaces)) return workspaces;
|
|
129
|
+
const workspacePkgs = workspaces?.packages;
|
|
130
|
+
if (Array.isArray(workspacePkgs)) return workspacePkgs;
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
async function listWorkspacePackages(cwd) {
|
|
134
|
+
const globs = await readWorkspaceGlobs(cwd);
|
|
135
|
+
if (!globs) return [];
|
|
136
|
+
const pjPaths = await fg(globs.map((g) => g.replace(/\/?$/, "/package.json")), { cwd, absolute: true });
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const pj of pjPaths) {
|
|
139
|
+
const pkg = await readJson(pj).catch(() => EMPTY_PKG);
|
|
140
|
+
if (typeof pkg.name === "string") out.push({ name: pkg.name, path: pj.replace(/\/package\.json$/, "") });
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
async function detectRepo(args, _deps) {
|
|
145
|
+
const { cwd } = args;
|
|
146
|
+
const rootPkg = await readJson(join(cwd, "package.json")).catch(() => EMPTY_PKG);
|
|
147
|
+
const packageManager = await detectPackageManager(cwd);
|
|
148
|
+
const stats = /* @__PURE__ */ new Map();
|
|
149
|
+
stats.set(cwd, await statSet(cwd));
|
|
150
|
+
const pkgCache = /* @__PURE__ */ new Map();
|
|
151
|
+
pkgCache.set(cwd, rootPkg);
|
|
152
|
+
const rootName = typeof rootPkg.name === "string" ? rootPkg.name : "root";
|
|
153
|
+
const rootCandidate = { name: rootName, path: cwd };
|
|
154
|
+
const workspacePackages = await listWorkspacePackages(cwd);
|
|
155
|
+
const isMonorepo = workspacePackages.length > 0;
|
|
156
|
+
const candidates = isMonorepo ? [rootCandidate, ...workspacePackages] : [rootCandidate];
|
|
157
|
+
for (const wp of workspacePackages) {
|
|
158
|
+
pkgCache.set(wp.path, await readJson(join(wp.path, "package.json")).catch(() => EMPTY_PKG));
|
|
159
|
+
stats.set(wp.path, await statSet(wp.path));
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
cwd,
|
|
163
|
+
packageManager,
|
|
164
|
+
isMonorepo,
|
|
165
|
+
workspacePackages,
|
|
166
|
+
candidates,
|
|
167
|
+
hasDependency: (t, dep) => depPresent(pkgCache.get(t) ?? rootPkg, dep),
|
|
168
|
+
hasTypeScript: (t) => stats.get(t)?.has("ts") ?? false,
|
|
169
|
+
hasVitest: (t) => depPresent(pkgCache.get(t) ?? rootPkg, "vitest"),
|
|
170
|
+
hasPlaywright: (t) => depPresent(pkgCache.get(t) ?? rootPkg, "@playwright/test"),
|
|
171
|
+
hasExistingVitestConfig: (t) => stats.get(t)?.has("vitest-config") ?? false,
|
|
172
|
+
hasExistingPlaywrightConfig: (t) => stats.get(t)?.has("playwright-config") ?? false
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/plan.ts
|
|
177
|
+
import { join as join3 } from "path";
|
|
178
|
+
|
|
179
|
+
// src/templates/index.ts
|
|
180
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
181
|
+
import { fileURLToPath } from "url";
|
|
182
|
+
import { dirname, join as join2 } from "path";
|
|
183
|
+
var _fileDir = dirname(fileURLToPath(import.meta.url));
|
|
184
|
+
var here = _fileDir.endsWith("/cli") ? join2(_fileDir, "..", "templates") : _fileDir;
|
|
185
|
+
async function renderTemplate(name) {
|
|
186
|
+
return readFile3(join2(here, `${name}.tmpl`), "utf8");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/plan.ts
|
|
190
|
+
async function resolvePlan(args, _deps) {
|
|
191
|
+
const { facts, flags } = args;
|
|
192
|
+
const ops = [];
|
|
193
|
+
if (flags.writeTsconfig) {
|
|
194
|
+
for (const target of flags.targets) {
|
|
195
|
+
if (!facts.hasTypeScript(target)) {
|
|
196
|
+
ops.push({
|
|
197
|
+
kind: "write",
|
|
198
|
+
target,
|
|
199
|
+
path: join3(target, "tsconfig.json"),
|
|
200
|
+
contents: await renderTemplate("tsconfig.json")
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const target of flags.targets) {
|
|
206
|
+
if (flags.frameworks.includes("vitest")) {
|
|
207
|
+
const vitestDeps = ["vitest", "executable-stories-vitest", "executable-stories-formatters"];
|
|
208
|
+
const missing = vitestDeps.filter((dep) => !facts.hasDependency(target, dep));
|
|
209
|
+
if (missing.length > 0) {
|
|
210
|
+
ops.push({
|
|
211
|
+
kind: "install",
|
|
212
|
+
target,
|
|
213
|
+
deps: missing,
|
|
214
|
+
dev: true,
|
|
215
|
+
packageManager: facts.packageManager
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (!facts.hasExistingVitestConfig(target) || flags.force) {
|
|
219
|
+
ops.push({
|
|
220
|
+
kind: "write",
|
|
221
|
+
target,
|
|
222
|
+
path: join3(target, "vitest.config.ts"),
|
|
223
|
+
contents: await renderTemplate("vitest-config.ts")
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
ops.push({
|
|
227
|
+
kind: "write",
|
|
228
|
+
target,
|
|
229
|
+
path: join3(target, "src/example.story.test.ts"),
|
|
230
|
+
contents: await renderTemplate("vitest-sample.story.test.ts")
|
|
231
|
+
});
|
|
232
|
+
ops.push({ kind: "patch-package-json", target, scripts: { test: "vitest run" } });
|
|
233
|
+
}
|
|
234
|
+
if (flags.frameworks.includes("playwright")) {
|
|
235
|
+
const pwDeps = ["@playwright/test", "executable-stories-playwright", "executable-stories-formatters"];
|
|
236
|
+
const missing = pwDeps.filter((dep) => !facts.hasDependency(target, dep));
|
|
237
|
+
if (missing.length > 0) {
|
|
238
|
+
ops.push({
|
|
239
|
+
kind: "install",
|
|
240
|
+
target,
|
|
241
|
+
deps: missing,
|
|
242
|
+
dev: true,
|
|
243
|
+
packageManager: facts.packageManager
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (!facts.hasExistingPlaywrightConfig(target) || flags.force) {
|
|
247
|
+
ops.push({
|
|
248
|
+
kind: "write",
|
|
249
|
+
target,
|
|
250
|
+
path: join3(target, "playwright.config.ts"),
|
|
251
|
+
contents: await renderTemplate("playwright-config.ts")
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
ops.push({
|
|
255
|
+
kind: "write",
|
|
256
|
+
target,
|
|
257
|
+
path: join3(target, "tests/example.story.spec.ts"),
|
|
258
|
+
contents: await renderTemplate("playwright-sample.story.spec.ts")
|
|
259
|
+
});
|
|
260
|
+
ops.push({
|
|
261
|
+
kind: "patch-package-json",
|
|
262
|
+
target,
|
|
263
|
+
scripts: { "test:e2e": "playwright test" }
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
ops,
|
|
269
|
+
summary: { targets: flags.targets, frameworks: flags.frameworks, packageManager: facts.packageManager }
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/apply.ts
|
|
274
|
+
async function applyPlan(args, deps) {
|
|
275
|
+
const { plan, dryRun, force } = args;
|
|
276
|
+
const written = [];
|
|
277
|
+
const installed = [];
|
|
278
|
+
const patched = [];
|
|
279
|
+
const skipped = [];
|
|
280
|
+
const notes = [];
|
|
281
|
+
for (const op of plan.ops) {
|
|
282
|
+
if (dryRun) {
|
|
283
|
+
notes.push(`[dry-run] ${describeOp(op)}`);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
switch (op.kind) {
|
|
287
|
+
case "write":
|
|
288
|
+
await applyWrite(op, force, deps, written, skipped);
|
|
289
|
+
break;
|
|
290
|
+
case "install":
|
|
291
|
+
await applyInstall(op, deps, installed);
|
|
292
|
+
break;
|
|
293
|
+
case "patch-package-json":
|
|
294
|
+
await applyPatch(op, force, deps, patched, skipped);
|
|
295
|
+
break;
|
|
296
|
+
case "note":
|
|
297
|
+
notes.push(`${op.level}: ${op.message}`);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return { ok: true, written, installed, patched, skipped, notes };
|
|
302
|
+
}
|
|
303
|
+
function describeOp(op) {
|
|
304
|
+
switch (op.kind) {
|
|
305
|
+
case "write":
|
|
306
|
+
return `write ${op.path}`;
|
|
307
|
+
case "install":
|
|
308
|
+
return `install ${op.deps.join(" ")} in ${op.target}`;
|
|
309
|
+
case "patch-package-json":
|
|
310
|
+
return `patch ${op.target}/package.json scripts: ${Object.keys(op.scripts).join(", ")}`;
|
|
311
|
+
case "note":
|
|
312
|
+
return op.message;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function applyWrite(op, force, deps, written, skipped) {
|
|
316
|
+
if (await deps.fs.exists(op.path)) {
|
|
317
|
+
const existing = await deps.fs.readFile(op.path);
|
|
318
|
+
if (existing === op.contents) {
|
|
319
|
+
skipped.push({ path: op.path, reason: "identical" });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!force) {
|
|
323
|
+
skipped.push({ path: op.path, reason: "exists; use --force to overwrite" });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
await deps.fs.mkdir(parentDir(op.path));
|
|
328
|
+
await deps.fs.writeFile(op.path, op.contents);
|
|
329
|
+
written.push(op.path);
|
|
330
|
+
}
|
|
331
|
+
async function applyInstall(op, deps, installed) {
|
|
332
|
+
const pmCmd = op.packageManager;
|
|
333
|
+
const args = pmCmd === "npm" ? ["install", "--save-dev", ...op.deps] : ["add", "-D", ...op.deps];
|
|
334
|
+
const r = await deps.spawn(pmCmd, args, { cwd: op.target });
|
|
335
|
+
if (r.code !== 0) {
|
|
336
|
+
const stderr = r.stderr.trim() || "(no stderr output)";
|
|
337
|
+
const manual = `${pmCmd} ${args.join(" ")}`;
|
|
338
|
+
throw new Error(
|
|
339
|
+
`${pmCmd} dependency install failed in ${op.target}
|
|
340
|
+
command: ${manual}
|
|
341
|
+
stderr: ${stderr}
|
|
342
|
+
next steps:
|
|
343
|
+
1. Run with --dry-run to inspect planned operations.
|
|
344
|
+
2. Re-run install manually in target: cd ${op.target} && ${manual}
|
|
345
|
+
3. Verify your package manager and lockfile are healthy.`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
installed.push(...op.deps);
|
|
349
|
+
}
|
|
350
|
+
async function applyPatch(op, force, deps, patched, skipped) {
|
|
351
|
+
const path = `${op.target}/package.json`;
|
|
352
|
+
const raw = await deps.fs.readFile(path);
|
|
353
|
+
const pkg = JSON.parse(raw);
|
|
354
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
355
|
+
let changed = false;
|
|
356
|
+
for (const [name, value] of Object.entries(op.scripts)) {
|
|
357
|
+
const existing = pkg.scripts[name];
|
|
358
|
+
if (existing === value) {
|
|
359
|
+
skipped.push({ path: `${path}#scripts.${name}`, reason: "identical" });
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (existing && !force) {
|
|
363
|
+
skipped.push({ path: `${path}#scripts.${name}`, reason: `exists: ${existing}` });
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
pkg.scripts[name] = value;
|
|
367
|
+
changed = true;
|
|
368
|
+
}
|
|
369
|
+
if (changed) {
|
|
370
|
+
await deps.fs.writeFile(path, JSON.stringify(pkg, null, 2) + "\n");
|
|
371
|
+
patched.push(path);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function parentDir(path) {
|
|
375
|
+
if (/^[A-Za-z]:\\[^\\]+$/.test(path) || /^[A-Za-z]:\\[^\\]*\.[^\\]+$/.test(path)) {
|
|
376
|
+
return `${path.slice(0, 2)}\\`;
|
|
377
|
+
}
|
|
378
|
+
const normalized = path.replace(/\\/g, "/");
|
|
379
|
+
const idx = normalized.lastIndexOf("/");
|
|
380
|
+
if (idx <= 0) return ".";
|
|
381
|
+
const dir = normalized.slice(0, idx);
|
|
382
|
+
if (/^[A-Za-z]:$/.test(dir) && path.includes("\\")) return `${dir}\\`;
|
|
383
|
+
return dir.replace(/\//g, path.includes("\\") ? "\\" : "/");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/render/json.ts
|
|
387
|
+
function renderJson(args) {
|
|
388
|
+
return JSON.stringify({ ok: args.result.ok, plan: args.plan, result: args.result }, null, 2);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/render/human.ts
|
|
392
|
+
import * as p from "@clack/prompts";
|
|
393
|
+
import pc from "picocolors";
|
|
394
|
+
import process2 from "process";
|
|
395
|
+
function renderHuman({ plan, result }) {
|
|
396
|
+
const lines = [];
|
|
397
|
+
lines.push(pc.bold("Targets:"), " " + plan.summary.targets.join("\n "));
|
|
398
|
+
lines.push(pc.bold("Frameworks:"), " " + plan.summary.frameworks.join(", "));
|
|
399
|
+
if (result.written.length) {
|
|
400
|
+
lines.push(pc.green(`+ wrote (${result.written.length})`));
|
|
401
|
+
lines.push(...result.written.map((w) => " " + w));
|
|
402
|
+
}
|
|
403
|
+
if (result.installed.length) {
|
|
404
|
+
lines.push(pc.cyan(`\u21BB installed (${result.installed.length})`));
|
|
405
|
+
lines.push(...result.installed.map((i) => " " + i));
|
|
406
|
+
}
|
|
407
|
+
if (result.patched.length) {
|
|
408
|
+
lines.push(pc.cyan(`\u2261 patched (${result.patched.length})`));
|
|
409
|
+
lines.push(...result.patched.map((i) => " " + i));
|
|
410
|
+
}
|
|
411
|
+
if (result.skipped.length) {
|
|
412
|
+
lines.push(pc.yellow(`~ skipped (${result.skipped.length})`));
|
|
413
|
+
lines.push(...result.skipped.map((s) => ` ${s.path}: ${s.reason}`));
|
|
414
|
+
}
|
|
415
|
+
if (result.notes.length) {
|
|
416
|
+
lines.push(pc.dim("notes"));
|
|
417
|
+
lines.push(...result.notes.map((n) => " " + n));
|
|
418
|
+
}
|
|
419
|
+
p.note(lines.join("\n"), "summary");
|
|
420
|
+
const targets = plan.summary.targets;
|
|
421
|
+
const next = ["For each target:"];
|
|
422
|
+
const pm = plan.summary.packageManager;
|
|
423
|
+
const installCmd = pm === "yarn" ? "yarn install" : `${pm} install`;
|
|
424
|
+
const runTest = pm === "yarn" ? "yarn test" : `${pm} run test`;
|
|
425
|
+
const playwrightInstall = pm === "yarn" ? "yarn playwright install" : `${pm} exec playwright install`;
|
|
426
|
+
const openReport = reportOpenCommand();
|
|
427
|
+
for (const t of targets) {
|
|
428
|
+
next.push(` cd ${t}`);
|
|
429
|
+
next.push(` ${installCmd}`);
|
|
430
|
+
next.push(` ${runTest}`);
|
|
431
|
+
if (plan.summary.frameworks.includes("playwright")) {
|
|
432
|
+
next.push(` ${playwrightInstall}`);
|
|
433
|
+
}
|
|
434
|
+
next.push(` ${openReport}`);
|
|
435
|
+
next.push("");
|
|
436
|
+
}
|
|
437
|
+
p.outro(`Next:
|
|
438
|
+
${next.join("\n").trimEnd()}`);
|
|
439
|
+
}
|
|
440
|
+
function reportOpenCommand() {
|
|
441
|
+
switch (process2.platform) {
|
|
442
|
+
case "darwin":
|
|
443
|
+
return "open reports/executable-stories.html";
|
|
444
|
+
case "win32":
|
|
445
|
+
return "start reports\\executable-stories.html";
|
|
446
|
+
default:
|
|
447
|
+
return "xdg-open reports/executable-stories.html";
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/wizard.ts
|
|
452
|
+
import * as p2 from "@clack/prompts";
|
|
453
|
+
import pc2 from "picocolors";
|
|
454
|
+
async function promptTargets(facts) {
|
|
455
|
+
const choice = await p2.multiselect({
|
|
456
|
+
message: "Which package(s)?",
|
|
457
|
+
options: facts.candidates.map((c) => ({ value: c.path, label: c.name })),
|
|
458
|
+
required: true
|
|
459
|
+
});
|
|
460
|
+
if (p2.isCancel(choice)) return [];
|
|
461
|
+
return choice;
|
|
462
|
+
}
|
|
463
|
+
async function runWizard(args, _deps) {
|
|
464
|
+
const { facts } = args;
|
|
465
|
+
p2.intro(pc2.bgCyan(pc2.black(" executable-stories ")));
|
|
466
|
+
p2.note(summarise(facts), "detected");
|
|
467
|
+
const fw = await p2.multiselect({
|
|
468
|
+
message: "Which framework(s)?",
|
|
469
|
+
options: [
|
|
470
|
+
{
|
|
471
|
+
value: "vitest",
|
|
472
|
+
label: "Vitest",
|
|
473
|
+
hint: facts.hasVitest(facts.cwd) ? "already installed" : void 0
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
value: "playwright",
|
|
477
|
+
label: "Playwright",
|
|
478
|
+
hint: facts.hasPlaywright(facts.cwd) ? "already installed" : void 0
|
|
479
|
+
}
|
|
480
|
+
],
|
|
481
|
+
required: true
|
|
482
|
+
});
|
|
483
|
+
if (p2.isCancel(fw)) {
|
|
484
|
+
p2.cancel("Cancelled");
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
return { frameworks: fw };
|
|
488
|
+
}
|
|
489
|
+
function summarise(facts) {
|
|
490
|
+
return [
|
|
491
|
+
`package manager: ${facts.packageManager}`,
|
|
492
|
+
`typescript: ${facts.hasTypeScript(facts.cwd) ? "yes" : "no"}`,
|
|
493
|
+
`monorepo: ${facts.isMonorepo ? `yes (${facts.workspacePackages.length} workspace packages)` : "no"}`
|
|
494
|
+
].join("\n");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/targets.ts
|
|
498
|
+
function targetLeaf(path) {
|
|
499
|
+
const normalized = path.replace(/\\/g, "/");
|
|
500
|
+
return normalized.split("/").filter(Boolean).at(-1) ?? normalized;
|
|
501
|
+
}
|
|
502
|
+
function unique(values) {
|
|
503
|
+
return [...new Set(values)];
|
|
504
|
+
}
|
|
505
|
+
function targetHelpList(facts) {
|
|
506
|
+
const names = facts.workspacePackages.map((w) => w.name);
|
|
507
|
+
const leaves = facts.workspacePackages.map((w) => targetLeaf(w.path));
|
|
508
|
+
return unique(["root", ...names, ...leaves]).join(", ");
|
|
509
|
+
}
|
|
510
|
+
async function resolveTargets(flag, facts, opts) {
|
|
511
|
+
if (flag && flag.length > 0) {
|
|
512
|
+
const out = [];
|
|
513
|
+
for (const t of flag) {
|
|
514
|
+
if (t === "root") {
|
|
515
|
+
out.push(facts.cwd);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const matches = facts.workspacePackages.filter(
|
|
519
|
+
(w) => w.name === t || targetLeaf(w.path) === t
|
|
520
|
+
);
|
|
521
|
+
if (matches.length === 1) out.push(matches[0].path);
|
|
522
|
+
else if (matches.length > 1) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
`ambiguous target: ${t}
|
|
525
|
+
matches:
|
|
526
|
+
${matches.map((w) => ` - ${w.name} (${w.path})`).join("\n")}
|
|
527
|
+
tip: use the full workspace package name with --target`
|
|
528
|
+
);
|
|
529
|
+
} else {
|
|
530
|
+
const validNames = targetHelpList(facts);
|
|
531
|
+
throw new Error(
|
|
532
|
+
`unknown target: ${t}
|
|
533
|
+
valid targets: ${validNames}
|
|
534
|
+
tip: use --target root for the repo root`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return unique(out);
|
|
539
|
+
}
|
|
540
|
+
if (!facts.isMonorepo) return [facts.cwd];
|
|
541
|
+
if (!opts.interactive) return [facts.cwd];
|
|
542
|
+
return promptTargets(facts);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/errors.ts
|
|
546
|
+
function formatCliError(message, json) {
|
|
547
|
+
if (json) return JSON.stringify({ ok: false, error: message }, null, 2);
|
|
548
|
+
return `Error: ${message}`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/index.ts
|
|
552
|
+
async function confirmPlan(plan) {
|
|
553
|
+
const lines = [
|
|
554
|
+
`Targets: ${plan.summary.targets.join(", ")}`,
|
|
555
|
+
`Frameworks: ${plan.summary.frameworks.join(", ")}`,
|
|
556
|
+
"",
|
|
557
|
+
"Operations:",
|
|
558
|
+
...plan.ops.map((op) => {
|
|
559
|
+
switch (op.kind) {
|
|
560
|
+
case "install":
|
|
561
|
+
return ` install ${op.deps.join(" ")} (${op.packageManager})`;
|
|
562
|
+
case "write":
|
|
563
|
+
return ` write ${op.path}`;
|
|
564
|
+
case "patch-package-json":
|
|
565
|
+
return ` patch ${op.target}/package.json scripts: ${Object.keys(op.scripts).join(", ")}`;
|
|
566
|
+
case "note":
|
|
567
|
+
return ` ${op.level}: ${op.message}`;
|
|
568
|
+
}
|
|
569
|
+
})
|
|
570
|
+
];
|
|
571
|
+
p3.note(lines.join("\n"), "plan preview");
|
|
572
|
+
const ok = await p3.confirm({ message: "Proceed?", initialValue: true });
|
|
573
|
+
return !p3.isCancel(ok) && ok === true;
|
|
574
|
+
}
|
|
575
|
+
var program = new Command();
|
|
576
|
+
program.name("executable-stories-init").description("Bootstrap executable-stories (Vitest + Playwright) into a TypeScript repo").argument("[target]", "target directory (defaults to cwd)").option("--vitest", "set up Vitest").option("--playwright", "set up Playwright").option("--both", "set up both Vitest and Playwright").option("--ts", "write tsconfig.json if missing").option("--no-ts", "do not write tsconfig.json").option("-y, --yes", "accept all defaults, non-interactive").option("--dry-run", "print plan but do not write or install").option("--json", "machine-readable JSON output (implies --yes)").option("--interactive", "force interactive prompts").option("--force", "overwrite differing existing files").option("--target <pkg...>", "workspace package(s) to set up (default: prompt or root)").action(async (target, opts) => {
|
|
577
|
+
const cliOpts = buildOpts(opts);
|
|
578
|
+
try {
|
|
579
|
+
const cwd = resolve(target ?? process3.cwd());
|
|
580
|
+
const deps = buildDeps(cliOpts, cwd);
|
|
581
|
+
const facts = await detectRepo({ cwd }, deps);
|
|
582
|
+
const frameworks = opts.both ? ["vitest", "playwright"] : [opts.vitest && "vitest", opts.playwright && "playwright"].filter(Boolean);
|
|
583
|
+
if (frameworks.length === 0 && cliOpts.json) {
|
|
584
|
+
console.error(formatCliError("--json requires at least one of --vitest, --playwright, --both", true));
|
|
585
|
+
process3.exit(2);
|
|
586
|
+
}
|
|
587
|
+
if (frameworks.length === 0) {
|
|
588
|
+
const chosen = await runWizard({ facts }, deps);
|
|
589
|
+
if (!chosen) process3.exit(130);
|
|
590
|
+
frameworks.push(...chosen.frameworks);
|
|
591
|
+
}
|
|
592
|
+
const targets = await resolveTargets(opts.target, facts, cliOpts);
|
|
593
|
+
if (targets.length === 0) process3.exit(130);
|
|
594
|
+
const flags = {
|
|
595
|
+
targets,
|
|
596
|
+
frameworks,
|
|
597
|
+
writeTsconfig: opts.ts === true && targets.some((t) => !facts.hasTypeScript(t)),
|
|
598
|
+
force: Boolean(opts.force)
|
|
599
|
+
};
|
|
600
|
+
const plan = await resolvePlan({ facts, flags }, deps);
|
|
601
|
+
if (cliOpts.interactive && !opts.dryRun) {
|
|
602
|
+
const ok = await confirmPlan(plan);
|
|
603
|
+
if (!ok) {
|
|
604
|
+
if (!cliOpts.json) console.log("Cancelled.");
|
|
605
|
+
process3.exit(130);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const result = await applyPlan({ plan, dryRun: Boolean(opts.dryRun), force: flags.force }, deps);
|
|
609
|
+
if (cliOpts.json) {
|
|
610
|
+
console.log(renderJson({ plan, result }));
|
|
611
|
+
} else {
|
|
612
|
+
renderHuman({ plan, result });
|
|
613
|
+
}
|
|
614
|
+
process3.exit(result.ok ? 0 : 1);
|
|
615
|
+
} catch (error) {
|
|
616
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
617
|
+
console.error(formatCliError(message, cliOpts.json));
|
|
618
|
+
process3.exit(1);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
await program.parseAsync(process3.argv);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// When bundled by tsup into dist/cli/index.js, import.meta.url resolves to dist/cli/.
|
|
6
|
+
// Templates are copied to dist/templates/ by tsup onSuccess, so we go up one level.
|
|
7
|
+
// In source (dev/test), __dirname is src/templates/ and templates are co-located there.
|
|
8
|
+
const _fileDir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const here = _fileDir.endsWith('/cli') ? join(_fileDir, '..', 'templates') : _fileDir;
|
|
10
|
+
|
|
11
|
+
const _NAMES = [
|
|
12
|
+
'vitest-config.ts',
|
|
13
|
+
'playwright-config.ts',
|
|
14
|
+
'vitest-sample.story.test.ts',
|
|
15
|
+
'playwright-sample.story.spec.ts',
|
|
16
|
+
'tsconfig.json',
|
|
17
|
+
] as const;
|
|
18
|
+
export type TemplateName = (typeof _NAMES)[number];
|
|
19
|
+
|
|
20
|
+
export async function renderTemplate(name: TemplateName): Promise<string> {
|
|
21
|
+
return readFile(join(here, `${name}.tmpl`), 'utf8');
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { defineConfig } from '@playwright/test';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const reporterPath = require.resolve('executable-stories-playwright/reporter');
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
testMatch: '**/*.story.spec.ts',
|
|
9
|
+
reporter: [
|
|
10
|
+
['list'],
|
|
11
|
+
[reporterPath, { formats: ['markdown', 'html'], outputDir: 'reports' }],
|
|
12
|
+
],
|
|
13
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
import { given, then, when } from 'executable-stories-playwright';
|
|
3
|
+
|
|
4
|
+
test('Loads example.com', async ({ page }) => {
|
|
5
|
+
await given('a fresh browser context', async () => {});
|
|
6
|
+
await when('the user visits example.com', async () => {
|
|
7
|
+
await page.goto('https://example.com');
|
|
8
|
+
});
|
|
9
|
+
await then('the title contains Example', async () => {
|
|
10
|
+
await expect(page).toHaveTitle(/Example/);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import type { Reporter } from 'vitest/node';
|
|
3
|
+
import { defineConfig } from 'vitest/config';
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const { StoryReporter } = require('executable-stories-vitest/reporter');
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
test: {
|
|
10
|
+
reporters: [
|
|
11
|
+
'default',
|
|
12
|
+
new StoryReporter({
|
|
13
|
+
formats: ['markdown', 'html'],
|
|
14
|
+
outputDir: 'reports',
|
|
15
|
+
outputName: 'executable-stories',
|
|
16
|
+
rawRunPath: 'reports/raw-run.json',
|
|
17
|
+
}) as unknown as Reporter,
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { story } from 'executable-stories-vitest';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('Calculator', () => {
|
|
5
|
+
it('Adds two numbers', ({ task }) => {
|
|
6
|
+
story.init(task);
|
|
7
|
+
|
|
8
|
+
story.given('two numbers');
|
|
9
|
+
const a = 1;
|
|
10
|
+
const b = 2;
|
|
11
|
+
|
|
12
|
+
story.when('they are added');
|
|
13
|
+
const result = a + b;
|
|
14
|
+
|
|
15
|
+
story.then('the sum is 3');
|
|
16
|
+
expect(result).toBe(3);
|
|
17
|
+
});
|
|
18
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "executable-stories-init",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bootstrap executable-stories (Vitest + Playwright) into a TypeScript repo.",
|
|
5
|
+
"author": "Jag Reehal <jag@jagreehal.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"executable-stories-init": "./dist/cli/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@clack/prompts": "^1.3.0",
|
|
17
|
+
"commander": "^14.0.0",
|
|
18
|
+
"fast-glob": "^3.3.3",
|
|
19
|
+
"picocolors": "^1.1.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^25.6.0",
|
|
23
|
+
"eslint-config-executable-stories": "workspace:*",
|
|
24
|
+
"executable-stories-vitest": "workspace:*",
|
|
25
|
+
"tsup": "^8.5.1",
|
|
26
|
+
"typescript": "^6.0.3",
|
|
27
|
+
"vitest": "^4.1.4"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"type-check": "tsc --noEmit",
|
|
32
|
+
"lint": "eslint .",
|
|
33
|
+
"clean": "rm -rf dist",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"quality": "pnpm type-check && pnpm test && pnpm lint"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/jagreehal/executable-stories.git",
|
|
40
|
+
"directory": "packages/executable-stories-init"
|
|
41
|
+
}
|
|
42
|
+
}
|