@raftlabs/raftstack 1.9.3 → 1.10.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/.claude/commands/raftstack/inject.md +1 -0
- package/.claude/commands/raftstack/shape.md +10 -6
- package/.claude/skills/tdd/SKILL.md +586 -0
- package/dist/cli.js +505 -44
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -215,6 +215,72 @@ async function hasPrettier(targetDir = process.cwd()) {
|
|
|
215
215
|
}
|
|
216
216
|
return false;
|
|
217
217
|
}
|
|
218
|
+
async function hasReact(targetDir = process.cwd()) {
|
|
219
|
+
try {
|
|
220
|
+
const pkgPath = join2(targetDir, "package.json");
|
|
221
|
+
if (existsSync2(pkgPath)) {
|
|
222
|
+
const content = await readFile(pkgPath, "utf-8");
|
|
223
|
+
const pkg = JSON.parse(content);
|
|
224
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
225
|
+
return "react" in deps || "react-dom" in deps;
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
async function detectTestFramework(targetDir = process.cwd()) {
|
|
232
|
+
const vitestConfigFiles = [
|
|
233
|
+
"vitest.config.ts",
|
|
234
|
+
"vitest.config.js",
|
|
235
|
+
"vitest.config.mjs",
|
|
236
|
+
"vitest.config.cjs"
|
|
237
|
+
];
|
|
238
|
+
for (const file of vitestConfigFiles) {
|
|
239
|
+
if (existsSync2(join2(targetDir, file))) {
|
|
240
|
+
return "vitest";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const jestConfigFiles = [
|
|
244
|
+
"jest.config.ts",
|
|
245
|
+
"jest.config.js",
|
|
246
|
+
"jest.config.mjs",
|
|
247
|
+
"jest.config.cjs",
|
|
248
|
+
"jest.config.json"
|
|
249
|
+
];
|
|
250
|
+
for (const file of jestConfigFiles) {
|
|
251
|
+
if (existsSync2(join2(targetDir, file))) {
|
|
252
|
+
return "jest";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const pkgPath = join2(targetDir, "package.json");
|
|
257
|
+
if (existsSync2(pkgPath)) {
|
|
258
|
+
const content = await readFile(pkgPath, "utf-8");
|
|
259
|
+
const pkg = JSON.parse(content);
|
|
260
|
+
if (pkg.jest) {
|
|
261
|
+
return "jest";
|
|
262
|
+
}
|
|
263
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
264
|
+
if ("vitest" in deps) {
|
|
265
|
+
return "vitest";
|
|
266
|
+
}
|
|
267
|
+
if ("jest" in deps || "@jest/core" in deps) {
|
|
268
|
+
return "jest";
|
|
269
|
+
}
|
|
270
|
+
if (pkg.scripts?.test) {
|
|
271
|
+
const testScript = pkg.scripts.test;
|
|
272
|
+
if (testScript.includes("vitest")) {
|
|
273
|
+
return "vitest";
|
|
274
|
+
}
|
|
275
|
+
if (testScript.includes("jest")) {
|
|
276
|
+
return "jest";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
218
284
|
function getProjectTypeDescription(type) {
|
|
219
285
|
switch (type) {
|
|
220
286
|
case "nx":
|
|
@@ -386,6 +452,34 @@ async function promptPackageManager(targetDir) {
|
|
|
386
452
|
}
|
|
387
453
|
return getPackageManagerInfo(selected);
|
|
388
454
|
}
|
|
455
|
+
async function promptTestFramework(targetDir) {
|
|
456
|
+
const detected = await detectTestFramework(targetDir);
|
|
457
|
+
if (detected) {
|
|
458
|
+
p.log.info(`Detected ${pc.cyan(detected)} test framework`);
|
|
459
|
+
return detected;
|
|
460
|
+
}
|
|
461
|
+
p.log.warn("No test framework detected");
|
|
462
|
+
const selected = await p.select({
|
|
463
|
+
message: "Select a test framework:",
|
|
464
|
+
options: [
|
|
465
|
+
{
|
|
466
|
+
value: "vitest",
|
|
467
|
+
label: "Vitest",
|
|
468
|
+
hint: "Recommended for modern projects (fast, ESM-native)"
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
value: "jest",
|
|
472
|
+
label: "Jest",
|
|
473
|
+
hint: "Mature ecosystem, widely used"
|
|
474
|
+
}
|
|
475
|
+
]
|
|
476
|
+
});
|
|
477
|
+
if (p.isCancel(selected)) {
|
|
478
|
+
p.cancel("Setup cancelled.");
|
|
479
|
+
process.exit(0);
|
|
480
|
+
}
|
|
481
|
+
return selected;
|
|
482
|
+
}
|
|
389
483
|
async function promptConfirmation(config) {
|
|
390
484
|
console.log();
|
|
391
485
|
p.note(
|
|
@@ -395,6 +489,7 @@ async function promptConfirmation(config) {
|
|
|
395
489
|
`${pc.cyan("TypeScript:")} ${config.usesTypeScript ? "Yes" : "No"}`,
|
|
396
490
|
`${pc.cyan("ESLint:")} ${config.usesEslint ? "Yes" : "No"}`,
|
|
397
491
|
`${pc.cyan("Prettier:")} ${config.usesPrettier ? "Yes" : "No"}`,
|
|
492
|
+
`${pc.cyan("Test Framework:")} ${config.testFramework || "None"}`,
|
|
398
493
|
`${pc.cyan("Asana Integration:")} ${config.asanaBaseUrl ? "Yes" : "No"}`,
|
|
399
494
|
`${pc.cyan("AI Review:")} ${config.aiReviewTool === "none" ? "None" : config.aiReviewTool}`,
|
|
400
495
|
`${pc.cyan("CODEOWNERS:")} ${config.codeowners.length > 0 ? config.codeowners.join(", ") : "None"}`
|
|
@@ -419,6 +514,16 @@ async function collectConfig(targetDir = process.cwd()) {
|
|
|
419
514
|
const usesTypeScript = await hasTypeScript(targetDir);
|
|
420
515
|
const usesEslint = await hasEslint(targetDir);
|
|
421
516
|
const usesPrettier = await hasPrettier(targetDir);
|
|
517
|
+
const testFramework = await promptTestFramework(targetDir);
|
|
518
|
+
if (testFramework) {
|
|
519
|
+
p.note(
|
|
520
|
+
`${pc.green("\u2713")} TDD enforcement enabled:
|
|
521
|
+
\u2022 Tests run on commit (related tests)
|
|
522
|
+
\u2022 Full test suite + 80% coverage required on push
|
|
523
|
+
\u2022 Use ${pc.cyan("--no-verify")} to bypass (not recommended)`,
|
|
524
|
+
"Test-Driven Development"
|
|
525
|
+
);
|
|
526
|
+
}
|
|
422
527
|
const asanaBaseUrl = await promptAsanaConfig();
|
|
423
528
|
const aiReviewTool = await promptAIReview();
|
|
424
529
|
const codeowners = await promptCodeowners();
|
|
@@ -430,7 +535,8 @@ async function collectConfig(targetDir = process.cwd()) {
|
|
|
430
535
|
codeowners,
|
|
431
536
|
usesTypeScript,
|
|
432
537
|
usesEslint,
|
|
433
|
-
usesPrettier
|
|
538
|
+
usesPrettier,
|
|
539
|
+
testFramework
|
|
434
540
|
};
|
|
435
541
|
const confirmed = await promptConfirmation(config);
|
|
436
542
|
if (!confirmed) {
|
|
@@ -508,17 +614,30 @@ function getBuildCommand(projectType) {
|
|
|
508
614
|
return "pnpm build";
|
|
509
615
|
}
|
|
510
616
|
}
|
|
511
|
-
function getPrePushHook(projectType) {
|
|
617
|
+
function getPrePushHook(projectType, testFramework) {
|
|
512
618
|
const buildCommand = getBuildCommand(projectType);
|
|
513
|
-
|
|
619
|
+
let hook = `# Validate branch naming convention
|
|
514
620
|
validate-branch-name
|
|
515
|
-
|
|
621
|
+
`;
|
|
622
|
+
if (testFramework) {
|
|
623
|
+
hook += `
|
|
624
|
+
# Run full test suite with coverage (STRICT - blocks on failure)
|
|
625
|
+
echo "\u{1F9EA} Running full test suite with coverage..."
|
|
626
|
+
npm run test:coverage || {
|
|
627
|
+
echo "\u274C Tests failed or coverage below 80%. Push blocked."
|
|
628
|
+
echo "\u{1F4A1} Fix failing tests or use 'git push --no-verify' to bypass (not recommended)."
|
|
629
|
+
exit 1
|
|
630
|
+
}
|
|
631
|
+
`;
|
|
632
|
+
}
|
|
633
|
+
hook += `
|
|
516
634
|
# Build all packages - push only succeeds if builds pass
|
|
517
635
|
echo "\u{1F528} Building..."
|
|
518
636
|
${buildCommand}
|
|
519
637
|
`;
|
|
638
|
+
return hook;
|
|
520
639
|
}
|
|
521
|
-
async function generateHuskyHooks(targetDir, projectType, _pm) {
|
|
640
|
+
async function generateHuskyHooks(targetDir, projectType, _pm, testFramework) {
|
|
522
641
|
const result = {
|
|
523
642
|
created: [],
|
|
524
643
|
modified: [],
|
|
@@ -552,10 +671,14 @@ async function generateHuskyHooks(targetDir, projectType, _pm) {
|
|
|
552
671
|
}
|
|
553
672
|
}
|
|
554
673
|
const prePushPath = join4(huskyDir, "pre-push");
|
|
555
|
-
const prePushResult = await writeFileSafe(
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
674
|
+
const prePushResult = await writeFileSafe(
|
|
675
|
+
prePushPath,
|
|
676
|
+
getPrePushHook(projectType, testFramework),
|
|
677
|
+
{
|
|
678
|
+
executable: true,
|
|
679
|
+
backup: true
|
|
680
|
+
}
|
|
681
|
+
);
|
|
559
682
|
if (prePushResult.created) {
|
|
560
683
|
result.created.push(".husky/pre-push");
|
|
561
684
|
if (prePushResult.backedUp) {
|
|
@@ -727,7 +850,7 @@ async function generateCzGit(targetDir, _asanaBaseUrl) {
|
|
|
727
850
|
}
|
|
728
851
|
|
|
729
852
|
// src/generators/lint-staged.ts
|
|
730
|
-
function getLintStagedConfig(usesEslint, usesPrettier, usesTypeScript) {
|
|
853
|
+
function getLintStagedConfig(usesEslint, usesPrettier, usesTypeScript, testFramework) {
|
|
731
854
|
const config = {};
|
|
732
855
|
const codePatterns = [];
|
|
733
856
|
if (usesTypeScript) {
|
|
@@ -741,6 +864,13 @@ function getLintStagedConfig(usesEslint, usesPrettier, usesTypeScript) {
|
|
|
741
864
|
if (usesPrettier) {
|
|
742
865
|
codeCommands.push("prettier --write");
|
|
743
866
|
}
|
|
867
|
+
if (testFramework) {
|
|
868
|
+
if (testFramework === "vitest") {
|
|
869
|
+
codeCommands.push("vitest related --run --passWithNoTests");
|
|
870
|
+
} else {
|
|
871
|
+
codeCommands.push("jest --bail --findRelatedTests --passWithNoTests");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
744
874
|
if (codeCommands.length > 0) {
|
|
745
875
|
const pattern = `*.{${codePatterns.join(",")}}`;
|
|
746
876
|
config[pattern] = codeCommands;
|
|
@@ -933,11 +1063,18 @@ ${asanaSection}## Changes Made
|
|
|
933
1063
|
<!-- List the specific changes made in this PR -->
|
|
934
1064
|
-
|
|
935
1065
|
|
|
1066
|
+
## Development Process
|
|
1067
|
+
<!-- Mark completed items with an "x" -->
|
|
1068
|
+
- [ ] Tests written BEFORE implementation (TDD)
|
|
1069
|
+
- [ ] All tests pass locally (\`npm run test\`)
|
|
1070
|
+
- [ ] Coverage maintained/improved (\`npm run test:coverage\`)
|
|
1071
|
+
|
|
936
1072
|
## Testing
|
|
937
1073
|
<!-- Describe how you tested your changes -->
|
|
938
1074
|
- [ ] Unit tests added/updated
|
|
939
1075
|
- [ ] Integration tests added/updated
|
|
940
1076
|
- [ ] Manual testing performed
|
|
1077
|
+
- [ ] Coverage: [X]% (target: 80%+)
|
|
941
1078
|
|
|
942
1079
|
## Screenshots (if applicable)
|
|
943
1080
|
<!-- Add screenshots to help explain your changes -->
|
|
@@ -980,7 +1117,7 @@ async function generatePRTemplate(targetDir, hasAsana) {
|
|
|
980
1117
|
|
|
981
1118
|
// src/generators/github-workflows.ts
|
|
982
1119
|
import { join as join9 } from "path";
|
|
983
|
-
function getPRChecksWorkflow(projectType, usesTypeScript, usesEslint, pm) {
|
|
1120
|
+
function getPRChecksWorkflow(projectType, usesTypeScript, usesEslint, pm, testFramework) {
|
|
984
1121
|
const steps = [];
|
|
985
1122
|
steps.push(` - name: Checkout
|
|
986
1123
|
uses: actions/checkout@v4`);
|
|
@@ -1022,18 +1159,38 @@ function getPRChecksWorkflow(projectType, usesTypeScript, usesEslint, pm) {
|
|
|
1022
1159
|
- name: Build
|
|
1023
1160
|
run: ${pm.run} build`);
|
|
1024
1161
|
}
|
|
1025
|
-
if (
|
|
1162
|
+
if (testFramework) {
|
|
1163
|
+
if (projectType === "nx") {
|
|
1164
|
+
steps.push(`
|
|
1165
|
+
- name: Test with Coverage
|
|
1166
|
+
run: ${pm.run} nx affected --target=test --parallel=3 --coverage`);
|
|
1167
|
+
} else if (projectType === "turbo") {
|
|
1168
|
+
steps.push(`
|
|
1169
|
+
- name: Test with Coverage
|
|
1170
|
+
run: ${pm.run} turbo test:coverage`);
|
|
1171
|
+
} else {
|
|
1172
|
+
steps.push(`
|
|
1173
|
+
- name: Test with Coverage
|
|
1174
|
+
run: ${pm.run} test:coverage`);
|
|
1175
|
+
}
|
|
1026
1176
|
steps.push(`
|
|
1177
|
+
- name: Check Coverage Threshold
|
|
1178
|
+
run: |
|
|
1179
|
+
echo "\u2705 Coverage check passed (80% threshold enforced by test config)"`);
|
|
1180
|
+
} else {
|
|
1181
|
+
if (projectType === "nx") {
|
|
1182
|
+
steps.push(`
|
|
1027
1183
|
- name: Test
|
|
1028
1184
|
run: ${pm.run} nx affected --target=test --parallel=3`);
|
|
1029
|
-
|
|
1030
|
-
|
|
1185
|
+
} else if (projectType === "turbo") {
|
|
1186
|
+
steps.push(`
|
|
1031
1187
|
- name: Test
|
|
1032
1188
|
run: ${pm.run} turbo test`);
|
|
1033
|
-
|
|
1034
|
-
|
|
1189
|
+
} else {
|
|
1190
|
+
steps.push(`
|
|
1035
1191
|
- name: Test
|
|
1036
1192
|
run: ${pm.run} test`);
|
|
1193
|
+
}
|
|
1037
1194
|
}
|
|
1038
1195
|
return `name: PR Checks
|
|
1039
1196
|
|
|
@@ -1053,7 +1210,7 @@ jobs:
|
|
|
1053
1210
|
${steps.join("\n")}
|
|
1054
1211
|
`;
|
|
1055
1212
|
}
|
|
1056
|
-
async function generateGitHubWorkflows(targetDir, projectType, usesTypeScript, usesEslint, pm) {
|
|
1213
|
+
async function generateGitHubWorkflows(targetDir, projectType, usesTypeScript, usesEslint, pm, testFramework) {
|
|
1057
1214
|
const result = {
|
|
1058
1215
|
created: [],
|
|
1059
1216
|
modified: [],
|
|
@@ -1065,7 +1222,13 @@ async function generateGitHubWorkflows(targetDir, projectType, usesTypeScript, u
|
|
|
1065
1222
|
const prChecksPath = join9(workflowsDir, "pr-checks.yml");
|
|
1066
1223
|
const writeResult = await writeFileSafe(
|
|
1067
1224
|
prChecksPath,
|
|
1068
|
-
getPRChecksWorkflow(
|
|
1225
|
+
getPRChecksWorkflow(
|
|
1226
|
+
projectType,
|
|
1227
|
+
usesTypeScript,
|
|
1228
|
+
usesEslint,
|
|
1229
|
+
pm,
|
|
1230
|
+
testFramework
|
|
1231
|
+
),
|
|
1069
1232
|
{ backup: true }
|
|
1070
1233
|
);
|
|
1071
1234
|
if (writeResult.created) {
|
|
@@ -1444,14 +1607,53 @@ Includes user profile sync and token refresh.
|
|
|
1444
1607
|
Closes #123
|
|
1445
1608
|
\`\`\`
|
|
1446
1609
|
${asanaSection}
|
|
1610
|
+
## Test-Driven Development (TDD)
|
|
1611
|
+
|
|
1612
|
+
We enforce TDD to ensure code quality and maintainability. **Write tests before implementation.**
|
|
1613
|
+
|
|
1614
|
+
### TDD Workflow
|
|
1615
|
+
|
|
1616
|
+
1. **Write a failing test** - Define expected behavior
|
|
1617
|
+
2. **Write minimal code to pass** - Implement just enough to make it green
|
|
1618
|
+
3. **Refactor** - Improve code while tests stay green
|
|
1619
|
+
|
|
1620
|
+
### Running Tests
|
|
1621
|
+
|
|
1622
|
+
\`\`\`bash
|
|
1623
|
+
# Watch mode (during development)
|
|
1624
|
+
${pm.run} test:watch
|
|
1625
|
+
|
|
1626
|
+
# Run all tests
|
|
1627
|
+
${pm.run} test
|
|
1628
|
+
|
|
1629
|
+
# Check coverage
|
|
1630
|
+
${pm.run} test:coverage
|
|
1631
|
+
\`\`\`
|
|
1632
|
+
|
|
1633
|
+
### Coverage Requirements
|
|
1634
|
+
|
|
1635
|
+
- **Minimum:** 80% coverage (lines, functions, branches, statements)
|
|
1636
|
+
- **Critical paths:** Aim for 100% coverage
|
|
1637
|
+
- **New features:** Must include tests before merging
|
|
1638
|
+
|
|
1639
|
+
### Git Hooks
|
|
1640
|
+
|
|
1641
|
+
Tests run automatically:
|
|
1642
|
+
- **Pre-commit:** Related tests for changed files
|
|
1643
|
+
- **Pre-push:** Full test suite with coverage check
|
|
1644
|
+
|
|
1645
|
+
To bypass (not recommended): \`git push --no-verify\`
|
|
1646
|
+
|
|
1447
1647
|
## Pull Request Process
|
|
1448
1648
|
|
|
1449
1649
|
1. Ensure your branch is up to date with \`main\`/\`master\`
|
|
1450
|
-
2.
|
|
1451
|
-
3.
|
|
1452
|
-
4.
|
|
1453
|
-
5.
|
|
1454
|
-
6.
|
|
1650
|
+
2. **Write tests first (TDD)**
|
|
1651
|
+
3. Run tests and linting locally
|
|
1652
|
+
4. **Verify coverage** (\`${pm.run} test:coverage\`)
|
|
1653
|
+
5. Create a pull request using the provided template
|
|
1654
|
+
6. Request review from code owners
|
|
1655
|
+
7. Address any feedback
|
|
1656
|
+
8. Merge once approved and all checks pass
|
|
1455
1657
|
|
|
1456
1658
|
### PR Size Guidelines
|
|
1457
1659
|
|
|
@@ -1475,9 +1677,11 @@ If your PR is large:
|
|
|
1475
1677
|
|
|
1476
1678
|
Before committing, the following checks run automatically:
|
|
1477
1679
|
|
|
1680
|
+
- **Testing**: Related tests run on commit, full suite on push
|
|
1478
1681
|
- **Linting**: ESLint checks for code quality
|
|
1479
1682
|
- **Formatting**: Prettier ensures consistent style
|
|
1480
1683
|
- **Type checking**: TypeScript validates types
|
|
1684
|
+
- **Coverage**: 80% minimum enforced on push
|
|
1481
1685
|
- **Commit messages**: Commitlint validates format
|
|
1482
1686
|
- **Branch names**: validate-branch-name checks naming
|
|
1483
1687
|
|
|
@@ -1708,7 +1912,8 @@ import { join as join17, dirname as dirname4 } from "path";
|
|
|
1708
1912
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1709
1913
|
function getPackageConfigDir() {
|
|
1710
1914
|
const currentFilePath = fileURLToPath3(import.meta.url);
|
|
1711
|
-
const
|
|
1915
|
+
const currentDir = dirname4(currentFilePath);
|
|
1916
|
+
const packageRoot = currentDir.endsWith("dist") ? join17(currentDir, "..") : join17(currentDir, "..", "..");
|
|
1712
1917
|
return join17(packageRoot, ".claude");
|
|
1713
1918
|
}
|
|
1714
1919
|
async function copyDirectory3(srcDir, destDir, result, baseDir) {
|
|
@@ -1742,17 +1947,22 @@ async function generateClaudeConfig(targetDir) {
|
|
|
1742
1947
|
const packageConfigDir = getPackageConfigDir();
|
|
1743
1948
|
const targetClaudeDir = join17(targetDir, ".claude");
|
|
1744
1949
|
await ensureDir(targetClaudeDir);
|
|
1745
|
-
const
|
|
1746
|
-
const
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
result.backedUp.push(settingsResult.backedUp);
|
|
1950
|
+
const packageSettingsPath = join17(packageConfigDir, "settings.json");
|
|
1951
|
+
const targetSettingsPath = join17(targetClaudeDir, "settings.json");
|
|
1952
|
+
if (existsSync7(packageSettingsPath)) {
|
|
1953
|
+
if (existsSync7(targetSettingsPath)) {
|
|
1954
|
+
const backupPath = await backupFile(targetSettingsPath);
|
|
1955
|
+
if (backupPath) {
|
|
1956
|
+
const relativePath = backupPath.replace(targetDir + "/", "");
|
|
1957
|
+
result.backedUp.push(relativePath);
|
|
1958
|
+
}
|
|
1755
1959
|
}
|
|
1960
|
+
await copyFile4(packageSettingsPath, targetSettingsPath);
|
|
1961
|
+
result.created.push(".claude/settings.json");
|
|
1962
|
+
} else {
|
|
1963
|
+
console.warn(
|
|
1964
|
+
"Warning: settings.json not found in package. Skipping settings generation."
|
|
1965
|
+
);
|
|
1756
1966
|
}
|
|
1757
1967
|
const packageSubagentsDir = join17(packageConfigDir, "subagents");
|
|
1758
1968
|
const targetSubagentsDir = join17(targetClaudeDir, "subagents");
|
|
@@ -1770,7 +1980,7 @@ async function generateClaudeConfig(targetDir) {
|
|
|
1770
1980
|
import { existsSync as existsSync8 } from "fs";
|
|
1771
1981
|
import { readFile as readFile4 } from "fs/promises";
|
|
1772
1982
|
import { join as join18 } from "path";
|
|
1773
|
-
async function
|
|
1983
|
+
async function hasReact2(targetDir) {
|
|
1774
1984
|
try {
|
|
1775
1985
|
const pkgPath = join18(targetDir, "package.json");
|
|
1776
1986
|
if (existsSync8(pkgPath)) {
|
|
@@ -2035,7 +2245,7 @@ export default [
|
|
|
2035
2245
|
`;
|
|
2036
2246
|
}
|
|
2037
2247
|
async function detectReact(targetDir) {
|
|
2038
|
-
return
|
|
2248
|
+
return hasReact2(targetDir);
|
|
2039
2249
|
}
|
|
2040
2250
|
async function detectNextJs(targetDir) {
|
|
2041
2251
|
return hasNextJs(targetDir);
|
|
@@ -2052,7 +2262,7 @@ async function generateEslint(targetDir, usesTypeScript, force = false) {
|
|
|
2052
2262
|
return result;
|
|
2053
2263
|
}
|
|
2054
2264
|
const usesNextJs = await hasNextJs(targetDir);
|
|
2055
|
-
const usesReact = await
|
|
2265
|
+
const usesReact = await hasReact2(targetDir);
|
|
2056
2266
|
let config;
|
|
2057
2267
|
if (usesNextJs && usesTypeScript) {
|
|
2058
2268
|
config = generateNextJsConfig();
|
|
@@ -2136,6 +2346,41 @@ git push -u origin feature/my-feature
|
|
|
2136
2346
|
|
|
2137
2347
|
---
|
|
2138
2348
|
|
|
2349
|
+
## \u{1F9EA} Test-Driven Development (TDD)
|
|
2350
|
+
|
|
2351
|
+
### TDD Workflow
|
|
2352
|
+
|
|
2353
|
+
1. **Write the test first** (red phase)
|
|
2354
|
+
2. **Write minimal code to pass** (green phase)
|
|
2355
|
+
3. **Refactor** with confidence (tests stay green)
|
|
2356
|
+
|
|
2357
|
+
### Test Commands
|
|
2358
|
+
|
|
2359
|
+
\`\`\`bash
|
|
2360
|
+
# Run tests in watch mode (during development)
|
|
2361
|
+
${pm.run} test:watch
|
|
2362
|
+
|
|
2363
|
+
# Run all tests once
|
|
2364
|
+
${pm.run} test
|
|
2365
|
+
|
|
2366
|
+
# Run tests with coverage report
|
|
2367
|
+
${pm.run} test:coverage
|
|
2368
|
+
\`\`\`
|
|
2369
|
+
|
|
2370
|
+
### Git Hooks with Testing
|
|
2371
|
+
|
|
2372
|
+
- **Pre-commit:** Runs related tests for changed files
|
|
2373
|
+
- **Pre-push:** Runs full test suite with 80% coverage requirement
|
|
2374
|
+
- **Bypass (not recommended):** \`git push --no-verify\`
|
|
2375
|
+
|
|
2376
|
+
### Coverage Requirements
|
|
2377
|
+
|
|
2378
|
+
- **Minimum:** 80% coverage for lines, functions, branches, statements
|
|
2379
|
+
- **Critical paths:** Aim for 100%
|
|
2380
|
+
- **New features:** Must include tests before merging
|
|
2381
|
+
|
|
2382
|
+
---
|
|
2383
|
+
|
|
2139
2384
|
## \u{1F4DD} Commit Types
|
|
2140
2385
|
|
|
2141
2386
|
| Type | Emoji | Use For |
|
|
@@ -2158,7 +2403,9 @@ git push -u origin feature/my-feature
|
|
|
2158
2403
|
Before submitting:
|
|
2159
2404
|
- [ ] Branch name follows convention
|
|
2160
2405
|
- [ ] All commits have task links
|
|
2161
|
-
- [ ] Tests
|
|
2406
|
+
- [ ] **Tests written before implementation (TDD)**
|
|
2407
|
+
- [ ] **All tests pass** (\`${pm.run} test\`)
|
|
2408
|
+
- [ ] **Coverage \u2265 80%** (\`${pm.run} test:coverage\`)
|
|
2162
2409
|
- [ ] PR description filled out
|
|
2163
2410
|
- [ ] Size is reasonable (< 400 lines)
|
|
2164
2411
|
|
|
@@ -2178,6 +2425,12 @@ ${pm.run} lint
|
|
|
2178
2425
|
|
|
2179
2426
|
# Run tests
|
|
2180
2427
|
${pm.run} test
|
|
2428
|
+
|
|
2429
|
+
# Run tests with coverage
|
|
2430
|
+
${pm.run} test:coverage
|
|
2431
|
+
|
|
2432
|
+
# Run tests in watch mode
|
|
2433
|
+
${pm.run} test:watch
|
|
2181
2434
|
\`\`\`
|
|
2182
2435
|
|
|
2183
2436
|
---
|
|
@@ -2523,12 +2776,200 @@ async function generateSharedConfigs(targetDir, projectType) {
|
|
|
2523
2776
|
return result;
|
|
2524
2777
|
}
|
|
2525
2778
|
|
|
2779
|
+
// src/generators/testing.ts
|
|
2780
|
+
import { join as join21 } from "path";
|
|
2781
|
+
var TEST_DEPENDENCIES = {
|
|
2782
|
+
vitest: [
|
|
2783
|
+
"vitest",
|
|
2784
|
+
// ^2.1.0
|
|
2785
|
+
"@vitest/coverage-v8"
|
|
2786
|
+
// ^2.1.0
|
|
2787
|
+
],
|
|
2788
|
+
jest: [
|
|
2789
|
+
"jest",
|
|
2790
|
+
// ^29.7.0
|
|
2791
|
+
"@types/jest",
|
|
2792
|
+
// ^29.5.0
|
|
2793
|
+
"ts-jest"
|
|
2794
|
+
// ^29.2.0
|
|
2795
|
+
]
|
|
2796
|
+
};
|
|
2797
|
+
var REACT_TEST_PACKAGES = [
|
|
2798
|
+
"@testing-library/react",
|
|
2799
|
+
// ^16.1.0
|
|
2800
|
+
"@testing-library/jest-dom",
|
|
2801
|
+
// ^6.6.0
|
|
2802
|
+
"@testing-library/user-event"
|
|
2803
|
+
// ^14.6.0
|
|
2804
|
+
];
|
|
2805
|
+
function getVitestConfig(projectType) {
|
|
2806
|
+
if (projectType !== "single") {
|
|
2807
|
+
return `import { defineConfig } from "vitest/config";
|
|
2808
|
+
|
|
2809
|
+
export default defineConfig({
|
|
2810
|
+
test: {
|
|
2811
|
+
globals: true,
|
|
2812
|
+
environment: "node",
|
|
2813
|
+
coverage: {
|
|
2814
|
+
provider: "v8",
|
|
2815
|
+
reporter: ["text", "json", "html", "lcov"],
|
|
2816
|
+
thresholds: {
|
|
2817
|
+
lines: 80,
|
|
2818
|
+
functions: 80,
|
|
2819
|
+
branches: 80,
|
|
2820
|
+
statements: 80,
|
|
2821
|
+
},
|
|
2822
|
+
},
|
|
2823
|
+
},
|
|
2824
|
+
});
|
|
2825
|
+
`;
|
|
2826
|
+
}
|
|
2827
|
+
return `import { defineConfig } from "vitest/config";
|
|
2828
|
+
|
|
2829
|
+
export default defineConfig({
|
|
2830
|
+
test: {
|
|
2831
|
+
globals: true,
|
|
2832
|
+
environment: "node",
|
|
2833
|
+
coverage: {
|
|
2834
|
+
provider: "v8",
|
|
2835
|
+
reporter: ["text", "json", "html", "lcov"],
|
|
2836
|
+
thresholds: {
|
|
2837
|
+
lines: 80,
|
|
2838
|
+
functions: 80,
|
|
2839
|
+
branches: 80,
|
|
2840
|
+
statements: 80,
|
|
2841
|
+
},
|
|
2842
|
+
exclude: [
|
|
2843
|
+
"node_modules/**",
|
|
2844
|
+
"dist/**",
|
|
2845
|
+
"build/**",
|
|
2846
|
+
"**/*.config.{js,ts,mjs,cjs}",
|
|
2847
|
+
"**/*.test.{js,ts,jsx,tsx}",
|
|
2848
|
+
"**/__tests__/**",
|
|
2849
|
+
],
|
|
2850
|
+
},
|
|
2851
|
+
},
|
|
2852
|
+
});
|
|
2853
|
+
`;
|
|
2854
|
+
}
|
|
2855
|
+
function getJestConfig(projectType) {
|
|
2856
|
+
const isMonorepo2 = projectType !== "single";
|
|
2857
|
+
return `/** @type {import('jest').Config} */
|
|
2858
|
+
const config = {
|
|
2859
|
+
preset: "ts-jest",
|
|
2860
|
+
testEnvironment: "node",
|
|
2861
|
+
${isMonorepo2 ? "" : 'roots: ["<rootDir>/src"],\n '}testMatch: ["**/__tests__/**/*.test.ts", "**/*.test.ts"],
|
|
2862
|
+
collectCoverageFrom: [
|
|
2863
|
+
"src/**/*.{ts,tsx}",
|
|
2864
|
+
"!src/**/*.d.ts",
|
|
2865
|
+
"!src/**/*.config.{ts,js}",
|
|
2866
|
+
],
|
|
2867
|
+
coverageThresholds: {
|
|
2868
|
+
global: {
|
|
2869
|
+
lines: 80,
|
|
2870
|
+
functions: 80,
|
|
2871
|
+
branches: 80,
|
|
2872
|
+
statements: 80,
|
|
2873
|
+
},
|
|
2874
|
+
},
|
|
2875
|
+
coverageReporters: ["text", "json", "html", "lcov"],
|
|
2876
|
+
};
|
|
2877
|
+
|
|
2878
|
+
module.exports = config;
|
|
2879
|
+
`;
|
|
2880
|
+
}
|
|
2881
|
+
function getTestSetupFile(framework) {
|
|
2882
|
+
if (framework === "vitest") {
|
|
2883
|
+
return `import "@testing-library/jest-dom/vitest";
|
|
2884
|
+
`;
|
|
2885
|
+
}
|
|
2886
|
+
return `import "@testing-library/jest-dom";
|
|
2887
|
+
`;
|
|
2888
|
+
}
|
|
2889
|
+
function getTestScripts(framework) {
|
|
2890
|
+
if (framework === "vitest") {
|
|
2891
|
+
return {
|
|
2892
|
+
test: "vitest run",
|
|
2893
|
+
"test:watch": "vitest",
|
|
2894
|
+
"test:coverage": "vitest run --coverage",
|
|
2895
|
+
"test:ui": "vitest --ui"
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
return {
|
|
2899
|
+
test: "jest",
|
|
2900
|
+
"test:watch": "jest --watch",
|
|
2901
|
+
"test:coverage": "jest --coverage"
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
async function generateTestingSetup(targetDir, framework, projectType) {
|
|
2905
|
+
const result = {
|
|
2906
|
+
created: [],
|
|
2907
|
+
modified: [],
|
|
2908
|
+
skipped: [],
|
|
2909
|
+
backedUp: []
|
|
2910
|
+
};
|
|
2911
|
+
const configFileName = framework === "vitest" ? "vitest.config.ts" : "jest.config.js";
|
|
2912
|
+
const configPath = join21(targetDir, configFileName);
|
|
2913
|
+
const configContent = framework === "vitest" ? getVitestConfig(projectType) : getJestConfig(projectType);
|
|
2914
|
+
const configResult = await writeFileSafe(configPath, configContent, {
|
|
2915
|
+
backup: true
|
|
2916
|
+
});
|
|
2917
|
+
if (configResult.created) {
|
|
2918
|
+
result.created.push(configPath);
|
|
2919
|
+
} else if (configResult.modified) {
|
|
2920
|
+
result.modified.push(configPath);
|
|
2921
|
+
if (configResult.backedUp) result.backedUp.push(configPath);
|
|
2922
|
+
} else {
|
|
2923
|
+
result.skipped.push(configPath);
|
|
2924
|
+
}
|
|
2925
|
+
const isReactProject = await hasReact(targetDir);
|
|
2926
|
+
if (isReactProject) {
|
|
2927
|
+
const setupFileName = framework === "vitest" ? "vitest.setup.ts" : "jest.setup.ts";
|
|
2928
|
+
const setupPath = join21(targetDir, setupFileName);
|
|
2929
|
+
const setupContent = getTestSetupFile(framework);
|
|
2930
|
+
const setupResult = await writeFileSafe(setupPath, setupContent, {
|
|
2931
|
+
backup: true
|
|
2932
|
+
});
|
|
2933
|
+
if (setupResult.created) {
|
|
2934
|
+
result.created.push(setupPath);
|
|
2935
|
+
} else if (setupResult.modified) {
|
|
2936
|
+
result.modified.push(setupPath);
|
|
2937
|
+
if (setupResult.backedUp) result.backedUp.push(setupPath);
|
|
2938
|
+
} else {
|
|
2939
|
+
result.skipped.push(setupPath);
|
|
2940
|
+
}
|
|
2941
|
+
if (framework === "vitest") {
|
|
2942
|
+
const updatedConfig = getVitestConfig(projectType).replace(
|
|
2943
|
+
'environment: "node",',
|
|
2944
|
+
`environment: "jsdom",
|
|
2945
|
+
setupFiles: ["./vitest.setup.ts"],`
|
|
2946
|
+
);
|
|
2947
|
+
await writeFileSafe(configPath, updatedConfig, { backup: true });
|
|
2948
|
+
} else {
|
|
2949
|
+
const updatedConfig = getJestConfig(projectType).replace(
|
|
2950
|
+
'testEnvironment: "node",',
|
|
2951
|
+
`testEnvironment: "jsdom",
|
|
2952
|
+
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],`
|
|
2953
|
+
);
|
|
2954
|
+
await writeFileSafe(configPath, updatedConfig, { backup: true });
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
return result;
|
|
2958
|
+
}
|
|
2959
|
+
function getTestDependencies(framework, includeReact) {
|
|
2960
|
+
const baseDeps = TEST_DEPENDENCIES[framework];
|
|
2961
|
+
if (includeReact) {
|
|
2962
|
+
return [...baseDeps, ...REACT_TEST_PACKAGES];
|
|
2963
|
+
}
|
|
2964
|
+
return baseDeps;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2526
2967
|
// src/utils/git.ts
|
|
2527
2968
|
import { execa } from "execa";
|
|
2528
2969
|
import { existsSync as existsSync9 } from "fs";
|
|
2529
|
-
import { join as
|
|
2970
|
+
import { join as join22 } from "path";
|
|
2530
2971
|
async function isGitRepo(targetDir = process.cwd()) {
|
|
2531
|
-
if (existsSync9(
|
|
2972
|
+
if (existsSync9(join22(targetDir, ".git"))) {
|
|
2532
2973
|
return true;
|
|
2533
2974
|
}
|
|
2534
2975
|
try {
|
|
@@ -2595,13 +3036,18 @@ async function updateProjectPackageJson(targetDir, config) {
|
|
|
2595
3036
|
if (config.usesTypeScript) {
|
|
2596
3037
|
scripts.typecheck = "tsc --noEmit";
|
|
2597
3038
|
}
|
|
3039
|
+
if (config.testFramework) {
|
|
3040
|
+
const testScripts = getTestScripts(config.testFramework);
|
|
3041
|
+
Object.assign(scripts, testScripts);
|
|
3042
|
+
}
|
|
2598
3043
|
pkg = mergeScripts(pkg, scripts, false);
|
|
2599
3044
|
const lintStagedConfig = getLintStagedConfig(
|
|
2600
3045
|
true,
|
|
2601
3046
|
// usesEslint - always true now since we install it
|
|
2602
3047
|
true,
|
|
2603
3048
|
// usesPrettier - always true now since we install it
|
|
2604
|
-
config.usesTypeScript
|
|
3049
|
+
config.usesTypeScript,
|
|
3050
|
+
config.testFramework
|
|
2605
3051
|
);
|
|
2606
3052
|
pkg = addPackageJsonConfig(pkg, "lint-staged", lintStagedConfig, true);
|
|
2607
3053
|
await writePackageJson(pkg, targetDir);
|
|
@@ -2641,6 +3087,10 @@ async function runInit(targetDir = process.cwd()) {
|
|
|
2641
3087
|
} else if (usesReact) {
|
|
2642
3088
|
packagesToInstall = [...packagesToInstall, ...REACT_ESLINT_PACKAGES];
|
|
2643
3089
|
}
|
|
3090
|
+
if (config.testFramework) {
|
|
3091
|
+
const testDeps = getTestDependencies(config.testFramework, usesReact);
|
|
3092
|
+
packagesToInstall = [...packagesToInstall, ...testDeps];
|
|
3093
|
+
}
|
|
2644
3094
|
installSpinner.start("Installing dependencies...");
|
|
2645
3095
|
const installResult = await installPackages(
|
|
2646
3096
|
config.packageManager,
|
|
@@ -2669,13 +3119,23 @@ async function runInit(targetDir = process.cwd()) {
|
|
|
2669
3119
|
const results = [];
|
|
2670
3120
|
try {
|
|
2671
3121
|
results.push(
|
|
2672
|
-
await generateHuskyHooks(
|
|
3122
|
+
await generateHuskyHooks(
|
|
3123
|
+
targetDir,
|
|
3124
|
+
config.projectType,
|
|
3125
|
+
config.packageManager,
|
|
3126
|
+
config.testFramework
|
|
3127
|
+
)
|
|
2673
3128
|
);
|
|
2674
3129
|
results.push(await generateCommitlint(targetDir, config.asanaBaseUrl));
|
|
2675
3130
|
results.push(await generateCzGit(targetDir, config.asanaBaseUrl));
|
|
2676
3131
|
results.push(await generateBranchValidation(targetDir));
|
|
2677
3132
|
results.push(await generateEslint(targetDir, config.usesTypeScript, false));
|
|
2678
3133
|
results.push(await generatePrettier(targetDir));
|
|
3134
|
+
if (config.testFramework) {
|
|
3135
|
+
results.push(
|
|
3136
|
+
await generateTestingSetup(targetDir, config.testFramework, config.projectType)
|
|
3137
|
+
);
|
|
3138
|
+
}
|
|
2679
3139
|
if (isMonorepo(config.projectType)) {
|
|
2680
3140
|
results.push(await generateSharedConfigs(targetDir, config.projectType));
|
|
2681
3141
|
}
|
|
@@ -2687,7 +3147,8 @@ async function runInit(targetDir = process.cwd()) {
|
|
|
2687
3147
|
config.usesTypeScript,
|
|
2688
3148
|
true,
|
|
2689
3149
|
// usesEslint - always true now
|
|
2690
|
-
config.packageManager
|
|
3150
|
+
config.packageManager,
|
|
3151
|
+
config.testFramework
|
|
2691
3152
|
)
|
|
2692
3153
|
);
|
|
2693
3154
|
results.push(await generateCodeowners(targetDir, config.codeowners));
|
|
@@ -3699,7 +4160,7 @@ async function runInstallCommands(targetDir = process.cwd()) {
|
|
|
3699
4160
|
// package.json
|
|
3700
4161
|
var package_default = {
|
|
3701
4162
|
name: "@raftlabs/raftstack",
|
|
3702
|
-
version: "1.
|
|
4163
|
+
version: "1.10.0",
|
|
3703
4164
|
description: "CLI tool for setting up Git hooks, commit conventions, and GitHub integration",
|
|
3704
4165
|
type: "module",
|
|
3705
4166
|
main: "./dist/index.js",
|