@toolstackhq/create-qa-patterns 1.0.14 → 1.0.15
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 +5 -0
- package/index.js +252 -1076
- package/lib/args.js +139 -0
- package/lib/constants.js +115 -0
- package/lib/interactive.js +131 -0
- package/lib/local-env.js +65 -0
- package/lib/metadata.js +329 -0
- package/lib/output.js +326 -0
- package/lib/prereqs.js +72 -0
- package/lib/scaffold.js +120 -0
- package/lib/templates.js +40 -0
- package/package.json +5 -3
- package/templates/cypress-template/.env.example +2 -2
- package/templates/cypress-template/.github/workflows/cypress-tests.yml +2 -2
- package/templates/cypress-template/README.md +10 -6
- package/templates/cypress-template/allurerc.mjs +1 -1
- package/templates/cypress-template/config/environments.ts +13 -11
- package/templates/cypress-template/config/runtime-config.ts +17 -12
- package/templates/cypress-template/config/secret-manager.ts +1 -1
- package/templates/cypress-template/config/test-env.ts +3 -3
- package/templates/cypress-template/cypress/e2e/ui-journey.cy.ts +12 -10
- package/templates/cypress-template/cypress/support/app-config.ts +5 -5
- package/templates/cypress-template/cypress/support/commands.ts +7 -7
- package/templates/cypress-template/cypress/support/data/data-factory.ts +6 -4
- package/templates/cypress-template/cypress/support/data/id-generator.ts +1 -1
- package/templates/cypress-template/cypress/support/data/seeded-faker.ts +2 -2
- package/templates/cypress-template/cypress/support/e2e.ts +2 -2
- package/templates/cypress-template/cypress/support/pages/login-page.ts +4 -4
- package/templates/cypress-template/cypress/support/pages/people-page.ts +10 -10
- package/templates/cypress-template/cypress.config.ts +9 -9
- package/templates/cypress-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/cypress-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/cypress-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/cypress-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/cypress-template/eslint.config.mjs +53 -45
- package/templates/cypress-template/package.json +6 -5
- package/templates/cypress-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/cypress-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/cypress-template/scripts/run-cypress.mjs +33 -24
- package/templates/cypress-template/scripts/run-tests.sh +1 -0
- package/templates/cypress-template/tsconfig.json +7 -1
- package/templates/playwright-template/.env.example +6 -6
- package/templates/playwright-template/.github/workflows/playwright-tests.yml +14 -5
- package/templates/playwright-template/README.md +6 -5
- package/templates/playwright-template/allurerc.mjs +1 -1
- package/templates/playwright-template/components/flash-message.ts +2 -2
- package/templates/playwright-template/config/environments.ts +16 -14
- package/templates/playwright-template/config/runtime-config.ts +17 -12
- package/templates/playwright-template/config/secret-manager.ts +1 -1
- package/templates/playwright-template/config/test-env.ts +3 -3
- package/templates/playwright-template/data/factories/data-factory.ts +6 -4
- package/templates/playwright-template/data/generators/id-generator.ts +1 -1
- package/templates/playwright-template/data/generators/seeded-faker.ts +2 -2
- package/templates/playwright-template/demo-apps/api-demo-server/src/server.js +9 -9
- package/templates/playwright-template/demo-apps/api-demo-server/src/store.js +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/playwright-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/playwright-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/playwright-template/eslint.config.mjs +40 -40
- package/templates/playwright-template/fixtures/test-fixtures.ts +27 -12
- package/templates/playwright-template/lint/architecture-plugin.cjs +36 -31
- package/templates/playwright-template/package.json +7 -6
- package/templates/playwright-template/pages/base-page.ts +4 -4
- package/templates/playwright-template/pages/login-page.ts +9 -9
- package/templates/playwright-template/pages/people-page.ts +21 -17
- package/templates/playwright-template/playwright.config.ts +22 -19
- package/templates/playwright-template/reporters/structured-reporter.ts +11 -8
- package/templates/playwright-template/scripts/ensure-local-env.mjs +37 -0
- package/templates/playwright-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/playwright-template/scripts/run-tests.sh +1 -0
- package/templates/playwright-template/tests/api-people.spec.ts +8 -6
- package/templates/playwright-template/tests/ui-journey.spec.ts +13 -8
- package/templates/playwright-template/tsconfig.json +3 -11
- package/templates/playwright-template/utils/logger.ts +12 -8
- package/templates/playwright-template/utils/test-step.ts +5 -5
- package/templates/wdio-template/.env.example +14 -0
- package/templates/wdio-template/.github/workflows/wdio-tests.yml +46 -0
- package/templates/wdio-template/README.md +241 -0
- package/templates/wdio-template/allurerc.mjs +10 -0
- package/templates/wdio-template/components/README.md +5 -0
- package/templates/wdio-template/components/flash-message.ts +16 -0
- package/templates/wdio-template/config/README.md +5 -0
- package/templates/wdio-template/config/environments.ts +40 -0
- package/templates/wdio-template/config/runtime-config.ts +53 -0
- package/templates/wdio-template/config/secret-manager.ts +29 -0
- package/templates/wdio-template/config/test-env.ts +9 -0
- package/templates/wdio-template/data/README.md +9 -0
- package/templates/wdio-template/data/factories/README.md +6 -0
- package/templates/wdio-template/data/factories/data-factory.ts +36 -0
- package/templates/wdio-template/data/generators/README.md +5 -0
- package/templates/wdio-template/data/generators/id-generator.ts +18 -0
- package/templates/wdio-template/data/generators/seeded-faker.ts +14 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/public/styles.css +120 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/server.js +152 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/store.js +71 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/templates.js +121 -0
- package/templates/wdio-template/eslint.config.mjs +86 -0
- package/templates/wdio-template/lint/architecture-plugin.cjs +123 -0
- package/templates/wdio-template/package-lock.json +11058 -0
- package/templates/wdio-template/package.json +44 -0
- package/templates/wdio-template/pages/README.md +6 -0
- package/templates/wdio-template/pages/base-page.ts +15 -0
- package/templates/wdio-template/pages/login-page.ts +27 -0
- package/templates/wdio-template/pages/people-page.ts +54 -0
- package/templates/wdio-template/reporters/README.md +5 -0
- package/templates/wdio-template/reporters/structured-reporter.ts +78 -0
- package/templates/wdio-template/scripts/README.md +5 -0
- package/templates/wdio-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/wdio-template/scripts/generate-allure-report.mjs +72 -0
- package/templates/wdio-template/scripts/run-tests.sh +7 -0
- package/templates/wdio-template/scripts/run-wdio.mjs +114 -0
- package/templates/wdio-template/tests/README.md +7 -0
- package/templates/wdio-template/tests/ui-journey.spec.ts +52 -0
- package/templates/wdio-template/tsconfig.json +22 -0
- package/templates/wdio-template/utils/README.md +5 -0
- package/templates/wdio-template/utils/logger.ts +60 -0
- package/templates/wdio-template/utils/test-step.ts +20 -0
- package/templates/wdio-template/wdio.conf.ts +58 -0
- package/tests/args.test.js +58 -0
- package/tests/local-env.test.js +70 -0
- package/tests/metadata.test.js +147 -0
- package/tests/templates.test.js +44 -0
package/lib/metadata.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
function sha256(content) {
|
|
6
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizePath(value) {
|
|
10
|
+
return value.split(path.sep).join('/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function pathMatchesPattern(relativePath, pattern) {
|
|
14
|
+
if (pattern.endsWith('/**')) {
|
|
15
|
+
const prefix = pattern.slice(0, -3);
|
|
16
|
+
return relativePath === prefix || relativePath.startsWith(`${prefix}/`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return relativePath === pattern;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function collectRelativeFiles(rootDirectory) {
|
|
23
|
+
const results = [];
|
|
24
|
+
|
|
25
|
+
function visit(currentDirectory) {
|
|
26
|
+
const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
|
|
27
|
+
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const absolutePath = path.join(currentDirectory, entry.name);
|
|
30
|
+
const relativePath = normalizePath(
|
|
31
|
+
path.relative(rootDirectory, absolutePath)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
visit(absolutePath);
|
|
36
|
+
} else {
|
|
37
|
+
results.push(relativePath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
visit(rootDirectory);
|
|
43
|
+
|
|
44
|
+
return results.sort();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isManagedFile(template, relativePath, managedPatterns) {
|
|
48
|
+
const patterns = [
|
|
49
|
+
...managedPatterns.common,
|
|
50
|
+
...(managedPatterns[template.id] || [])
|
|
51
|
+
];
|
|
52
|
+
return patterns.some((pattern) => pathMatchesPattern(relativePath, pattern));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function transformTemplateFile(
|
|
56
|
+
relativePath,
|
|
57
|
+
content,
|
|
58
|
+
targetDirectory,
|
|
59
|
+
template,
|
|
60
|
+
toPackageName
|
|
61
|
+
) {
|
|
62
|
+
const packageName = toPackageName(targetDirectory, template);
|
|
63
|
+
|
|
64
|
+
if (relativePath === 'package.json') {
|
|
65
|
+
const pkg = JSON.parse(content);
|
|
66
|
+
return `${JSON.stringify({ ...pkg, name: packageName }, null, 2)}\n`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (relativePath === 'package-lock.json') {
|
|
70
|
+
const lock = JSON.parse(content);
|
|
71
|
+
return `${JSON.stringify(
|
|
72
|
+
{
|
|
73
|
+
...lock,
|
|
74
|
+
name: packageName,
|
|
75
|
+
packages: lock.packages
|
|
76
|
+
? {
|
|
77
|
+
...lock.packages,
|
|
78
|
+
'': {
|
|
79
|
+
...lock.packages[''],
|
|
80
|
+
name: packageName
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
: lock.packages
|
|
84
|
+
},
|
|
85
|
+
null,
|
|
86
|
+
2
|
|
87
|
+
)}\n`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return content;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderTemplateFile(template, relativePath, targetDirectory, options) {
|
|
94
|
+
const { defaultGitignore, getTemplateDirectory, toPackageName } = options;
|
|
95
|
+
|
|
96
|
+
if (relativePath === '.gitignore') {
|
|
97
|
+
const gitignorePath = path.join(
|
|
98
|
+
getTemplateDirectory(template.id),
|
|
99
|
+
'.gitignore'
|
|
100
|
+
);
|
|
101
|
+
const gitignoreContent = fs.existsSync(gitignorePath)
|
|
102
|
+
? fs.readFileSync(gitignorePath, 'utf8')
|
|
103
|
+
: defaultGitignore;
|
|
104
|
+
return transformTemplateFile(
|
|
105
|
+
relativePath,
|
|
106
|
+
gitignoreContent,
|
|
107
|
+
targetDirectory,
|
|
108
|
+
template,
|
|
109
|
+
toPackageName
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sourcePath = path.join(getTemplateDirectory(template.id), relativePath);
|
|
114
|
+
const content = fs.readFileSync(sourcePath, 'utf8');
|
|
115
|
+
return transformTemplateFile(
|
|
116
|
+
relativePath,
|
|
117
|
+
content,
|
|
118
|
+
targetDirectory,
|
|
119
|
+
template,
|
|
120
|
+
toPackageName
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getManagedRelativePaths(template, options) {
|
|
125
|
+
const { getTemplateDirectory, managedPatterns, metadataFilename } = options;
|
|
126
|
+
const templateDirectory = getTemplateDirectory(template.id);
|
|
127
|
+
const templateFiles = collectRelativeFiles(templateDirectory).filter(
|
|
128
|
+
(relativePath) => isManagedFile(template, relativePath, managedPatterns)
|
|
129
|
+
);
|
|
130
|
+
const managedFiles = new Set(templateFiles);
|
|
131
|
+
managedFiles.add('.gitignore');
|
|
132
|
+
managedFiles.delete(metadataFilename);
|
|
133
|
+
return [...managedFiles].sort();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getMetadataPath(targetDirectory, metadataFilename) {
|
|
137
|
+
return path.join(targetDirectory, metadataFilename);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildProjectMetadata(template, targetDirectory, options) {
|
|
141
|
+
const { cliPackageVersion, metadataFilename } = options;
|
|
142
|
+
const managedFiles = {};
|
|
143
|
+
|
|
144
|
+
for (const relativePath of getManagedRelativePaths(template, options)) {
|
|
145
|
+
const absolutePath = path.join(targetDirectory, relativePath);
|
|
146
|
+
if (!fs.existsSync(absolutePath)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
managedFiles[relativePath] = {
|
|
151
|
+
baselineHash: sha256(fs.readFileSync(absolutePath, 'utf8'))
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
schemaVersion: 1,
|
|
157
|
+
template: template.id,
|
|
158
|
+
templateVersion: cliPackageVersion,
|
|
159
|
+
packageName: options.toPackageName(targetDirectory, template),
|
|
160
|
+
generatedAt: new Date().toISOString(),
|
|
161
|
+
managedFiles,
|
|
162
|
+
metadataFilename
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function writeProjectMetadata(
|
|
167
|
+
template,
|
|
168
|
+
targetDirectory,
|
|
169
|
+
existingMetadata,
|
|
170
|
+
options
|
|
171
|
+
) {
|
|
172
|
+
const nextMetadata = buildProjectMetadata(template, targetDirectory, options);
|
|
173
|
+
|
|
174
|
+
if (existingMetadata) {
|
|
175
|
+
nextMetadata.generatedAt =
|
|
176
|
+
existingMetadata.generatedAt || nextMetadata.generatedAt;
|
|
177
|
+
nextMetadata.templateVersion =
|
|
178
|
+
existingMetadata.templateVersion || nextMetadata.templateVersion;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fs.writeFileSync(
|
|
182
|
+
getMetadataPath(targetDirectory, options.metadataFilename),
|
|
183
|
+
`${JSON.stringify(nextMetadata, null, 2)}\n`,
|
|
184
|
+
'utf8'
|
|
185
|
+
);
|
|
186
|
+
return nextMetadata;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readProjectMetadata(targetDirectory, metadataFilename) {
|
|
190
|
+
const metadataPath = getMetadataPath(targetDirectory, metadataFilename);
|
|
191
|
+
|
|
192
|
+
if (!fs.existsSync(metadataPath)) {
|
|
193
|
+
throw new Error(`No ${metadataFilename} file found in ${targetDirectory}.`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function detectTemplateFromProject(targetDirectory, metadataFilename) {
|
|
200
|
+
const metadataPath = getMetadataPath(targetDirectory, metadataFilename);
|
|
201
|
+
if (fs.existsSync(metadataPath)) {
|
|
202
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
203
|
+
return metadata.template;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (fs.existsSync(path.join(targetDirectory, 'playwright.config.ts'))) {
|
|
207
|
+
return 'playwright-template';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (fs.existsSync(path.join(targetDirectory, 'cypress.config.ts'))) {
|
|
211
|
+
return 'cypress-template';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (fs.existsSync(path.join(targetDirectory, 'wdio.conf.ts'))) {
|
|
215
|
+
return 'wdio-template';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
throw new Error(`Could not detect the template used for ${targetDirectory}.`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function analyzeUpgrade(template, targetDirectory, metadata, options) {
|
|
222
|
+
const managedPaths = getManagedRelativePaths(template, options);
|
|
223
|
+
const results = [];
|
|
224
|
+
|
|
225
|
+
for (const relativePath of managedPaths) {
|
|
226
|
+
const absolutePath = path.join(targetDirectory, relativePath);
|
|
227
|
+
const latestContent = renderTemplateFile(
|
|
228
|
+
template,
|
|
229
|
+
relativePath,
|
|
230
|
+
targetDirectory,
|
|
231
|
+
options
|
|
232
|
+
);
|
|
233
|
+
const latestHash = sha256(latestContent);
|
|
234
|
+
const baselineHash =
|
|
235
|
+
metadata.managedFiles?.[relativePath]?.baselineHash || null;
|
|
236
|
+
const currentExists = fs.existsSync(absolutePath);
|
|
237
|
+
const currentHash = currentExists
|
|
238
|
+
? sha256(fs.readFileSync(absolutePath, 'utf8'))
|
|
239
|
+
: null;
|
|
240
|
+
|
|
241
|
+
let status = 'up-to-date';
|
|
242
|
+
|
|
243
|
+
if (!baselineHash) {
|
|
244
|
+
if (!currentExists) {
|
|
245
|
+
status = 'new-file';
|
|
246
|
+
} else if (currentHash === latestHash) {
|
|
247
|
+
status = 'new-file';
|
|
248
|
+
} else {
|
|
249
|
+
status = 'conflict';
|
|
250
|
+
}
|
|
251
|
+
} else if (!currentExists) {
|
|
252
|
+
status = 'conflict';
|
|
253
|
+
} else if (currentHash === latestHash) {
|
|
254
|
+
status = 'up-to-date';
|
|
255
|
+
} else if (currentHash === baselineHash) {
|
|
256
|
+
status = 'safe-update';
|
|
257
|
+
} else {
|
|
258
|
+
status = 'conflict';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
results.push({
|
|
262
|
+
relativePath,
|
|
263
|
+
status,
|
|
264
|
+
latestContent,
|
|
265
|
+
latestHash,
|
|
266
|
+
currentHash,
|
|
267
|
+
baselineHash,
|
|
268
|
+
currentExists
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return results;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function applySafeUpdates(targetDirectory, metadata, results, options) {
|
|
276
|
+
const nextMetadata = {
|
|
277
|
+
...metadata,
|
|
278
|
+
managedFiles: {
|
|
279
|
+
...metadata.managedFiles
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
let appliedCount = 0;
|
|
284
|
+
|
|
285
|
+
for (const entry of results) {
|
|
286
|
+
if (!['safe-update', 'new-file'].includes(entry.status)) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const absolutePath = path.join(targetDirectory, entry.relativePath);
|
|
291
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
292
|
+
fs.writeFileSync(absolutePath, entry.latestContent, 'utf8');
|
|
293
|
+
nextMetadata.managedFiles[entry.relativePath] = {
|
|
294
|
+
baselineHash: entry.latestHash
|
|
295
|
+
};
|
|
296
|
+
appliedCount += 1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const remainingConflicts = results.filter(
|
|
300
|
+
(entry) => entry.status === 'conflict'
|
|
301
|
+
).length;
|
|
302
|
+
if (remainingConflicts === 0) {
|
|
303
|
+
nextMetadata.templateVersion = options.cliPackageVersion;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
fs.writeFileSync(
|
|
307
|
+
getMetadataPath(targetDirectory, options.metadataFilename),
|
|
308
|
+
`${JSON.stringify(nextMetadata, null, 2)}\n`,
|
|
309
|
+
'utf8'
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
appliedCount,
|
|
314
|
+
remainingConflicts
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
analyzeUpgrade,
|
|
320
|
+
applySafeUpdates,
|
|
321
|
+
buildProjectMetadata,
|
|
322
|
+
collectRelativeFiles,
|
|
323
|
+
detectTemplateFromProject,
|
|
324
|
+
getManagedRelativePaths,
|
|
325
|
+
readProjectMetadata,
|
|
326
|
+
renderTemplateFile,
|
|
327
|
+
sha256,
|
|
328
|
+
writeProjectMetadata
|
|
329
|
+
};
|
package/lib/output.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const { MIN_NODE_VERSION } = require('./constants');
|
|
2
|
+
|
|
3
|
+
function createColors(processObject = process) {
|
|
4
|
+
const colorEnabled =
|
|
5
|
+
Boolean(processObject.stdout?.isTTY) && !('NO_COLOR' in processObject.env);
|
|
6
|
+
|
|
7
|
+
function style(text, ...codes) {
|
|
8
|
+
if (!colorEnabled) {
|
|
9
|
+
return text;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return `\u001b[${codes.join(';')}m${text}\u001b[0m`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
bold(text) {
|
|
17
|
+
return style(text, 1);
|
|
18
|
+
},
|
|
19
|
+
dim(text) {
|
|
20
|
+
return style(text, 2);
|
|
21
|
+
},
|
|
22
|
+
cyan(text) {
|
|
23
|
+
return style(text, 36);
|
|
24
|
+
},
|
|
25
|
+
green(text) {
|
|
26
|
+
return style(text, 32);
|
|
27
|
+
},
|
|
28
|
+
yellow(text) {
|
|
29
|
+
return style(text, 33);
|
|
30
|
+
},
|
|
31
|
+
red(text) {
|
|
32
|
+
return style(text, 31);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseNodeVersion(version) {
|
|
38
|
+
const normalized = version.replace(/^v/, '');
|
|
39
|
+
const [major = '0', minor = '0', patch = '0'] = normalized.split('.');
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
major: Number.parseInt(major, 10),
|
|
43
|
+
minor: Number.parseInt(minor, 10),
|
|
44
|
+
patch: Number.parseInt(patch, 10)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isNodeVersionSupported(version) {
|
|
49
|
+
if (version.major !== MIN_NODE_VERSION.major) {
|
|
50
|
+
return version.major > MIN_NODE_VERSION.major;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (version.minor !== MIN_NODE_VERSION.minor) {
|
|
54
|
+
return version.minor > MIN_NODE_VERSION.minor;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return version.patch >= MIN_NODE_VERSION.patch;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertSupportedNodeVersion(processVersion = process.version) {
|
|
61
|
+
const currentVersion = parseNodeVersion(processVersion);
|
|
62
|
+
|
|
63
|
+
if (!isNodeVersionSupported(currentVersion)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Node ${MIN_NODE_VERSION.major}.${MIN_NODE_VERSION.minor}.${MIN_NODE_VERSION.patch}+ is required. Current version: ${processVersion}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printHelp(templates, colors, defaultTemplate) {
|
|
71
|
+
const supportedTemplates = templates
|
|
72
|
+
.map(
|
|
73
|
+
(template) =>
|
|
74
|
+
` ${template.id}${template.aliases.length > 0 ? ` (${template.aliases.join(', ')})` : ''}`
|
|
75
|
+
)
|
|
76
|
+
.join('\n');
|
|
77
|
+
|
|
78
|
+
process.stdout.write(`${colors.bold('create-qa-patterns')}
|
|
79
|
+
|
|
80
|
+
Usage:
|
|
81
|
+
create-qa-patterns
|
|
82
|
+
create-qa-patterns <target-directory>
|
|
83
|
+
create-qa-patterns <template> [target-directory]
|
|
84
|
+
create-qa-patterns --template <template> [target-directory]
|
|
85
|
+
create-qa-patterns upgrade check [target-directory]
|
|
86
|
+
create-qa-patterns upgrade apply --safe [target-directory]
|
|
87
|
+
|
|
88
|
+
Options:
|
|
89
|
+
--yes Accept all post-generate prompts
|
|
90
|
+
--no-install Skip npm install
|
|
91
|
+
--no-setup Skip template-specific setup such as Playwright browser install
|
|
92
|
+
--no-test Skip npm test
|
|
93
|
+
--template Explicitly choose a template without using positional arguments
|
|
94
|
+
--safe Required with upgrade apply; only updates unchanged managed files
|
|
95
|
+
|
|
96
|
+
Interactive mode:
|
|
97
|
+
When run without an explicit template, the CLI shows an interactive template picker.
|
|
98
|
+
Default template in non-interactive mode: ${defaultTemplate}
|
|
99
|
+
|
|
100
|
+
Supported templates:
|
|
101
|
+
${supportedTemplates}
|
|
102
|
+
`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function printPrerequisiteWarnings(prerequisites, colors) {
|
|
106
|
+
if (!prerequisites.npm) {
|
|
107
|
+
process.stdout.write(
|
|
108
|
+
`${colors.yellow('Warning:')} npm was not found. Automated install and test steps will be unavailable.\n`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!prerequisites.npx) {
|
|
113
|
+
process.stdout.write(
|
|
114
|
+
`${colors.yellow('Warning:')} npx was not found. Template setup steps that depend on npx will be unavailable.\n`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!prerequisites.docker) {
|
|
119
|
+
process.stdout.write(
|
|
120
|
+
`${colors.yellow('Warning:')} docker was not found. Docker-based template flows will not run until Docker is installed.\n`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!prerequisites.git) {
|
|
125
|
+
process.stdout.write(
|
|
126
|
+
`${colors.yellow('Warning:')} git was not found. The generated project cannot be initialized as a repository automatically.\n`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
!prerequisites.npm ||
|
|
132
|
+
!prerequisites.npx ||
|
|
133
|
+
!prerequisites.docker ||
|
|
134
|
+
!prerequisites.git
|
|
135
|
+
) {
|
|
136
|
+
process.stdout.write('\n');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function printPlaywrightInstallRecovery(targetDirectory, colors) {
|
|
141
|
+
process.stdout.write(`
|
|
142
|
+
${colors.yellow('Playwright browser installation did not complete.')}
|
|
143
|
+
|
|
144
|
+
Common cause:
|
|
145
|
+
Missing OS packages required to run Playwright browsers.
|
|
146
|
+
|
|
147
|
+
Recommended next steps:
|
|
148
|
+
cd ${targetDirectory}
|
|
149
|
+
sudo npx playwright install-deps
|
|
150
|
+
npx playwright install
|
|
151
|
+
|
|
152
|
+
If you already know the missing package name, install it with your system package manager and then rerun:
|
|
153
|
+
npx playwright install
|
|
154
|
+
|
|
155
|
+
The template was generated successfully. You can complete browser setup later.
|
|
156
|
+
|
|
157
|
+
`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function printSuccess(
|
|
161
|
+
template,
|
|
162
|
+
targetDirectory,
|
|
163
|
+
generatedInCurrentDirectory,
|
|
164
|
+
colors
|
|
165
|
+
) {
|
|
166
|
+
process.stdout.write(`\n${colors.green(colors.bold('Success'))}
|
|
167
|
+
Generated ${template.label} in ${targetDirectory}
|
|
168
|
+
\n`);
|
|
169
|
+
|
|
170
|
+
if (!generatedInCurrentDirectory) {
|
|
171
|
+
process.stdout.write(
|
|
172
|
+
`${colors.cyan('Your shell stays in the original directory. To work in the generated project, run:')}\n cd ${targetDirectory}\n\n`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function formatStatus(status, colors) {
|
|
178
|
+
switch (status) {
|
|
179
|
+
case 'completed':
|
|
180
|
+
return colors.green('completed');
|
|
181
|
+
case 'skipped':
|
|
182
|
+
return colors.dim('skipped');
|
|
183
|
+
case 'unavailable':
|
|
184
|
+
return colors.yellow('unavailable');
|
|
185
|
+
case 'manual-recovery':
|
|
186
|
+
return colors.yellow('manual recovery required');
|
|
187
|
+
default:
|
|
188
|
+
return colors.dim('not run');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatUpgradeStatus(status, colors) {
|
|
193
|
+
switch (status) {
|
|
194
|
+
case 'safe-update':
|
|
195
|
+
return colors.green('safe update available');
|
|
196
|
+
case 'new-file':
|
|
197
|
+
return colors.green('new managed file available');
|
|
198
|
+
case 'conflict':
|
|
199
|
+
return colors.yellow('manual review required');
|
|
200
|
+
default:
|
|
201
|
+
return colors.dim('up to date');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function printUpgradeReport(
|
|
206
|
+
targetDirectory,
|
|
207
|
+
metadata,
|
|
208
|
+
results,
|
|
209
|
+
cliPackageVersion,
|
|
210
|
+
colors
|
|
211
|
+
) {
|
|
212
|
+
const safeCount = results.filter(
|
|
213
|
+
(entry) => entry.status === 'safe-update'
|
|
214
|
+
).length;
|
|
215
|
+
const newCount = results.filter(
|
|
216
|
+
(entry) => entry.status === 'new-file'
|
|
217
|
+
).length;
|
|
218
|
+
const conflictCount = results.filter(
|
|
219
|
+
(entry) => entry.status === 'conflict'
|
|
220
|
+
).length;
|
|
221
|
+
|
|
222
|
+
process.stdout.write(`\n${colors.bold('Upgrade check')}\n`);
|
|
223
|
+
process.stdout.write(` Target: ${targetDirectory}\n`);
|
|
224
|
+
process.stdout.write(` Template: ${metadata.template}\n`);
|
|
225
|
+
process.stdout.write(
|
|
226
|
+
` Current baseline version: ${metadata.templateVersion}\n`
|
|
227
|
+
);
|
|
228
|
+
process.stdout.write(` CLI template version: ${cliPackageVersion}\n`);
|
|
229
|
+
process.stdout.write(` Safe updates: ${safeCount}\n`);
|
|
230
|
+
process.stdout.write(` New managed files: ${newCount}\n`);
|
|
231
|
+
process.stdout.write(` Conflicts: ${conflictCount}\n\n`);
|
|
232
|
+
|
|
233
|
+
for (const entry of results) {
|
|
234
|
+
if (entry.status === 'up-to-date') {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
process.stdout.write(
|
|
239
|
+
` ${entry.relativePath}: ${formatUpgradeStatus(entry.status, colors)}\n`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (safeCount === 0 && newCount === 0 && conflictCount === 0) {
|
|
244
|
+
process.stdout.write(
|
|
245
|
+
`${colors.green('Everything already matches the current managed template files.')}\n`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
process.stdout.write('\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function printSummary(summary, colors) {
|
|
253
|
+
process.stdout.write(`\n${colors.bold('Summary')}\n`);
|
|
254
|
+
process.stdout.write(` Template: ${summary.template.id}\n`);
|
|
255
|
+
process.stdout.write(` Target: ${summary.targetDirectory}\n`);
|
|
256
|
+
process.stdout.write(
|
|
257
|
+
` Git repository: ${formatStatus(summary.gitInit, colors)}\n`
|
|
258
|
+
);
|
|
259
|
+
process.stdout.write(
|
|
260
|
+
` Demo apps: ${summary.demoAppsManagedByTemplate ? 'bundled and auto-started in dev when using default local URLs' : 'external application required'}\n`
|
|
261
|
+
);
|
|
262
|
+
if (summary.localCredentials) {
|
|
263
|
+
process.stdout.write(
|
|
264
|
+
` Local credentials: ${summary.localCredentials.username} / ${summary.localCredentials.password}\n`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
process.stdout.write(
|
|
268
|
+
` npm install: ${formatStatus(summary.npmInstall, colors)}\n`
|
|
269
|
+
);
|
|
270
|
+
if (summary.template.setup) {
|
|
271
|
+
process.stdout.write(
|
|
272
|
+
` ${summary.template.setup.summaryLabel}: ${formatStatus(summary.extraSetup, colors)}\n`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
process.stdout.write(
|
|
276
|
+
` npm test: ${formatStatus(summary.testRun, colors)}\n`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function printNextSteps(summary, colors) {
|
|
281
|
+
const steps = [];
|
|
282
|
+
|
|
283
|
+
if (!summary.generatedInCurrentDirectory) {
|
|
284
|
+
steps.push(`cd ${summary.targetRelativePath}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (summary.npmInstall !== 'completed') {
|
|
288
|
+
steps.push('npm install');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (summary.template.setup && summary.extraSetup !== 'completed') {
|
|
292
|
+
steps.push(summary.template.setup.nextStep);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (summary.testRun !== 'completed') {
|
|
296
|
+
steps.push('npm test');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (steps.length > 0) {
|
|
300
|
+
process.stdout.write(`${colors.cyan('Next steps:')}\n`);
|
|
301
|
+
for (const step of steps) {
|
|
302
|
+
process.stdout.write(` ${step}\n`);
|
|
303
|
+
}
|
|
304
|
+
process.stdout.write('\n');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (summary.demoAppsManagedByTemplate) {
|
|
308
|
+
process.stdout.write(
|
|
309
|
+
`${colors.yellow(colors.bold('Demo apps included:'))} sample tests run against bundled demo apps in local ${colors.bold('dev')}. Delete or replace ${colors.bold('demo-apps/')} if you do not want them.\n`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
process.stdout.write(`${colors.green(colors.bold('Happy testing.'))}\n`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = {
|
|
317
|
+
assertSupportedNodeVersion,
|
|
318
|
+
createColors,
|
|
319
|
+
printHelp,
|
|
320
|
+
printNextSteps,
|
|
321
|
+
printPlaywrightInstallRecovery,
|
|
322
|
+
printPrerequisiteWarnings,
|
|
323
|
+
printSuccess,
|
|
324
|
+
printSummary,
|
|
325
|
+
printUpgradeReport
|
|
326
|
+
};
|
package/lib/prereqs.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
function getCommandName(base) {
|
|
5
|
+
if (process.platform === 'win32') {
|
|
6
|
+
return `${base}.cmd`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return base;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function commandExists(command) {
|
|
13
|
+
const result = spawnSync(getCommandName(command), ['--version'], {
|
|
14
|
+
stdio: 'ignore'
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return !result.error && result.status === 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collectPrerequisites() {
|
|
21
|
+
return {
|
|
22
|
+
npm: commandExists('npm'),
|
|
23
|
+
npx: commandExists('npx'),
|
|
24
|
+
docker: commandExists('docker'),
|
|
25
|
+
git: commandExists('git')
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function initializeGitRepository(targetDirectory) {
|
|
30
|
+
if (require('node:fs').existsSync(path.join(targetDirectory, '.git'))) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = spawnSync(getCommandName('git'), ['init'], {
|
|
35
|
+
cwd: targetDirectory,
|
|
36
|
+
encoding: 'utf8'
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
throw new Error(result.stderr || 'git init failed.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runCommand(command, args, cwd) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const child = spawn(getCommandName(command), args, {
|
|
47
|
+
cwd,
|
|
48
|
+
stdio: 'inherit'
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
child.on('close', (code) => {
|
|
52
|
+
if (code === 0) {
|
|
53
|
+
resolve();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
reject(
|
|
58
|
+
new Error(`${command} ${args.join(' ')} exited with code ${code}`)
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
child.on('error', reject);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
collectPrerequisites,
|
|
68
|
+
commandExists,
|
|
69
|
+
getCommandName,
|
|
70
|
+
initializeGitRepository,
|
|
71
|
+
runCommand
|
|
72
|
+
};
|