create-allwright 0.0.59

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 ADDED
@@ -0,0 +1,23 @@
1
+ # create-allwright
2
+
3
+ Create a new allwright test project with:
4
+
5
+ ```bash
6
+ npm init allwright@latest
7
+ ```
8
+
9
+ Or:
10
+
11
+ ```bash
12
+ npm create allwright@latest
13
+ ```
14
+
15
+ The initializer prompts for:
16
+
17
+ - `TypeScript` or `JavaScript`
18
+ - one or more surfaces such as `Web` and `Mobile Android`
19
+ - an optional target directory
20
+
21
+ It scaffolds a Node project with `package.json`, Vitest config, shared allwright config, starter tests, and a short README for the generated app.
22
+
23
+ Dependencies are installed automatically with the package manager that ran the initializer. Existing lockfiles take precedence; use `--package-manager npm|yarn|pnpm|bun` to override detection, or `--no-install` to scaffold without installing.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,470 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { access, mkdir, writeFile } from "node:fs/promises";
4
+ import { createRequire } from "node:module";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import * as p from "@clack/prompts";
8
+ const VITEST_VERSION = "^3.2.4";
9
+ const TYPESCRIPT_VERSION = "^5.9.2";
10
+ const PACKAGE_VERSION = getPackageVersion();
11
+ const ALLWRIGHT_VERSION = `^${PACKAGE_VERSION}`;
12
+ const SURFACE_OPTIONS = [
13
+ { label: "Web", value: "web" },
14
+ { label: "Mobile Android", value: "mobile-android" },
15
+ ];
16
+ async function main() {
17
+ const options = parseArgs(process.argv.slice(2));
18
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
19
+ const language = options.language ??
20
+ (options.yes
21
+ ? "ts"
22
+ : await selectLanguage(interactive));
23
+ const surfaces = options.surfaces.length > 0
24
+ ? dedupeSurfaces(options.surfaces)
25
+ : options.yes
26
+ ? ["web"]
27
+ : await selectSurfaces(interactive);
28
+ const targetDir = path.resolve(process.cwd(), options.targetDir);
29
+ const relativeTargetDir = path.relative(process.cwd(), targetDir) || ".";
30
+ const files = buildProjectFiles({
31
+ language,
32
+ surfaces,
33
+ packageName: inferPackageName(path.basename(targetDir) || "allwright-app"),
34
+ });
35
+ await ensureTargetDirectory(targetDir);
36
+ const conflicts = await findConflicts(targetDir, Object.keys(files));
37
+ if (conflicts.length > 0 && !options.force) {
38
+ const proceed = options.yes || (await confirmOverwrite(interactive, relativeTargetDir, conflicts));
39
+ if (!proceed) {
40
+ throw new Error("Initialization cancelled.");
41
+ }
42
+ }
43
+ await writeProjectFiles(targetDir, files);
44
+ const packageManager = options.packageManager ?? (await detectPackageManager(targetDir));
45
+ if (options.install) {
46
+ await installDependencies(targetDir, packageManager);
47
+ }
48
+ printSummary(targetDir, relativeTargetDir, language, surfaces, packageManager, options.install);
49
+ }
50
+ function parseArgs(args) {
51
+ const options = {
52
+ targetDir: ".",
53
+ surfaces: [],
54
+ yes: false,
55
+ force: false,
56
+ install: true,
57
+ };
58
+ for (let index = 0; index < args.length; index += 1) {
59
+ const arg = args[index];
60
+ switch (arg) {
61
+ case "--yes":
62
+ case "-y":
63
+ options.yes = true;
64
+ break;
65
+ case "--force":
66
+ options.force = true;
67
+ break;
68
+ case "--no-install":
69
+ options.install = false;
70
+ break;
71
+ case "--package-manager": {
72
+ const value = args[index + 1];
73
+ if (!value) {
74
+ throw new Error("Expected a package manager after --package-manager.");
75
+ }
76
+ assertUnset(options.packageManager, "package manager");
77
+ options.packageManager = parsePackageManager(value);
78
+ index += 1;
79
+ break;
80
+ }
81
+ case "--typescript":
82
+ case "--ts":
83
+ assertUnset(options.language, "language");
84
+ options.language = "ts";
85
+ break;
86
+ case "--javascript":
87
+ case "--js":
88
+ assertUnset(options.language, "language");
89
+ options.language = "js";
90
+ break;
91
+ case "--web":
92
+ options.surfaces.push("web");
93
+ break;
94
+ case "--mobile":
95
+ case "--mobile-android":
96
+ options.surfaces.push("mobile-android");
97
+ break;
98
+ case "--both":
99
+ options.surfaces.push("web", "mobile-android");
100
+ break;
101
+ case "--surface": {
102
+ const value = args[index + 1];
103
+ if (!value) {
104
+ throw new Error("Expected a surface id after --surface.");
105
+ }
106
+ options.surfaces.push(parseSurfaceId(value));
107
+ index += 1;
108
+ break;
109
+ }
110
+ default:
111
+ if (arg.startsWith("-")) {
112
+ throw new Error(`Unknown option: ${arg}`);
113
+ }
114
+ if (options.targetDir !== ".") {
115
+ throw new Error(`Unexpected extra argument: ${arg}`);
116
+ }
117
+ options.targetDir = arg;
118
+ break;
119
+ }
120
+ }
121
+ return options;
122
+ }
123
+ function assertUnset(value, kind) {
124
+ if (value !== undefined) {
125
+ throw new Error(`Received more than one ${kind} option.`);
126
+ }
127
+ }
128
+ function getPackageVersion() {
129
+ const require = createRequire(import.meta.url);
130
+ const packageJson = require("../package.json");
131
+ if (!packageJson.version) {
132
+ throw new Error("Could not determine create-allwright package version.");
133
+ }
134
+ return packageJson.version;
135
+ }
136
+ async function selectLanguage(interactive) {
137
+ requireInteractive(interactive, "pass --typescript, --javascript, or --yes");
138
+ const result = await p.select({
139
+ message: "Which language would you like to use?",
140
+ options: [
141
+ { label: "TypeScript", value: "ts", hint: "recommended" },
142
+ { label: "JavaScript", value: "js" },
143
+ ],
144
+ });
145
+ return resolvePromptResult(result);
146
+ }
147
+ async function selectSurfaces(interactive) {
148
+ requireInteractive(interactive, "pass --surface <id> or --yes");
149
+ const result = await p.multiselect({
150
+ message: "Which surfaces would you like to test?",
151
+ options: SURFACE_OPTIONS.map((surface) => ({
152
+ ...surface,
153
+ hint: surface.value === "web" ? "browser automation" : "Android app automation",
154
+ })),
155
+ required: true,
156
+ });
157
+ return dedupeSurfaces(resolvePromptResult(result));
158
+ }
159
+ async function confirmOverwrite(interactive, relativeTargetDir, conflicts) {
160
+ if (!interactive) {
161
+ return false;
162
+ }
163
+ const result = await p.confirm({
164
+ message: `Overwrite ${conflicts.length} existing file${conflicts.length === 1 ? "" : "s"} in ${relativeTargetDir}?`,
165
+ initialValue: false,
166
+ });
167
+ return resolvePromptResult(result);
168
+ }
169
+ function requireInteractive(interactive, hint) {
170
+ if (!interactive) {
171
+ throw new Error(`Run again in an interactive terminal, or ${hint}.`);
172
+ }
173
+ }
174
+ function resolvePromptResult(result) {
175
+ if (p.isCancel(result)) {
176
+ p.cancel("Initialization cancelled.");
177
+ process.exit(0);
178
+ }
179
+ return result;
180
+ }
181
+ async function ensureTargetDirectory(targetDir) {
182
+ await mkdir(targetDir, { recursive: true });
183
+ }
184
+ async function findConflicts(targetDir, paths) {
185
+ const conflicts = [];
186
+ for (const relativePath of paths) {
187
+ try {
188
+ await access(path.join(targetDir, relativePath));
189
+ conflicts.push(relativePath);
190
+ }
191
+ catch {
192
+ // file does not exist yet
193
+ }
194
+ }
195
+ return conflicts;
196
+ }
197
+ async function writeProjectFiles(targetDir, files) {
198
+ for (const [relativePath, contents] of Object.entries(files)) {
199
+ const destination = path.join(targetDir, relativePath);
200
+ await mkdir(path.dirname(destination), { recursive: true });
201
+ await writeFile(destination, contents, "utf8");
202
+ }
203
+ }
204
+ function buildProjectFiles(input) {
205
+ const { language, surfaces, packageName } = input;
206
+ const extension = language === "ts" ? "ts" : "js";
207
+ const files = {
208
+ ".gitignore": "node_modules/\ncoverage/\n.allwright/\n",
209
+ "package.json": packageJsonContents(packageName, language),
210
+ "allwright.config.yaml": allwrightConfigContents(surfaces),
211
+ [`vitest.config.${extension}`]: vitestConfigContents(language),
212
+ "README.md": generatedReadmeContents(surfaces),
213
+ };
214
+ if (language === "ts") {
215
+ files["tsconfig.json"] = tsconfigContents();
216
+ }
217
+ if (hasSurface(surfaces, "web")) {
218
+ files[`tests/web.spec.${extension}`] = webSpecContents(language);
219
+ }
220
+ if (hasSurface(surfaces, "mobile-android")) {
221
+ files[`tests/mobile.spec.${extension}`] = mobileSpecContents(language);
222
+ }
223
+ return files;
224
+ }
225
+ function packageJsonContents(packageName, language) {
226
+ const devDependencies = language === "ts"
227
+ ? ` "devDependencies": {
228
+ "@allwright.dev/vitest": "${ALLWRIGHT_VERSION}",
229
+ "@types/node": "^24.4.0",
230
+ "typescript": "${TYPESCRIPT_VERSION}",
231
+ "vitest": "${VITEST_VERSION}"
232
+ }`
233
+ : ` "devDependencies": {
234
+ "@allwright.dev/vitest": "${ALLWRIGHT_VERSION}",
235
+ "vitest": "${VITEST_VERSION}"
236
+ }`;
237
+ return `{
238
+ "name": "${packageName}",
239
+ "version": "0.0.0",
240
+ "private": true,
241
+ "type": "module",
242
+ "scripts": {
243
+ "test": "vitest run",
244
+ "test:watch": "vitest"
245
+ },
246
+ ${devDependencies}
247
+ }
248
+ `;
249
+ }
250
+ function tsconfigContents() {
251
+ return `{
252
+ "compilerOptions": {
253
+ "target": "ES2022",
254
+ "module": "ESNext",
255
+ "moduleResolution": "Bundler",
256
+ "strict": true,
257
+ "esModuleInterop": true,
258
+ "skipLibCheck": true,
259
+ "forceConsistentCasingInFileNames": true,
260
+ "types": ["vitest/globals"]
261
+ },
262
+ "include": ["tests", "vitest.config.ts"]
263
+ }
264
+ `;
265
+ }
266
+ function vitestConfigContents(language) {
267
+ const defineConfigImport = language === "ts"
268
+ ? 'import { defineConfig } from "vitest/config";'
269
+ : 'import { defineConfig } from "vitest/config";';
270
+ return `import { allwrightVitestConfig } from "@allwright.dev/vitest/config";
271
+ ${defineConfigImport}
272
+
273
+ export default allwrightVitestConfig(
274
+ defineConfig({
275
+ test: {
276
+ globals: true,
277
+ environment: "node",
278
+ },
279
+ }),
280
+ );
281
+ `;
282
+ }
283
+ function allwrightConfigContents(surfaces) {
284
+ const lines = [
285
+ "schemaVersion: 1",
286
+ "",
287
+ "server:",
288
+ ' addr: "127.0.0.1:50051"',
289
+ ];
290
+ if (hasSurface(surfaces, "web")) {
291
+ lines.push("");
292
+ lines.push("web:");
293
+ lines.push(" browser:");
294
+ lines.push(" name: chromium");
295
+ }
296
+ if (hasSurface(surfaces, "mobile-android")) {
297
+ lines.push("");
298
+ lines.push("mobile:");
299
+ lines.push(" android:");
300
+ lines.push(" app:");
301
+ lines.push(" id: com.example.airticket");
302
+ lines.push(' binary: "https://allwright.dev/Flights-debug.apk"');
303
+ }
304
+ lines.push("");
305
+ lines.push("expect:");
306
+ lines.push(" timeoutMs: 7000");
307
+ lines.push(" intervalMs: 100");
308
+ return `${lines.join("\n")}\n`;
309
+ }
310
+ function webSpecContents(language) {
311
+ const importLine = language === "ts"
312
+ ? 'import { expect, test } from "@allwright.dev/vitest";'
313
+ : 'import { expect, test } from "@allwright.dev/vitest";';
314
+ return `${importLine}
315
+
316
+ const WEB_URL = "https://themoderninternet.vercel.app";
317
+ const ENTRY_SELECTOR =
318
+ "xpath=//div[contains(@class,'card')][.//h2[normalize-space()='Form Inputs']]//button[normalize-space()='Visit page']";
319
+ const HEADING_SELECTOR = 'xpath=//h1[text()="Form Inputs"]';
320
+
321
+ test("opens the Form Inputs page", { timeout: 30_000 }, async ({ page }) => {
322
+ await page.goto(WEB_URL);
323
+ await page.click(ENTRY_SELECTOR);
324
+ await expect(page.locator(HEADING_SELECTOR)).toHaveText("Form Inputs");
325
+ });
326
+ `;
327
+ }
328
+ function mobileSpecContents(language) {
329
+ const importLine = language === "ts"
330
+ ? 'import { test } from "@allwright.dev/vitest";'
331
+ : 'import { test } from "@allwright.dev/vitest";';
332
+ return `${importLine}
333
+
334
+ test("opens the Android demo app and navigates to sign up", { timeout: 180_000 }, async ({ androidApp }) => {
335
+ await androidApp.click("text=Account");
336
+ await androidApp.click("text=Login");
337
+ await androidApp.click("text=Sign Up");
338
+ });
339
+ `;
340
+ }
341
+ function generatedReadmeContents(surfaces) {
342
+ const nextSteps = [
343
+ "## Next steps",
344
+ "",
345
+ "1. Run `npm install`.",
346
+ "2. Run `npm test`.",
347
+ ];
348
+ if (hasSurface(surfaces, "mobile-android")) {
349
+ nextSteps.push("3. Start an Android emulator or connect a device with USB debugging enabled.", "4. Confirm it is visible with `adb devices -l`, then run `npm test`.", "5. The first Android run downloads and installs the Airticket demo app configured in `allwright.config.yaml`.");
350
+ }
351
+ return `# allwright project
352
+
353
+ Scaffolded with \`npm init allwright\`.
354
+
355
+ This starter uses \`@allwright.dev/vitest\` so your test suite can drive the allwright engine with Playwright-style fixtures.
356
+
357
+ ${nextSteps.join("\n")}
358
+ `;
359
+ }
360
+ function inferPackageName(input) {
361
+ const normalized = input
362
+ .trim()
363
+ .toLowerCase()
364
+ .replace(/[^a-z0-9._-]+/g, "-")
365
+ .replace(/^-+|-+$/g, "");
366
+ return normalized || "allwright-app";
367
+ }
368
+ function hasSurface(surfaces, surface) {
369
+ return surfaces.includes(surface);
370
+ }
371
+ function parseSurfaceId(input) {
372
+ const normalized = input.trim().toLowerCase();
373
+ if (normalized === "web") {
374
+ return "web";
375
+ }
376
+ if (normalized === "mobile" || normalized === "mobile-android" || normalized === "android") {
377
+ return "mobile-android";
378
+ }
379
+ throw new Error(`Unknown surface: ${input}`);
380
+ }
381
+ function parsePackageManager(input) {
382
+ const normalized = input.trim().toLowerCase();
383
+ if (normalized === "bun" || normalized === "npm" || normalized === "pnpm" || normalized === "yarn") {
384
+ return normalized;
385
+ }
386
+ throw new Error(`Unknown package manager: ${input}. Use bun, npm, pnpm, or yarn.`);
387
+ }
388
+ function dedupeSurfaces(surfaces) {
389
+ return dedupeStringValues(surfaces);
390
+ }
391
+ function dedupeStringValues(values) {
392
+ return [...new Set(values)];
393
+ }
394
+ async function detectPackageManager(targetDir) {
395
+ const lockfiles = [
396
+ { filename: "bun.lock", packageManager: "bun" },
397
+ { filename: "bun.lockb", packageManager: "bun" },
398
+ { filename: "pnpm-lock.yaml", packageManager: "pnpm" },
399
+ { filename: "yarn.lock", packageManager: "yarn" },
400
+ { filename: "package-lock.json", packageManager: "npm" },
401
+ ];
402
+ for (const lockfile of lockfiles) {
403
+ try {
404
+ await access(path.join(targetDir, lockfile.filename));
405
+ return lockfile.packageManager;
406
+ }
407
+ catch {
408
+ // Keep looking for a package manager lockfile.
409
+ }
410
+ }
411
+ const userAgent = process.env.npm_config_user_agent ?? "";
412
+ if (userAgent.startsWith("bun/")) {
413
+ return "bun";
414
+ }
415
+ if (userAgent.startsWith("pnpm/")) {
416
+ return "pnpm";
417
+ }
418
+ if (userAgent.startsWith("yarn/")) {
419
+ return "yarn";
420
+ }
421
+ return "npm";
422
+ }
423
+ async function installDependencies(targetDir, packageManager) {
424
+ p.log.step(`Installing dependencies with ${packageManager}...`);
425
+ await new Promise((resolve, reject) => {
426
+ const child = spawn(packageManager, ["install"], {
427
+ cwd: targetDir,
428
+ stdio: "inherit",
429
+ });
430
+ child.once("error", (error) => {
431
+ reject(new Error(`Could not run ${packageManager} install: ${error.message}. Use --no-install or choose another manager with --package-manager.`));
432
+ });
433
+ child.once("close", (code) => {
434
+ if (code === 0) {
435
+ resolve();
436
+ }
437
+ else {
438
+ reject(new Error(`${packageManager} install exited with code ${code ?? "unknown"}.`));
439
+ }
440
+ });
441
+ });
442
+ }
443
+ function printSummary(absoluteTargetDir, relativeTargetDir, language, surfaces, packageManager, installed) {
444
+ const displayTargetDir = relativeTargetDir === "." || (!relativeTargetDir.startsWith("..") && !path.isAbsolute(relativeTargetDir))
445
+ ? relativeTargetDir
446
+ : absoluteTargetDir;
447
+ console.log("\nInitialized an allwright project.");
448
+ console.log(`- Directory: ${displayTargetDir}`);
449
+ console.log(`- Language: ${language === "ts" ? "TypeScript" : "JavaScript"}`);
450
+ console.log(`- Surfaces: ${surfaces.map(formatSurfaceLabel).join(", ")}`);
451
+ console.log(`- Package manager: ${packageManager}`);
452
+ console.log("\nNext steps:");
453
+ if (displayTargetDir !== ".") {
454
+ console.log(` cd ${displayTargetDir}`);
455
+ }
456
+ if (!installed) {
457
+ console.log(` ${packageManager} install`);
458
+ }
459
+ if (hasSurface(surfaces, "mobile-android")) {
460
+ console.log(" adb devices -l");
461
+ }
462
+ console.log(`${packageManager === "npm" ? " npm test" : ` ${packageManager} test`}`);
463
+ }
464
+ function formatSurfaceLabel(surface) {
465
+ return SURFACE_OPTIONS.find((option) => option.value === surface)?.label ?? surface;
466
+ }
467
+ void main().catch((error) => {
468
+ console.error(error instanceof Error ? error.message : String(error));
469
+ process.exitCode = 1;
470
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "create-allwright",
3
+ "version": "0.0.59",
4
+ "description": "Create a new allwright test project with npm init allwright.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/allwright-dev/allwright.git"
10
+ },
11
+ "homepage": "https://allwright.dev",
12
+ "bugs": {
13
+ "url": "https://github.com/allwright-dev/allwright/issues"
14
+ },
15
+ "keywords": [
16
+ "allwright",
17
+ "create",
18
+ "initializer",
19
+ "npm-init",
20
+ "testing"
21
+ ],
22
+ "bin": {
23
+ "create-allwright": "./dist/index.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json",
34
+ "prepublishOnly": "bun run build"
35
+ },
36
+ "dependencies": {
37
+ "@clack/prompts": "^0.11.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^24.4.0",
41
+ "typescript": "^5.9.2"
42
+ }
43
+ }