@testspectra/cli 1.0.21 → 1.0.23
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/commands/init.js +138 -65
- package/package.json +2 -3
package/dist/commands/init.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import chalk from "chalk";
|
|
4
6
|
import { ConfigLoader } from "../config/loader.js";
|
|
5
7
|
import { TypeGenerator } from "../types/generator.js";
|
|
6
|
-
import inquirer from "inquirer";
|
|
7
8
|
function parsePnpmWorkspaceGlobs(content) {
|
|
8
9
|
const lines = content.split("\n");
|
|
9
10
|
const globs = [];
|
|
@@ -27,6 +28,54 @@ function parsePnpmWorkspaceGlobs(content) {
|
|
|
27
28
|
}
|
|
28
29
|
return globs;
|
|
29
30
|
}
|
|
31
|
+
function ensurePnpmWorkspaceIncludes(pnpmWorkspacePath, targetPath) {
|
|
32
|
+
const normalizedTarget = targetPath.replace(/\\/g, "/");
|
|
33
|
+
if (!fs.existsSync(pnpmWorkspacePath)) {
|
|
34
|
+
const newContent = `packages:\n - '${normalizedTarget}'\n`;
|
|
35
|
+
fs.writeFileSync(pnpmWorkspacePath, newContent, "utf-8");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const content = fs.readFileSync(pnpmWorkspacePath, "utf-8");
|
|
39
|
+
const currentGlobs = parsePnpmWorkspaceGlobs(content);
|
|
40
|
+
// Check if target is already covered by an existing glob or exact path
|
|
41
|
+
const isAlreadyCovered = currentGlobs.some((glob) => {
|
|
42
|
+
const normGlob = glob.replace(/\\/g, "/");
|
|
43
|
+
if (normGlob === normalizedTarget)
|
|
44
|
+
return true;
|
|
45
|
+
if (normGlob === `${normalizedTarget}/*` || normGlob === `${normalizedTarget}/**`)
|
|
46
|
+
return true;
|
|
47
|
+
if (normGlob.endsWith("/*")) {
|
|
48
|
+
const parent = normGlob.slice(0, -2);
|
|
49
|
+
if (normalizedTarget.startsWith(`${parent}/`) && normalizedTarget.split("/").length === parent.split("/").length + 1)
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (normGlob.endsWith("/**")) {
|
|
53
|
+
const parent = normGlob.slice(0, -3);
|
|
54
|
+
if (normalizedTarget.startsWith(`${parent}/`))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
});
|
|
59
|
+
if (!isAlreadyCovered) {
|
|
60
|
+
// Insert new package pattern right under packages:
|
|
61
|
+
const lines = content.split("\n");
|
|
62
|
+
let insertIndex = -1;
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
if (lines[i].trim().startsWith("packages:")) {
|
|
65
|
+
insertIndex = i + 1;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (insertIndex !== -1) {
|
|
70
|
+
lines.splice(insertIndex, 0, ` - '${normalizedTarget}'`);
|
|
71
|
+
fs.writeFileSync(pnpmWorkspacePath, lines.join("\n"), "utf-8");
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const newContent = `packages:\n - '${normalizedTarget}'\n` + content;
|
|
75
|
+
fs.writeFileSync(pnpmWorkspacePath, newContent, "utf-8");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
30
79
|
function scanDirectoriesForProjects(cwd, patterns) {
|
|
31
80
|
const foundProjects = [];
|
|
32
81
|
const visited = new Set();
|
|
@@ -112,9 +161,10 @@ export async function initCommand(options = {}) {
|
|
|
112
161
|
const cwd = process.cwd();
|
|
113
162
|
const existingConfig = ConfigLoader.findConfigFile(cwd);
|
|
114
163
|
if (existingConfig && existingConfig === path.join(cwd, path.basename(existingConfig)) && !options.force) {
|
|
115
|
-
|
|
164
|
+
p.log.warn(chalk.yellow(`Config already exists at ${path.basename(existingConfig)}. Use --force to overwrite.`));
|
|
116
165
|
return;
|
|
117
166
|
}
|
|
167
|
+
p.intro(chalk.bold.cyan("✨ TestSpectra Enterprise Scaffolder"));
|
|
118
168
|
// Detect monorepo environment
|
|
119
169
|
let isMonorepo = false;
|
|
120
170
|
let pnpmPatterns = [];
|
|
@@ -134,20 +184,27 @@ export async function initCommand(options = {}) {
|
|
|
134
184
|
const detectedProjects = isMonorepo ? scanDirectoriesForProjects(cwd, pnpmPatterns) : [];
|
|
135
185
|
let selectedTemplate = options.template;
|
|
136
186
|
if (!selectedTemplate && !options.force) {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
187
|
+
const templateChoice = await p.select({
|
|
188
|
+
message: "Select TestSpectra setup mode:",
|
|
189
|
+
initialValue: isMonorepo ? "nx" : "default",
|
|
190
|
+
options: [
|
|
191
|
+
{
|
|
192
|
+
label: "Nx Monorepo (Recommended)",
|
|
193
|
+
value: "nx",
|
|
194
|
+
hint: "Interactive workspace feature discovery & centralized config",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
label: "Standard Project",
|
|
198
|
+
value: "default",
|
|
199
|
+
hint: "Standalone single-suite E2E testing project",
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
if (p.isCancel(templateChoice)) {
|
|
204
|
+
p.cancel("Setup cancelled.");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
selectedTemplate = templateChoice;
|
|
151
208
|
}
|
|
152
209
|
const templateName = selectedTemplate || (isMonorepo ? "nx" : "default");
|
|
153
210
|
// 1. Resolve template directory
|
|
@@ -161,10 +218,11 @@ export async function initCommand(options = {}) {
|
|
|
161
218
|
templateDir = path.resolve(__dirname, `../../../templates/${templateName}`);
|
|
162
219
|
}
|
|
163
220
|
if (!fs.existsSync(templateDir)) {
|
|
164
|
-
|
|
221
|
+
p.cancel(chalk.red(`Template directory not found: ${templateDir}`));
|
|
222
|
+
return;
|
|
165
223
|
}
|
|
166
224
|
// 2. Resolve CLI dependency version
|
|
167
|
-
let cliVersion = "^1.0.
|
|
225
|
+
let cliVersion = "^1.0.21";
|
|
168
226
|
try {
|
|
169
227
|
const cliPackageJsonPath = path.resolve(__dirname, "../../package.json");
|
|
170
228
|
if (fs.existsSync(cliPackageJsonPath)) {
|
|
@@ -234,47 +292,53 @@ export async function initCommand(options = {}) {
|
|
|
234
292
|
}
|
|
235
293
|
// --- MONOREPO INTERACTIVE FLOW ---
|
|
236
294
|
if (templateName === "nx" && (detectedProjects.length > 0 || isMonorepo)) {
|
|
237
|
-
console.log(`\n\x1b[36m[TestSpectra Monorepo Discovery]\x1b[0m`);
|
|
238
|
-
console.log(`Found ${detectedProjects.length} workspace feature project(s).`);
|
|
239
295
|
let chosenProjects = [];
|
|
240
296
|
if (detectedProjects.length > 0) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
297
|
+
p.log.step(chalk.cyan(`Monorepo Auto-Discovery: Found ${detectedProjects.length} workspace feature package(s)`));
|
|
298
|
+
const projectChoices = await p.multiselect({
|
|
299
|
+
message: "Select workspace features to initialize with TestSpectra:",
|
|
300
|
+
options: detectedProjects.map((proj) => ({
|
|
301
|
+
value: proj.relPath,
|
|
302
|
+
label: `${proj.relPath} (${proj.name})`,
|
|
303
|
+
hint: proj.hasNxProject ? "Nx project.json" : "package.json",
|
|
304
|
+
})),
|
|
305
|
+
initialValues: detectedProjects.map((p) => p.relPath),
|
|
306
|
+
required: true,
|
|
307
|
+
});
|
|
308
|
+
if (p.isCancel(projectChoices)) {
|
|
309
|
+
p.cancel("Setup cancelled.");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
chosenProjects = projectChoices;
|
|
313
|
+
}
|
|
314
|
+
const e2eFolderNameInput = await p.text({
|
|
315
|
+
message: "Enter E2E test folder name for each selected feature:",
|
|
316
|
+
placeholder: "e2e",
|
|
317
|
+
defaultValue: "e2e",
|
|
318
|
+
});
|
|
319
|
+
if (p.isCancel(e2eFolderNameInput)) {
|
|
320
|
+
p.cancel("Setup cancelled.");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const sharedTestingPathInput = await p.text({
|
|
324
|
+
message: "Enter path for Shared Testing library (POMs, Steps, Actions, Fixtures):",
|
|
325
|
+
placeholder: "shared/testing",
|
|
326
|
+
defaultValue: "shared/testing",
|
|
327
|
+
});
|
|
328
|
+
if (p.isCancel(sharedTestingPathInput)) {
|
|
329
|
+
p.cancel("Setup cancelled.");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const e2eFolderName = e2eFolderNameInput.trim() || "e2e";
|
|
333
|
+
const sharedTestingRelPath = sharedTestingPathInput.trim() || "shared/testing";
|
|
271
334
|
const sharedTestingAbsPath = path.join(cwd, sharedTestingRelPath);
|
|
335
|
+
const s = p.spinner();
|
|
336
|
+
s.start("Scaffolding distributed monorepo structure and ambient types...");
|
|
272
337
|
// 1. Centralized Root Config
|
|
273
338
|
const rootConfigSrc = path.join(templateDir, "spectra.config.ts");
|
|
274
339
|
const rootConfigDest = path.join(cwd, "spectra.config.ts");
|
|
275
340
|
if (fs.existsSync(rootConfigSrc) && (!fs.existsSync(rootConfigDest) || options.force)) {
|
|
276
341
|
fs.copyFileSync(rootConfigSrc, rootConfigDest);
|
|
277
|
-
console.log(`\x1b[32m[TestSpectra]\x1b[0m Created centralized configuration at ./spectra.config.ts`);
|
|
278
342
|
}
|
|
279
343
|
// 2. Centralized Root .testspectra folder for cache/logs
|
|
280
344
|
const rootTestDataDir = path.join(cwd, ".testspectra");
|
|
@@ -285,7 +349,6 @@ export async function initCommand(options = {}) {
|
|
|
285
349
|
const sharedTestingSrc = path.join(templateDir, "shared/testing");
|
|
286
350
|
if (fs.existsSync(sharedTestingSrc)) {
|
|
287
351
|
copyRecursive(sharedTestingSrc, sharedTestingAbsPath);
|
|
288
|
-
console.log(`\x1b[32m[TestSpectra]\x1b[0m Initialized shared testing library at ./${sharedTestingRelPath}`);
|
|
289
352
|
}
|
|
290
353
|
// 4. Distribute E2E Folders to Selected Feature Modules
|
|
291
354
|
const sampleE2eSrc = path.join(templateDir, "modules/auth/e2e");
|
|
@@ -318,9 +381,14 @@ export async function initCommand(options = {}) {
|
|
|
318
381
|
if (fs.existsSync(localConfig))
|
|
319
382
|
fs.unlinkSync(localConfig);
|
|
320
383
|
featureE2eDirs.push(targetE2eDir);
|
|
321
|
-
console.log(`\x1b[32m[TestSpectra]\x1b[0m Initialized ${e2eProjectName} in ./${path.relative(cwd, targetE2eDir)}`);
|
|
322
384
|
}
|
|
323
|
-
// 5.
|
|
385
|
+
// 5. Ensure pnpm-workspace.yaml includes shared library and all feature E2E directories
|
|
386
|
+
const pnpmWorkspaceFilePath = path.join(cwd, "pnpm-workspace.yaml");
|
|
387
|
+
ensurePnpmWorkspaceIncludes(pnpmWorkspaceFilePath, sharedTestingRelPath);
|
|
388
|
+
for (const e2eDir of featureE2eDirs) {
|
|
389
|
+
ensurePnpmWorkspaceIncludes(pnpmWorkspaceFilePath, path.relative(cwd, e2eDir));
|
|
390
|
+
}
|
|
391
|
+
// 6. Update Root Solution tsconfig.json references
|
|
324
392
|
const rootTsConfigPath = path.join(cwd, "tsconfig.json");
|
|
325
393
|
const references = [];
|
|
326
394
|
for (const e2eDir of featureE2eDirs) {
|
|
@@ -344,12 +412,13 @@ export async function initCommand(options = {}) {
|
|
|
344
412
|
else {
|
|
345
413
|
rootTsConfig = { files: [], references };
|
|
346
414
|
}
|
|
347
|
-
|
|
415
|
+
fs.writeFileSync(rootTsConfigPath, JSON.stringify(rootTsConfig, null, 2), "utf-8");
|
|
416
|
+
// 7. Generate Ambient Types for Shared + All E2E Feature Directories
|
|
348
417
|
TypeGenerator.writeDeclarationFiles(cwd);
|
|
349
418
|
for (const e2eDir of featureE2eDirs) {
|
|
350
419
|
TypeGenerator.writeDeclarationFiles(e2eDir);
|
|
351
420
|
}
|
|
352
|
-
//
|
|
421
|
+
// 8. Generate ARCHITECTURE.md in root documenting the exact generated layout
|
|
353
422
|
const archDocContent = `# TestSpectra Enterprise Monorepo Architecture
|
|
354
423
|
|
|
355
424
|
This workspace uses TestSpectra's **"Centralized Configuration, Distributed Implementation"** model for automated cross-platform testing.
|
|
@@ -431,15 +500,18 @@ pnpm type-check
|
|
|
431
500
|
const readmeContent = `# ${projectName}\n\nAutomated cross-platform E2E testing powered by [TestSpectra](https://github.com/testspectra).\n${archSection}`;
|
|
432
501
|
fs.writeFileSync(rootReadmePath, readmeContent, "utf-8");
|
|
433
502
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
503
|
+
s.stop("Monorepo scaffolding complete!");
|
|
504
|
+
p.log.success(chalk.green("Centralized Configuration: ./spectra.config.ts"));
|
|
505
|
+
p.log.success(chalk.green("Centralized App Data/Cache: ./.testspectra/"));
|
|
506
|
+
p.log.success(chalk.green(`Shared Testing Library: ./${sharedTestingRelPath}`));
|
|
507
|
+
p.log.success(chalk.green("Architecture Documentation: ./ARCHITECTURE.md"));
|
|
508
|
+
p.log.message(chalk.cyan(`Initialized Features (${featureE2eDirs.length}):\n${featureE2eDirs.map((d) => ` - ./${path.relative(cwd, d)}`).join("\n")}`));
|
|
509
|
+
p.outro(chalk.bold.green("🎉 Enterprise Nx Monorepo ready for testing!"));
|
|
440
510
|
return;
|
|
441
511
|
}
|
|
442
512
|
// --- STANDALONE / DEFAULT PROJECT FLOW ---
|
|
513
|
+
const s = p.spinner();
|
|
514
|
+
s.start("Scaffolding standalone TestSpectra project...");
|
|
443
515
|
copyRecursive(templateDir, cwd);
|
|
444
516
|
if (!isInternalWorkspace) {
|
|
445
517
|
const pnpmWorkspacePath = path.join(cwd, "pnpm-workspace.yaml");
|
|
@@ -491,7 +563,8 @@ pnpm type-check
|
|
|
491
563
|
const standaloneReadme = `# ${projectName}\n\nTestSpectra automated testing project.\n${standaloneArchSection}`;
|
|
492
564
|
fs.writeFileSync(standaloneReadmePath, standaloneReadme, "utf-8");
|
|
493
565
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
566
|
+
s.stop("Standalone project ready!");
|
|
567
|
+
p.log.success(chalk.green("Generated ambient multi-platform types in .testspectra/types/"));
|
|
568
|
+
p.log.success(chalk.green("Architecture documentation created at ./ARCHITECTURE.md"));
|
|
569
|
+
p.outro(chalk.bold.green("🎉 Project initialized successfully!"));
|
|
497
570
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testspectra/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,16 +20,15 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@clack/prompts": "^1.7.0",
|
|
23
24
|
"@testspectra/matchers": "^1.0.0",
|
|
24
25
|
"chalk": "^5.3.0",
|
|
25
26
|
"commander": "^12.1.0",
|
|
26
27
|
"dotenv": "^16.4.5",
|
|
27
|
-
"inquirer": "^9.3.2",
|
|
28
28
|
"ora": "^8.0.1",
|
|
29
29
|
"zod": "^3.23.8"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@types/inquirer": "^9.0.7",
|
|
33
32
|
"@types/node": "^20.14.0",
|
|
34
33
|
"typescript": "^5.4.5"
|
|
35
34
|
},
|