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,184 @@
1
+ import assert from "node:assert/strict"
2
+ import { spawn } from "node:child_process"
3
+ import fs from "node:fs"
4
+ import os from "node:os"
5
+ import path from "node:path"
6
+ import test from "node:test"
7
+
8
+ const fixtureConfig = "support/gherkin-fixtures/playwright.config.ts"
9
+
10
+ type FixtureResult = {
11
+ status: number | null
12
+ output: string
13
+ }
14
+
15
+ type FixtureCase = {
16
+ fixture: string
17
+ name: string
18
+ assertResult: (result: FixtureResult) => void
19
+ }
20
+
21
+ const fixtureCases: FixtureCase[] = [
22
+ {
23
+ fixture: "happy-path",
24
+ name: "passes when Gherkin and implementation steps match",
25
+ assertResult(result) {
26
+ assert.equal(result.status, 0, result.output)
27
+ },
28
+ },
29
+ {
30
+ fixture: "step-mismatch",
31
+ name: "fails when an implementation step name differs from Gherkin",
32
+ assertResult(result) {
33
+ assert.notEqual(result.status, 0)
34
+ assert.match(result.output, /Step mismatch/)
35
+ assert.match(result.output, /Expected \\"Given the documented step name\\"/)
36
+ },
37
+ },
38
+ {
39
+ fixture: "missing-step",
40
+ name: "fails when a Gherkin step is not implemented",
41
+ assertResult(result) {
42
+ assert.notEqual(result.status, 0)
43
+ assert.match(result.output, /Missing implemented step\(s\)/)
44
+ assert.match(result.output, /Given the documented step is not implemented/)
45
+ },
46
+ },
47
+ {
48
+ fixture: "extra-step",
49
+ name: "fails when the implementation has an extra step",
50
+ assertResult(result) {
51
+ assert.notEqual(result.status, 0)
52
+ assert.match(result.output, /Unexpected extra step/)
53
+ assert.match(result.output, /Then an extra implementation step runs/)
54
+ },
55
+ },
56
+ {
57
+ fixture: "missing-id",
58
+ name: "fails when a scenario is missing an id tag",
59
+ assertResult(result) {
60
+ assert.notEqual(result.status, 0)
61
+ assert.match(result.output, /missing a required @id: tag/)
62
+ },
63
+ },
64
+ {
65
+ fixture: "duplicate-id",
66
+ name: "fails when scenario ids are duplicated",
67
+ assertResult(result) {
68
+ assert.notEqual(result.status, 0)
69
+ assert.match(result.output, /Duplicate scenario id \\"fixture\.duplicate-id\\"/)
70
+ },
71
+ },
72
+ {
73
+ fixture: "missing-implementation",
74
+ name: "fails when a scenario has no implementation",
75
+ assertResult(result) {
76
+ assert.notEqual(result.status, 0)
77
+ assert.match(result.output, /Missing implementation\(s\)/)
78
+ assert.match(result.output, /fixture\.missing-implementation/)
79
+ },
80
+ },
81
+ {
82
+ fixture: "extra-implementation",
83
+ name: "fails when an implementation has no scenario",
84
+ assertResult(result) {
85
+ assert.notEqual(result.status, 0)
86
+ assert.match(result.output, /Extra implementation\(s\)/)
87
+ assert.match(result.output, /fixture\.unused-implementation/)
88
+ },
89
+ },
90
+ {
91
+ fixture: "scenario-outline",
92
+ name: "fails when a scenario outline is used",
93
+ assertResult(result) {
94
+ assert.notEqual(result.status, 0)
95
+ assert.match(result.output, /uses Scenario Outline, which is not supported/)
96
+ },
97
+ },
98
+ ]
99
+
100
+ test("validates Gherkin adapter fixtures", async () => {
101
+ const results = await Promise.all(
102
+ fixtureCases.map(async (fixtureCase) => ({
103
+ fixtureCase,
104
+ result: await runFixture(fixtureCase.fixture),
105
+ })),
106
+ )
107
+
108
+ for (const { fixtureCase, result } of results) {
109
+ fixtureCase.assertResult(result)
110
+ }
111
+ })
112
+
113
+ async function runFixture(name: string): Promise<FixtureResult> {
114
+ const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "backbone-gherkin-adapter-"))
115
+ const jsonOutput = path.join(outputDir, "playwright-report.json")
116
+
117
+ try {
118
+ const result = await spawnFixture(name, jsonOutput)
119
+
120
+ return {
121
+ status: result.status,
122
+ output: [
123
+ result.error ? `${result.error}` : "",
124
+ result.stdout,
125
+ result.stderr,
126
+ fs.existsSync(jsonOutput) ? fs.readFileSync(jsonOutput, "utf8") : "",
127
+ ].join("\n"),
128
+ }
129
+ } finally {
130
+ fs.rmSync(outputDir, { force: true, recursive: true })
131
+ }
132
+ }
133
+
134
+ type SpawnFixtureResult = {
135
+ error?: Error
136
+ status: number | null
137
+ stderr: string
138
+ stdout: string
139
+ }
140
+
141
+ function spawnFixture(name: string, jsonOutput: string): Promise<SpawnFixtureResult> {
142
+ return new Promise((resolve) => {
143
+ const child = spawn(
144
+ "pnpm",
145
+ [
146
+ "exec",
147
+ "playwright",
148
+ "test",
149
+ `support/gherkin-fixtures/${name}.spec.ts`,
150
+ "--config",
151
+ fixtureConfig,
152
+ "--reporter=json",
153
+ ],
154
+ {
155
+ cwd: new URL("..", import.meta.url),
156
+ env: {
157
+ ...process.env,
158
+ PLAYWRIGHT_JSON_OUTPUT_NAME: jsonOutput,
159
+ },
160
+ },
161
+ )
162
+
163
+ let stderr = ""
164
+ let stdout = ""
165
+
166
+ child.stderr?.setEncoding("utf8")
167
+ child.stderr?.on("data", (chunk: string) => {
168
+ stderr += chunk
169
+ })
170
+
171
+ child.stdout?.setEncoding("utf8")
172
+ child.stdout?.on("data", (chunk: string) => {
173
+ stdout += chunk
174
+ })
175
+
176
+ child.on("error", (error) => {
177
+ resolve({ error, status: null, stderr, stdout })
178
+ })
179
+
180
+ child.on("close", (status) => {
181
+ resolve({ status, stderr, stdout })
182
+ })
183
+ })
184
+ }
@@ -0,0 +1,321 @@
1
+ import { readFileSync } from "node:fs"
2
+ import path from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ import { generateMessages } from "@cucumber/gherkin"
6
+ import {
7
+ IdGenerator,
8
+ SourceMediaType,
9
+ type Envelope,
10
+ type Scenario as GherkinScenario,
11
+ } from "@cucumber/messages"
12
+ import {
13
+ test as baseTest,
14
+ type PlaywrightTestArgs,
15
+ type PlaywrightTestOptions,
16
+ type TestInfo,
17
+ } from "@playwright/test"
18
+
19
+ type ScenarioStep = <T>(name: string, body: () => T | Promise<T>) => Promise<T>
20
+
21
+ export type ScenarioRuntime = {
22
+ readonly id: string
23
+ readonly name: string
24
+ step: ScenarioStep
25
+ }
26
+
27
+ type ScenarioImplementationArgs<ExtraArgs extends object = object> = PlaywrightTestArgs &
28
+ Pick<PlaywrightTestOptions, "baseURL"> & {
29
+ scenario: ScenarioRuntime
30
+ } & ExtraArgs
31
+
32
+ export type ScenarioImplementation<ExtraArgs extends object = object> = (
33
+ args: ScenarioImplementationArgs<ExtraArgs>,
34
+ testInfo: TestInfo,
35
+ ) => void | Promise<void>
36
+
37
+ type ScenarioDefinition = {
38
+ id: string
39
+ name: string
40
+ location: string
41
+ steps: string[]
42
+ }
43
+
44
+ const idTagPrefix = "@id:"
45
+
46
+ type FeatureTest = Pick<typeof baseTest, "describe" | "extend">
47
+
48
+ export const feature = createFeature(baseTest)
49
+
50
+ export function createFeature<ExtraArgs extends object>(test: FeatureTest) {
51
+ return function registerFeature(
52
+ featureFile: string,
53
+ implementations: Record<string, ScenarioImplementation<ExtraArgs>>,
54
+ ): void {
55
+ const featurePath = resolveFeaturePath(featureFile)
56
+ const parsedFeature = parseFeature(featurePath)
57
+
58
+ validateImplementations(featurePath, parsedFeature.scenarios, implementations)
59
+
60
+ test.describe(parsedFeature.name, () => {
61
+ for (const scenarioDefinition of parsedFeature.scenarios) {
62
+ const scenarioTest = test.extend<{ scenario: ScenarioRuntime }>({
63
+ scenario: [
64
+ // oxlint-disable-next-line no-empty-pattern -- Playwright fixtures require object destructuring here.
65
+ async ({}, use) => {
66
+ const scenario = createScenarioRuntime(scenarioDefinition)
67
+ let implementationFailed = false
68
+
69
+ try {
70
+ await use(scenario)
71
+ } catch (error) {
72
+ implementationFailed = true
73
+ throw error
74
+ } finally {
75
+ if (!implementationFailed) {
76
+ scenario.assertComplete()
77
+ }
78
+ }
79
+ },
80
+ { auto: true },
81
+ ],
82
+ })
83
+
84
+ const implementation = implementations[scenarioDefinition.id]
85
+
86
+ if (implementation === undefined) {
87
+ throw new Error(`Missing implementation for scenario id "${scenarioDefinition.id}".`)
88
+ }
89
+
90
+ const runScenario = scenarioTest as unknown as (
91
+ name: string,
92
+ implementation: ScenarioImplementation<ExtraArgs>,
93
+ ) => void
94
+
95
+ runScenario(scenarioDefinition.name, implementation)
96
+ }
97
+ })
98
+ }
99
+ }
100
+
101
+ function parseFeature(featurePath: string): { name: string; scenarios: ScenarioDefinition[] } {
102
+ const source = readFileSync(featurePath, "utf8")
103
+ const envelopes = generateMessages(
104
+ source,
105
+ featurePath,
106
+ SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN,
107
+ {
108
+ includeGherkinDocument: true,
109
+ includePickles: false,
110
+ includeSource: false,
111
+ newId: IdGenerator.incrementing(),
112
+ },
113
+ )
114
+
115
+ assertNoParseErrors(featurePath, envelopes)
116
+
117
+ const gherkinDocument = envelopes.find((envelope) => envelope.gherkinDocument)?.gherkinDocument
118
+ const gherkinFeature = gherkinDocument?.feature
119
+
120
+ if (!gherkinFeature) {
121
+ throw new Error(`Feature file ${featurePath} does not contain a Feature.`)
122
+ }
123
+
124
+ const scenarios: ScenarioDefinition[] = []
125
+
126
+ for (const child of gherkinFeature.children) {
127
+ if (child.background) {
128
+ throw new Error(
129
+ `${featurePath}:${child.background.location.line} uses Background, which is not supported.`,
130
+ )
131
+ }
132
+
133
+ if (child.scenario) {
134
+ scenarios.push(parseScenario(featurePath, child.scenario))
135
+ }
136
+
137
+ if (child.rule) {
138
+ for (const ruleChild of child.rule.children) {
139
+ if (ruleChild.background) {
140
+ throw new Error(
141
+ `${featurePath}:${ruleChild.background.location.line} uses Background, which is not supported.`,
142
+ )
143
+ }
144
+
145
+ if (ruleChild.scenario) {
146
+ scenarios.push(parseScenario(featurePath, ruleChild.scenario))
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ assertUniqueScenarioIds(featurePath, scenarios)
153
+
154
+ return {
155
+ name: gherkinFeature.name,
156
+ scenarios,
157
+ }
158
+ }
159
+
160
+ function parseScenario(featurePath: string, scenario: GherkinScenario): ScenarioDefinition {
161
+ if (scenario.examples.length > 0) {
162
+ throw new Error(
163
+ `${featurePath}:${scenario.location.line} uses Scenario Outline, which is not supported.`,
164
+ )
165
+ }
166
+
167
+ const ids = scenario.tags
168
+ .map((tag) => tag.name)
169
+ .filter((tagName) => tagName.startsWith(idTagPrefix))
170
+ .map((tagName) => tagName.slice(idTagPrefix.length))
171
+
172
+ if (ids.length === 0) {
173
+ throw new Error(
174
+ `${featurePath}:${scenario.location.line} is missing a required ${idTagPrefix} tag.`,
175
+ )
176
+ }
177
+
178
+ if (ids.length > 1) {
179
+ throw new Error(`${featurePath}:${scenario.location.line} has multiple ${idTagPrefix} tags.`)
180
+ }
181
+
182
+ const [id] = ids
183
+
184
+ if (id === undefined) {
185
+ throw new Error(
186
+ `${featurePath}:${scenario.location.line} is missing a required ${idTagPrefix} tag.`,
187
+ )
188
+ }
189
+
190
+ return {
191
+ id,
192
+ name: scenario.name,
193
+ location: `${featurePath}:${scenario.location.line}`,
194
+ steps: scenario.steps.map((step) => `${step.keyword}${step.text}`),
195
+ }
196
+ }
197
+
198
+ function createScenarioRuntime(
199
+ scenarioDefinition: ScenarioDefinition,
200
+ ): ScenarioRuntime & { assertComplete: () => void } {
201
+ let nextStepIndex = 0
202
+
203
+ return {
204
+ id: scenarioDefinition.id,
205
+ name: scenarioDefinition.name,
206
+ async step<T>(name: string, body: () => T | Promise<T>): Promise<T> {
207
+ const expectedStep = scenarioDefinition.steps[nextStepIndex]
208
+
209
+ if (expectedStep === undefined) {
210
+ throw new Error(
211
+ `Unexpected extra step in ${scenarioDefinition.location}: "${name}". ` +
212
+ `The feature only defines ${scenarioDefinition.steps.length} step(s).`,
213
+ )
214
+ }
215
+
216
+ if (name !== expectedStep) {
217
+ throw new Error(
218
+ `Step mismatch in ${scenarioDefinition.location} at step ${nextStepIndex + 1}. ` +
219
+ `Expected "${expectedStep}", but the implementation called "${name}".`,
220
+ )
221
+ }
222
+
223
+ nextStepIndex += 1
224
+ return baseTest.step(name, body)
225
+ },
226
+ assertComplete() {
227
+ const missingSteps = scenarioDefinition.steps.slice(nextStepIndex)
228
+
229
+ if (missingSteps.length > 0) {
230
+ throw new Error(
231
+ `Missing implemented step(s) in ${scenarioDefinition.location}: ${missingSteps
232
+ .map((step) => `"${step}"`)
233
+ .join(", ")}.`,
234
+ )
235
+ }
236
+ },
237
+ }
238
+ }
239
+
240
+ function validateImplementations(
241
+ featurePath: string,
242
+ scenarios: ScenarioDefinition[],
243
+ implementations: Record<string, unknown>,
244
+ ): void {
245
+ const expectedIds = new Set(scenarios.map((scenario) => scenario.id))
246
+ const implementedIds = new Set(Object.keys(implementations))
247
+
248
+ const missingIds = [...expectedIds].filter((id) => !implementedIds.has(id))
249
+ if (missingIds.length > 0) {
250
+ throw new Error(`Missing implementation(s) for ${featurePath}: ${missingIds.join(", ")}.`)
251
+ }
252
+
253
+ const extraIds = [...implementedIds].filter((id) => !expectedIds.has(id))
254
+ if (extraIds.length > 0) {
255
+ throw new Error(`Extra implementation(s) for ${featurePath}: ${extraIds.join(", ")}.`)
256
+ }
257
+ }
258
+
259
+ function assertUniqueScenarioIds(featurePath: string, scenarios: ScenarioDefinition[]): void {
260
+ const seen = new Map<string, ScenarioDefinition>()
261
+
262
+ for (const scenario of scenarios) {
263
+ const existingScenario = seen.get(scenario.id)
264
+
265
+ if (existingScenario) {
266
+ throw new Error(
267
+ `Duplicate scenario id "${scenario.id}" in ${featurePath}: ` +
268
+ `${existingScenario.location} and ${scenario.location}.`,
269
+ )
270
+ }
271
+
272
+ seen.set(scenario.id, scenario)
273
+ }
274
+ }
275
+
276
+ function assertNoParseErrors(featurePath: string, envelopes: readonly Envelope[]): void {
277
+ const parseErrors = envelopes
278
+ .map((envelope) => envelope.parseError)
279
+ .filter((parseError) => parseError !== undefined)
280
+
281
+ if (parseErrors.length > 0) {
282
+ throw new Error(
283
+ `Could not parse ${featurePath}: ${parseErrors
284
+ .map((parseError) => parseError.message)
285
+ .join("; ")}`,
286
+ )
287
+ }
288
+ }
289
+
290
+ function resolveFeaturePath(featureFile: string): string {
291
+ if (path.isAbsolute(featureFile)) {
292
+ return featureFile
293
+ }
294
+
295
+ return path.resolve(path.dirname(resolveCallerPath()), featureFile)
296
+ }
297
+
298
+ function resolveCallerPath(): string {
299
+ const stack = new Error().stack?.split("\n") ?? []
300
+
301
+ for (const line of stack) {
302
+ const candidate = parseStackLinePath(line)
303
+
304
+ if (candidate && !candidate.endsWith("/support/gherkin.ts")) {
305
+ return candidate
306
+ }
307
+ }
308
+
309
+ return path.join(process.cwd(), "playwright.config.ts")
310
+ }
311
+
312
+ function parseStackLinePath(line: string): string | undefined {
313
+ const match = line.match(/\(?((?:file:\/\/)?\/.*?):\d+:\d+\)?$/)
314
+ const rawPath = match?.[1]
315
+
316
+ if (!rawPath) {
317
+ return undefined
318
+ }
319
+
320
+ return rawPath.startsWith("file://") ? fileURLToPath(rawPath) : rawPath
321
+ }