create-backbone-template 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +33 -0
  2. package/bin/create-backbone-template.js +5 -0
  3. package/package.json +30 -0
  4. package/src/create-backbone-template.js +204 -0
  5. package/template/.agents/skills/agent-browser/SKILL.md +55 -0
  6. package/template/.agents/skills/create-plan/SKILL.md +52 -0
  7. package/template/.agents/skills/create-plan/agents/openai.yaml +4 -0
  8. package/template/.agents/skills/create-pr-presentation/SKILL.md +86 -0
  9. package/template/.agents/skills/create-pr-presentation/agents/openai.yaml +4 -0
  10. package/template/.agents/skills/implement-plan/SKILL.md +26 -0
  11. package/template/.agents/skills/implement-plan/agents/openai.yaml +4 -0
  12. package/template/.agents/skills/review-plan/SKILL.md +38 -0
  13. package/template/.agents/skills/review-plan/agents/openai.yaml +4 -0
  14. package/template/.env.schema +30 -0
  15. package/template/.env.test +6 -0
  16. package/template/.oxlintrc.json +67 -0
  17. package/template/.vscode/extensions.json +3 -0
  18. package/template/.vscode/settings.json +23 -0
  19. package/template/AGENTS.md +55 -0
  20. package/template/Cargo.lock +2648 -0
  21. package/template/Cargo.toml +29 -0
  22. package/template/Justfile +140 -0
  23. package/template/README.md +72 -0
  24. package/template/TODO.md +1 -0
  25. package/template/_gitignore +12 -0
  26. package/template/buf.gen.yaml +7 -0
  27. package/template/buf.yaml +10 -0
  28. package/template/client/.oxfmtrc.json +8 -0
  29. package/template/client/.oxlintrc.json +57 -0
  30. package/template/client/README.md +19 -0
  31. package/template/client/_gitignore +5 -0
  32. package/template/client/index.html +12 -0
  33. package/template/client/package.json +47 -0
  34. package/template/client/packages/design-system/package.json +19 -0
  35. package/template/client/packages/design-system/src/index.ts +2 -0
  36. package/template/client/packages/design-system-basic/package.json +18 -0
  37. package/template/client/packages/design-system-basic/src/button.stories.tsx +50 -0
  38. package/template/client/packages/design-system-basic/src/button.tsx +26 -0
  39. package/template/client/packages/design-system-basic/src/empty-state.stories.tsx +18 -0
  40. package/template/client/packages/design-system-basic/src/empty-state.tsx +17 -0
  41. package/template/client/packages/design-system-basic/src/form-field.stories.tsx +15 -0
  42. package/template/client/packages/design-system-basic/src/form-field.tsx +10 -0
  43. package/template/client/packages/design-system-basic/src/form.stories.tsx +27 -0
  44. package/template/client/packages/design-system-basic/src/form.tsx +9 -0
  45. package/template/client/packages/design-system-basic/src/heading.stories.tsx +14 -0
  46. package/template/client/packages/design-system-basic/src/heading.tsx +25 -0
  47. package/template/client/packages/design-system-basic/src/index.tsx +15 -0
  48. package/template/client/packages/design-system-basic/src/inline.stories.tsx +13 -0
  49. package/template/client/packages/design-system-basic/src/inline.tsx +5 -0
  50. package/template/client/packages/design-system-basic/src/layout.stories.tsx +24 -0
  51. package/template/client/packages/design-system-basic/src/layout.tsx +14 -0
  52. package/template/client/packages/design-system-basic/src/loader.stories.tsx +8 -0
  53. package/template/client/packages/design-system-basic/src/loader.tsx +11 -0
  54. package/template/client/packages/design-system-basic/src/navigation.stories.tsx +16 -0
  55. package/template/client/packages/design-system-basic/src/navigation.tsx +18 -0
  56. package/template/client/packages/design-system-basic/src/notice.stories.tsx +13 -0
  57. package/template/client/packages/design-system-basic/src/notice.tsx +5 -0
  58. package/template/client/packages/design-system-basic/src/stack.stories.tsx +17 -0
  59. package/template/client/packages/design-system-basic/src/stack.tsx +5 -0
  60. package/template/client/packages/design-system-basic/src/styles.css +254 -0
  61. package/template/client/packages/design-system-basic/src/text-input.stories.tsx +13 -0
  62. package/template/client/packages/design-system-basic/src/text-input.tsx +5 -0
  63. package/template/client/packages/design-system-basic/src/text.stories.tsx +21 -0
  64. package/template/client/packages/design-system-basic/src/text.tsx +5 -0
  65. package/template/client/packages/design-system-contract/package.json +15 -0
  66. package/template/client/packages/design-system-contract/src/button.ts +10 -0
  67. package/template/client/packages/design-system-contract/src/empty-state.ts +9 -0
  68. package/template/client/packages/design-system-contract/src/form-field.ts +9 -0
  69. package/template/client/packages/design-system-contract/src/form.ts +9 -0
  70. package/template/client/packages/design-system-contract/src/heading.ts +9 -0
  71. package/template/client/packages/design-system-contract/src/index.ts +13 -0
  72. package/template/client/packages/design-system-contract/src/inline.ts +7 -0
  73. package/template/client/packages/design-system-contract/src/layout.ts +8 -0
  74. package/template/client/packages/design-system-contract/src/loader.ts +7 -0
  75. package/template/client/packages/design-system-contract/src/navigation.ts +13 -0
  76. package/template/client/packages/design-system-contract/src/notice.ts +8 -0
  77. package/template/client/packages/design-system-contract/src/stack.ts +8 -0
  78. package/template/client/packages/design-system-contract/src/text-input.ts +5 -0
  79. package/template/client/packages/design-system-contract/src/text.ts +9 -0
  80. package/template/client/packages/design-system-lint/fixtures/invalid/external-ui-import.tsx +5 -0
  81. package/template/client/packages/design-system-lint/fixtures/invalid/raw-dom-jsx.tsx +3 -0
  82. package/template/client/packages/design-system-lint/fixtures/invalid/two-violations.tsx +7 -0
  83. package/template/client/packages/design-system-lint/fixtures/valid/design-system-only.tsx +13 -0
  84. package/template/client/packages/design-system-lint/package.json +23 -0
  85. package/template/client/packages/design-system-lint/src/check-design-system-architecture.ts +22 -0
  86. package/template/client/packages/design-system-lint/src/design-system-architecture.ts +286 -0
  87. package/template/client/packages/design-system-lint/src/oxlint-plugin.ts +11 -0
  88. package/template/client/packages/design-system-lint/src/page-architecture.ts +382 -0
  89. package/template/client/packages/design-system-lint/src/rules.ts +111 -0
  90. package/template/client/packages/design-system-lint/test/design-system-architecture.test.ts +243 -0
  91. package/template/client/packages/design-system-lint/test/oxlint-fixtures.test.ts +159 -0
  92. package/template/client/packages/design-system-lint/test/page-architecture.test.ts +175 -0
  93. package/template/client/packages/design-system-lint/test/rules.test.ts +65 -0
  94. package/template/client/packages/design-system-lint/tsconfig.json +29 -0
  95. package/template/client/src/App.tsx +77 -0
  96. package/template/client/src/design-system-components.test.tsx +75 -0
  97. package/template/client/src/gen/helloworld/v1/helloworld_pb.ts +63 -0
  98. package/template/client/src/main.tsx +18 -0
  99. package/template/client/src/pages/hello/hello-page.stories.tsx +20 -0
  100. package/template/client/src/pages/hello/hello-page.test.tsx +90 -0
  101. package/template/client/src/pages/hello/hello-page.tsx +126 -0
  102. package/template/client/src/pages/page.ts +20 -0
  103. package/template/client/src/testing/create-preview-events.test.ts +36 -0
  104. package/template/client/src/testing/create-preview-events.ts +30 -0
  105. package/template/client/src/vite-env.d.ts +1 -0
  106. package/template/client/tsconfig.json +32 -0
  107. package/template/client/vite.config.ts +21 -0
  108. package/template/client/vite.ladle.config.ts +5 -0
  109. package/template/e2e/.gherkin-lintrc +20 -0
  110. package/template/e2e/.oxfmtrc.json +15 -0
  111. package/template/e2e/.oxlintrc.json +37 -0
  112. package/template/e2e/_gitignore +4 -0
  113. package/template/e2e/features/helloworld.feature +10 -0
  114. package/template/e2e/package.json +42 -0
  115. package/template/e2e/playwright.config.ts +16 -0
  116. package/template/e2e/support/app-gherkin.ts +4 -0
  117. package/template/e2e/support/fixtures.ts +236 -0
  118. package/template/e2e/support/gherkin-fixtures/duplicate-id.feature +9 -0
  119. package/template/e2e/support/gherkin-fixtures/duplicate-id.spec.ts +7 -0
  120. package/template/e2e/support/gherkin-fixtures/extra-implementation.spec.ts +7 -0
  121. package/template/e2e/support/gherkin-fixtures/extra-step.spec.ts +10 -0
  122. package/template/e2e/support/gherkin-fixtures/happy-path.spec.ts +4 -0
  123. package/template/e2e/support/gherkin-fixtures/missing-id.feature +4 -0
  124. package/template/e2e/support/gherkin-fixtures/missing-id.spec.ts +7 -0
  125. package/template/e2e/support/gherkin-fixtures/missing-implementation.spec.ts +7 -0
  126. package/template/e2e/support/gherkin-fixtures/missing-step.spec.ts +7 -0
  127. package/template/e2e/support/gherkin-fixtures/playwright.config.ts +7 -0
  128. package/template/e2e/support/gherkin-fixtures/scenario-outline.feature +9 -0
  129. package/template/e2e/support/gherkin-fixtures/scenario-outline.spec.ts +7 -0
  130. package/template/e2e/support/gherkin-fixtures/step-mismatch.spec.ts +9 -0
  131. package/template/e2e/support/gherkin-fixtures/valid-implementations.ts +23 -0
  132. package/template/e2e/support/gherkin-fixtures/valid-scenarios.feature +26 -0
  133. package/template/e2e/support/gherkin.test.ts +184 -0
  134. package/template/e2e/support/gherkin.ts +321 -0
  135. package/template/e2e/support/oxlint-plugin.test.ts +328 -0
  136. package/template/e2e/support/oxlint-plugin.ts +485 -0
  137. package/template/e2e/tests/helloworld.spec.ts +39 -0
  138. package/template/e2e/tsconfig.json +26 -0
  139. package/template/e2e/tsconfig.oxlint-plugin.json +12 -0
  140. package/template/package.json +9 -0
  141. package/template/pnpm-lock.yaml +10723 -0
  142. package/template/pnpm-workspace.yaml +8 -0
  143. package/template/pr-slide/README.md +95 -0
  144. package/template/pr-slide/package.json +23 -0
  145. package/template/pr-slide/src/cli.js +262 -0
  146. package/template/pr-slide/src/generate-pr-deck.js +833 -0
  147. package/template/pr-slide/src/git-context.js +91 -0
  148. package/template/pr-slide/src/presentation-paths.js +9 -0
  149. package/template/pr-slide/src/presentations.js +53 -0
  150. package/template/pr-slide/test/generate-pr-deck.test.js +118 -0
  151. package/template/pr-slide/test/presentation-paths.test.js +14 -0
  152. package/template/pr-slide/test/presentations.test.js +50 -0
  153. package/template/proto/helloworld/v1/helloworld.proto +15 -0
  154. package/template/scripts/run-e2e.sh +10 -0
  155. package/template/server/Cargo.toml +26 -0
  156. package/template/server/build.rs +9 -0
  157. package/template/server/dylint/backbone_server_lints/.cargo/config.toml +6 -0
  158. package/template/server/dylint/backbone_server_lints/Cargo.lock +1581 -0
  159. package/template/server/dylint/backbone_server_lints/Cargo.toml +21 -0
  160. package/template/server/dylint/backbone_server_lints/README.md +5 -0
  161. package/template/server/dylint/backbone_server_lints/_gitignore +1 -0
  162. package/template/server/dylint/backbone_server_lints/rust-toolchain +3 -0
  163. package/template/server/dylint/backbone_server_lints/src/lib.rs +612 -0
  164. package/template/server/dylint/backbone_server_lints/ui/lib.rs +4 -0
  165. package/template/server/dylint/backbone_server_lints/ui/lib.stderr +10 -0
  166. package/template/server/dylint/backbone_server_lints/ui/long_file.rs +303 -0
  167. package/template/server/dylint/backbone_server_lints/ui/long_file.stderr +6 -0
  168. package/template/server/dylint/backbone_server_lints/ui/main.rs +59 -0
  169. package/template/server/dylint/backbone_server_lints/ui/main.stderr +85 -0
  170. package/template/server/migrations/20260520120000_create_projects.sql +12 -0
  171. package/template/server/migrations/20260524160000_create_hello_world_inputs.sql +12 -0
  172. package/template/server/src/config.rs +27 -0
  173. package/template/server/src/db/hello_world.rs +34 -0
  174. package/template/server/src/db/hello_world_tests.rs +11 -0
  175. package/template/server/src/db/mod.rs +39 -0
  176. package/template/server/src/lib.rs +10 -0
  177. package/template/server/src/main.rs +43 -0
  178. package/template/server/src/rpc/greeter/mod.rs +31 -0
  179. package/template/server/src/rpc/greeter/say_hello.rs +27 -0
  180. package/template/server/src/rpc/mod.rs +8 -0
  181. package/template/server/src/state.rs +13 -0
  182. package/template/skills-lock.json +11 -0
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["eslint", "typescript", "unicorn", "oxc", "import", "promise", "node"],
4
+ "jsPlugins": ["./support/dist/oxlint-plugin.js"],
5
+ "categories": {
6
+ "correctness": "deny",
7
+ "suspicious": "deny",
8
+ "perf": "deny",
9
+ "pedantic": "off",
10
+ "style": "off",
11
+ "restriction": "off",
12
+ "nursery": "off"
13
+ },
14
+ "options": {
15
+ "denyWarnings": true,
16
+ "reportUnusedDisableDirectives": "deny",
17
+ "typeAware": true
18
+ },
19
+ "rules": {
20
+ "typescript/no-unsafe-type-assertion": "off",
21
+ "unicorn/no-array-sort": "off"
22
+ },
23
+ "overrides": [
24
+ {
25
+ "files": ["**/*.test.ts"],
26
+ "rules": {
27
+ "typescript/no-floating-promises": "off"
28
+ }
29
+ },
30
+ {
31
+ "files": ["tests/**/*.spec.ts"],
32
+ "rules": {
33
+ "backbone-e2e/valid-gherkin-feature": "error"
34
+ }
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ playwright-report
3
+ test-results
4
+
@@ -0,0 +1,10 @@
1
+ Feature: Hello page
2
+
3
+ @id:hello.say-hello
4
+ Scenario: Say hello through the UI
5
+ Given the Rust server is healthy
6
+ And the visitor is on the hello page
7
+ Then they see the default hello message
8
+ When they ask to greet Playwright
9
+ Then they see the Playwright greeting
10
+ And the Playwright input is saved
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "backbone-e2e",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "pnpm run test:support && playwright test",
8
+ "test:prepared": "pnpm run test:support:prepared && playwright test",
9
+ "test:browser": "playwright test",
10
+ "test:support": "pnpm run test:adapter && pnpm run test:oxlint-plugin",
11
+ "test:support:prepared": "pnpm run test:adapter && pnpm run test:oxlint-plugin:prepared",
12
+ "test:adapter": "node --disable-warning=ExperimentalWarning --experimental-strip-types --test support/gherkin.test.ts",
13
+ "test:oxlint-plugin": "pnpm run build:oxlint-plugin && node --disable-warning=ExperimentalWarning --experimental-strip-types --test support/oxlint-plugin.test.ts",
14
+ "test:oxlint-plugin:prepared": "node --disable-warning=ExperimentalWarning --experimental-strip-types --test support/oxlint-plugin.test.ts",
15
+ "build:oxlint-plugin": "tsc --project tsconfig.oxlint-plugin.json",
16
+ "format": "oxfmt .",
17
+ "format:check": "oxfmt --check .",
18
+ "lint": "pnpm run format:check && pnpm run lint:code && pnpm run lint:features",
19
+ "lint:prepared": "pnpm run format:check && pnpm run lint:code:prepared && pnpm run lint:features",
20
+ "lint:code": "pnpm run build:oxlint-plugin && oxlint --config .oxlintrc.json playwright.config.ts support/app-gherkin.ts support/fixtures.ts support/gherkin.ts support/gherkin.test.ts support/oxlint-plugin.ts support/oxlint-plugin.test.ts tests",
21
+ "lint:code:prepared": "oxlint --config .oxlintrc.json playwright.config.ts support/app-gherkin.ts support/fixtures.ts support/gherkin.ts support/gherkin.test.ts support/oxlint-plugin.ts support/oxlint-plugin.test.ts tests",
22
+ "lint:features": "gherkin-lint -c .gherkin-lintrc \"features/**/*.feature\"",
23
+ "typecheck": "tsc --noEmit",
24
+ "test:debug": "playwright test --max-failures=1 --headed --debug",
25
+ "test:headed": "playwright test --headed",
26
+ "test:ui": "playwright test --ui",
27
+ "report": "playwright show-report"
28
+ },
29
+ "devDependencies": {
30
+ "@cucumber/gherkin": "^39.1.0",
31
+ "@cucumber/messages": "^32.3.1",
32
+ "@playwright/test": "latest",
33
+ "@types/better-sqlite3": "^7.6.13",
34
+ "@types/node": "^25.9.1",
35
+ "better-sqlite3": "^12.10.0",
36
+ "gherkin-lint": "^4.2.4",
37
+ "oxfmt": "^0.51.0",
38
+ "oxlint": "^1.66.0",
39
+ "oxlint-tsgolint": "^0.22.1",
40
+ "typescript": "^6.0.3"
41
+ }
42
+ }
@@ -0,0 +1,16 @@
1
+ import { defineConfig, devices } from "@playwright/test"
2
+
3
+ export default defineConfig({
4
+ testDir: "./tests",
5
+ fullyParallel: true,
6
+ reporter: [["list"], ["html", { open: "never" }]],
7
+ use: {
8
+ trace: "on-first-retry",
9
+ },
10
+ projects: [
11
+ {
12
+ name: "chromium",
13
+ use: { ...devices["Desktop Chrome"] },
14
+ },
15
+ ],
16
+ })
@@ -0,0 +1,4 @@
1
+ import { test, type AppFixtures } from "./fixtures"
2
+ import { createFeature } from "./gherkin"
3
+
4
+ export const feature = createFeature<AppFixtures>(test)
@@ -0,0 +1,236 @@
1
+ import { spawn, type ChildProcess } from "node:child_process"
2
+ import { mkdirSync, rmSync } from "node:fs"
3
+ import net from "node:net"
4
+ import os from "node:os"
5
+ import path from "node:path"
6
+
7
+ import { test as base } from "@playwright/test"
8
+ import Database from "better-sqlite3"
9
+
10
+ type AppWorker = {
11
+ clientUrl: string
12
+ db: E2eDatabase
13
+ serverUrl: string
14
+ stop(): Promise<void>
15
+ }
16
+
17
+ type WorkerFixtures = {
18
+ app: AppWorker
19
+ }
20
+
21
+ export type AppFixtures = {
22
+ db: E2eDatabase
23
+ serverUrl: string
24
+ }
25
+
26
+ export const test = base.extend<AppFixtures, WorkerFixtures>({
27
+ app: [
28
+ // oxlint-disable-next-line no-empty-pattern -- Playwright worker fixtures require object destructuring here.
29
+ async ({}, use, workerInfo) => {
30
+ const app = await startAppWorker(workerInfo.workerIndex)
31
+
32
+ try {
33
+ await use(app)
34
+ } finally {
35
+ await app.stop()
36
+ }
37
+ },
38
+ { scope: "worker" },
39
+ ],
40
+ baseURL: async ({ app }, use) => {
41
+ await use(app.clientUrl)
42
+ },
43
+ db: async ({ app }, use) => {
44
+ await use(app.db)
45
+ },
46
+ serverUrl: async ({ app }, use) => {
47
+ await use(app.serverUrl)
48
+ },
49
+ })
50
+
51
+ export class E2eDatabase {
52
+ readonly #database: Database.Database
53
+
54
+ constructor(databasePath: string) {
55
+ this.#database = new Database(databasePath)
56
+ }
57
+
58
+ close(): void {
59
+ this.#database.close()
60
+ }
61
+
62
+ reset(): void {
63
+ this.#database.prepare("DELETE FROM hello_world_inputs").run()
64
+ this.#database.prepare("DELETE FROM projects").run()
65
+ }
66
+
67
+ listHelloWorldInputs(): string[] {
68
+ return this.#database
69
+ .prepare("SELECT input FROM hello_world_inputs ORDER BY created_at, id")
70
+ .all()
71
+ .map(readHelloWorldInput)
72
+ }
73
+ }
74
+
75
+ async function startAppWorker(workerIndex: number): Promise<AppWorker> {
76
+ const [serverPort, clientPort] = await Promise.all([freePort(), freePort()])
77
+ const rootDir = path.resolve("..")
78
+ const workerDir = path.join(os.tmpdir(), `backbone-e2e-${process.pid}-${workerIndex}`)
79
+ const databasePath = path.join(workerDir, "backbone.sqlite")
80
+ const databaseUrl = `sqlite://${databasePath}?mode=rwc`
81
+
82
+ rmSync(workerDir, { force: true, recursive: true })
83
+ mkdirSync(workerDir, { recursive: true })
84
+
85
+ const serverUrl = `http://127.0.0.1:${serverPort}`
86
+ const clientUrl = `http://127.0.0.1:${clientPort}`
87
+ const env = {
88
+ ...process.env,
89
+ APP_ENV: "test",
90
+ DATABASE_URL: databaseUrl,
91
+ SERVER_HOST: "127.0.0.1",
92
+ SERVER_PORT: String(serverPort),
93
+ VITE_DEV_SERVER_PORT: String(clientPort),
94
+ VITE_SERVER_URL: serverUrl,
95
+ }
96
+ const server = spawnProcess("cargo", ["run", "-p", "server"], rootDir, env)
97
+
98
+ await waitForUrl(`${serverUrl}/health`, [server], "server")
99
+
100
+ const client = spawnProcess(
101
+ "pnpm",
102
+ [
103
+ "--filter",
104
+ "backbone-client",
105
+ "exec",
106
+ "vite",
107
+ "--host",
108
+ "127.0.0.1",
109
+ "--port",
110
+ String(clientPort),
111
+ "--strictPort",
112
+ ],
113
+ rootDir,
114
+ env,
115
+ )
116
+
117
+ await waitForUrl(clientUrl, [server, client], "client")
118
+
119
+ const db = new E2eDatabase(databasePath)
120
+
121
+ return {
122
+ clientUrl,
123
+ db,
124
+ serverUrl,
125
+ async stop() {
126
+ db.close()
127
+ await stopProcess(client)
128
+ await stopProcess(server)
129
+ rmSync(workerDir, { force: true, recursive: true })
130
+ },
131
+ }
132
+ }
133
+
134
+ function spawnProcess(
135
+ command: string,
136
+ args: string[],
137
+ cwd: string,
138
+ env: NodeJS.ProcessEnv,
139
+ ): ChildProcess {
140
+ const child = spawn(command, args, {
141
+ cwd,
142
+ env,
143
+ stdio: ["ignore", "pipe", "pipe"],
144
+ })
145
+
146
+ child.stdout?.on("data", (chunk: Buffer) => {
147
+ process.stdout.write(chunk)
148
+ })
149
+ child.stderr?.on("data", (chunk: Buffer) => {
150
+ process.stderr.write(chunk)
151
+ })
152
+
153
+ return child
154
+ }
155
+
156
+ async function stopProcess(child: ChildProcess): Promise<void> {
157
+ if (child.exitCode !== null || child.signalCode !== null) {
158
+ return
159
+ }
160
+
161
+ child.kill("SIGTERM")
162
+
163
+ await new Promise<void>((resolve) => {
164
+ child.once("exit", () => {
165
+ resolve()
166
+ })
167
+ setTimeout(() => {
168
+ if (child.exitCode === null && child.signalCode === null) {
169
+ child.kill("SIGKILL")
170
+ }
171
+ resolve()
172
+ }, 3000)
173
+ })
174
+ }
175
+
176
+ async function waitForUrl(url: string, processes: ChildProcess[], name: string): Promise<void> {
177
+ for (let attempt = 0; attempt < 150; attempt += 1) {
178
+ for (const child of processes) {
179
+ if (child.exitCode !== null) {
180
+ throw new Error(`${name} dependency exited with code ${child.exitCode}`)
181
+ }
182
+ }
183
+
184
+ try {
185
+ // oxlint-disable-next-line no-await-in-loop -- Readiness polling is intentionally sequential.
186
+ const response = await fetch(url)
187
+
188
+ if (response.ok) {
189
+ return
190
+ }
191
+ } catch {
192
+ // Keep polling until the process is ready or exits.
193
+ }
194
+
195
+ // oxlint-disable-next-line no-await-in-loop -- Polling attempts need a delay between checks.
196
+ await delay(200)
197
+ }
198
+
199
+ throw new Error(`Timed out waiting for ${name} at ${url}`)
200
+ }
201
+
202
+ async function freePort(): Promise<number> {
203
+ return new Promise((resolve, reject) => {
204
+ const server = net.createServer()
205
+
206
+ server.once("error", reject)
207
+ server.listen(0, "127.0.0.1", () => {
208
+ const address = server.address()
209
+
210
+ if (typeof address === "object" && address !== null) {
211
+ const port = address.port
212
+ server.close(() => {
213
+ resolve(port)
214
+ })
215
+ } else {
216
+ server.close(() => {
217
+ reject(new Error("Could not allocate a TCP port"))
218
+ })
219
+ }
220
+ })
221
+ })
222
+ }
223
+
224
+ function readHelloWorldInput(row: unknown): string {
225
+ if (typeof row === "object" && row !== null && "input" in row && typeof row.input === "string") {
226
+ return row.input
227
+ }
228
+
229
+ throw new Error("Unexpected hello_world_inputs row shape")
230
+ }
231
+
232
+ async function delay(ms: number): Promise<void> {
233
+ await new Promise((resolve) => {
234
+ setTimeout(resolve, ms)
235
+ })
236
+ }
@@ -0,0 +1,9 @@
1
+ Feature: Duplicate id fixture
2
+
3
+ @id:fixture.duplicate-id
4
+ Scenario: First scenario
5
+ Given the first scenario uses the id
6
+
7
+ @id:fixture.duplicate-id
8
+ Scenario: Second scenario
9
+ Given the second scenario reuses the id
@@ -0,0 +1,7 @@
1
+ import { feature } from "../gherkin"
2
+
3
+ feature("./duplicate-id.feature", {
4
+ "fixture.duplicate-id": async ({ scenario }) => {
5
+ await scenario.step("Given the first scenario uses the id", async () => {})
6
+ },
7
+ })
@@ -0,0 +1,7 @@
1
+ import { feature } from "../gherkin"
2
+ import { validImplementations } from "./valid-implementations"
3
+
4
+ feature("./valid-scenarios.feature", {
5
+ ...validImplementations,
6
+ "fixture.unused-implementation": async () => {},
7
+ })
@@ -0,0 +1,10 @@
1
+ import { feature } from "../gherkin"
2
+ import { validImplementations } from "./valid-implementations"
3
+
4
+ feature("./valid-scenarios.feature", {
5
+ ...validImplementations,
6
+ "fixture.extra-step": async ({ scenario }) => {
7
+ await scenario.step("Given the only documented step", async () => {})
8
+ await scenario.step("Then an extra implementation step runs", async () => {})
9
+ },
10
+ })
@@ -0,0 +1,4 @@
1
+ import { feature } from "../gherkin"
2
+ import { validImplementations } from "./valid-implementations"
3
+
4
+ feature("./valid-scenarios.feature", validImplementations)
@@ -0,0 +1,4 @@
1
+ Feature: Missing id fixture
2
+
3
+ Scenario: Missing id fails
4
+ Given the scenario has no id tag
@@ -0,0 +1,7 @@
1
+ import { feature } from "../gherkin"
2
+
3
+ feature("./missing-id.feature", {
4
+ "fixture.missing-id": async ({ scenario }) => {
5
+ await scenario.step("Given the scenario has no id tag", async () => {})
6
+ },
7
+ })
@@ -0,0 +1,7 @@
1
+ import { feature } from "../gherkin"
2
+ import { validImplementations } from "./valid-implementations"
3
+
4
+ const { "fixture.missing-implementation": _missingImplementation, ...implementations } =
5
+ validImplementations
6
+
7
+ feature("./valid-scenarios.feature", implementations)
@@ -0,0 +1,7 @@
1
+ import { feature } from "../gherkin"
2
+ import { validImplementations } from "./valid-implementations"
3
+
4
+ feature("./valid-scenarios.feature", {
5
+ ...validImplementations,
6
+ "fixture.missing-step": async () => {},
7
+ })
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "@playwright/test"
2
+
3
+ export default defineConfig({
4
+ reporter: "line",
5
+ testDir: ".",
6
+ workers: 1,
7
+ })
@@ -0,0 +1,9 @@
1
+ Feature: Scenario outline fixture
2
+
3
+ @id:fixture.scenario-outline
4
+ Scenario Outline: Scenario outlines fail
5
+ Given the value is <value>
6
+
7
+ Examples:
8
+ | value |
9
+ | one |
@@ -0,0 +1,7 @@
1
+ import { feature } from "../gherkin"
2
+
3
+ feature("./scenario-outline.feature", {
4
+ "fixture.scenario-outline": async ({ scenario }) => {
5
+ await scenario.step("Given the value is one", async () => {})
6
+ },
7
+ })
@@ -0,0 +1,9 @@
1
+ import { feature } from "../gherkin"
2
+ import { validImplementations } from "./valid-implementations"
3
+
4
+ feature("./valid-scenarios.feature", {
5
+ ...validImplementations,
6
+ "fixture.step-mismatch": async ({ scenario }) => {
7
+ await scenario.step("Given a different step name", async () => {})
8
+ },
9
+ })
@@ -0,0 +1,23 @@
1
+ import type { ScenarioImplementation } from "../gherkin"
2
+
3
+ export const validImplementations: Record<string, ScenarioImplementation> = {
4
+ "fixture.happy-path": async ({ scenario }) => {
5
+ await scenario.step("Given the first step matches", async () => {})
6
+ await scenario.step("Then the second step matches", async () => {})
7
+ },
8
+ "fixture.step-mismatch": async ({ scenario }) => {
9
+ await scenario.step("Given the documented step name", async () => {})
10
+ },
11
+ "fixture.missing-step": async ({ scenario }) => {
12
+ await scenario.step("Given the documented step is not implemented", async () => {})
13
+ },
14
+ "fixture.extra-step": async ({ scenario }) => {
15
+ await scenario.step("Given the only documented step", async () => {})
16
+ },
17
+ "fixture.missing-implementation": async ({ scenario }) => {
18
+ await scenario.step("Given the scenario is not implemented", async () => {})
19
+ },
20
+ "fixture.extra-implementation": async ({ scenario }) => {
21
+ await scenario.step("Given the feature has one implementation", async () => {})
22
+ },
23
+ }
@@ -0,0 +1,26 @@
1
+ Feature: Valid adapter fixture scenarios
2
+
3
+ @id:fixture.happy-path
4
+ Scenario: Matching steps pass
5
+ Given the first step matches
6
+ Then the second step matches
7
+
8
+ @id:fixture.step-mismatch
9
+ Scenario: Renamed implementation step fails
10
+ Given the documented step name
11
+
12
+ @id:fixture.missing-step
13
+ Scenario: Missing implementation step fails
14
+ Given the documented step is not implemented
15
+
16
+ @id:fixture.extra-step
17
+ Scenario: Extra implementation step fails
18
+ Given the only documented step
19
+
20
+ @id:fixture.missing-implementation
21
+ Scenario: Missing implementation fails
22
+ Given the scenario is not implemented
23
+
24
+ @id:fixture.extra-implementation
25
+ Scenario: Extra implementation fails
26
+ Given the feature has one implementation