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,485 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import path from "node:path"
3
+
4
+ import { generateMessages } from "@cucumber/gherkin"
5
+ import { IdGenerator, SourceMediaType, type Scenario as GherkinScenario } from "@cucumber/messages"
6
+
7
+ type RuleContext = {
8
+ cwd: string
9
+ filename: string
10
+ report(report: { message: string; node: AstNode }): void
11
+ sourceCode?: {
12
+ ast: unknown
13
+ }
14
+ }
15
+
16
+ type RuleModule = {
17
+ meta: {
18
+ docs: {
19
+ description: string
20
+ }
21
+ type: "problem"
22
+ }
23
+ create(context: RuleContext): Record<"CallExpression", (node: AstNode) => void>
24
+ }
25
+
26
+ type AstNode = {
27
+ type: string
28
+ [key: string]: unknown
29
+ }
30
+
31
+ type FeatureScenario = {
32
+ id: string
33
+ location: string
34
+ name: string
35
+ steps: string[]
36
+ }
37
+
38
+ type Implementation = {
39
+ id: string
40
+ keyNode: AstNode
41
+ steps: ImplementedStep[]
42
+ }
43
+
44
+ type ImplementedStep = {
45
+ name: string
46
+ node: AstNode
47
+ }
48
+
49
+ const idTagPrefix = "@id:"
50
+
51
+ const validGherkinFeature: RuleModule = {
52
+ meta: {
53
+ type: "problem",
54
+ docs: {
55
+ description:
56
+ "Validate that Playwright feature(...) implementations match their Gherkin files.",
57
+ },
58
+ },
59
+ create(context) {
60
+ return {
61
+ CallExpression(node) {
62
+ if (!isIdentifier(node["callee"], "feature")) {
63
+ return
64
+ }
65
+
66
+ lintFeatureCall(context, node)
67
+ },
68
+ }
69
+ },
70
+ }
71
+
72
+ export default {
73
+ meta: {
74
+ name: "backbone-e2e",
75
+ },
76
+ rules: {
77
+ "valid-gherkin-feature": validGherkinFeature,
78
+ },
79
+ }
80
+
81
+ function lintFeatureCall(context: RuleContext, node: AstNode): void {
82
+ const [featurePathArgument, implementationsArgument] =
83
+ (node["arguments"] as unknown[] | undefined) ?? []
84
+
85
+ if (!isStringLiteral(featurePathArgument)) {
86
+ report(context, node, "feature(...) must use a string literal feature path.")
87
+ return
88
+ }
89
+
90
+ if (!isObjectExpression(implementationsArgument)) {
91
+ report(context, node, "feature(...) must use an object literal implementation map.")
92
+ return
93
+ }
94
+
95
+ const featurePath = path.resolve(path.dirname(context.filename), featurePathArgument.value)
96
+
97
+ if (!existsSync(featurePath)) {
98
+ report(context, featurePathArgument, `Feature file does not exist: ${featurePath}.`)
99
+ return
100
+ }
101
+
102
+ const scenarios = parseFeatureFile(featurePath, context, featurePathArgument)
103
+ const implementations = parseImplementationMap(context, implementationsArgument)
104
+
105
+ lintImplementationCoverage(context, scenarios, implementations)
106
+ }
107
+
108
+ function parseFeatureFile(
109
+ featurePath: string,
110
+ context: RuleContext,
111
+ reportNode: AstNode,
112
+ ): FeatureScenario[] {
113
+ const source = readFileSync(featurePath, "utf8")
114
+ const envelopes = generateMessages(
115
+ source,
116
+ featurePath,
117
+ SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN,
118
+ {
119
+ includeGherkinDocument: true,
120
+ includePickles: false,
121
+ includeSource: false,
122
+ newId: IdGenerator.incrementing(),
123
+ },
124
+ )
125
+
126
+ for (const envelope of envelopes) {
127
+ if (envelope.parseError) {
128
+ report(context, reportNode, envelope.parseError.message)
129
+ }
130
+ }
131
+
132
+ const feature = envelopes.find((envelope) => envelope.gherkinDocument)?.gherkinDocument?.feature
133
+
134
+ if (!feature) {
135
+ return []
136
+ }
137
+
138
+ const scenarios: FeatureScenario[] = []
139
+
140
+ for (const child of feature.children) {
141
+ if (child.scenario) {
142
+ scenarios.push(parseScenario(featurePath, child.scenario, context, reportNode))
143
+ }
144
+
145
+ if (child.rule) {
146
+ for (const ruleChild of child.rule.children) {
147
+ if (ruleChild.scenario) {
148
+ scenarios.push(parseScenario(featurePath, ruleChild.scenario, context, reportNode))
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ lintDuplicateScenarioIds(context, reportNode, scenarios)
155
+
156
+ return scenarios
157
+ }
158
+
159
+ function parseScenario(
160
+ featurePath: string,
161
+ scenario: GherkinScenario,
162
+ context: RuleContext,
163
+ reportNode: AstNode,
164
+ ): FeatureScenario {
165
+ const idTags = scenario.tags
166
+ .map((tag) => tag.name)
167
+ .filter((tagName) => tagName.startsWith(idTagPrefix))
168
+ const location = `${featurePath}:${scenario.location.line}`
169
+
170
+ if (scenario.keyword === "Scenario Outline") {
171
+ report(context, reportNode, `${location} uses Scenario Outline, which is not supported.`)
172
+ }
173
+
174
+ if (idTags.length !== 1) {
175
+ report(
176
+ context,
177
+ reportNode,
178
+ `${location} must have exactly one ${idTagPrefix} tag, found ${idTags.length}.`,
179
+ )
180
+ }
181
+
182
+ return {
183
+ id: idTags[0]?.slice(idTagPrefix.length) ?? "",
184
+ location,
185
+ name: scenario.name,
186
+ steps: scenario.steps.map((step) => `${step.keyword}${step.text}`),
187
+ }
188
+ }
189
+
190
+ function lintDuplicateScenarioIds(
191
+ context: RuleContext,
192
+ reportNode: AstNode,
193
+ scenarios: FeatureScenario[],
194
+ ): void {
195
+ const scenariosById = new Map<string, FeatureScenario[]>()
196
+
197
+ for (const scenario of scenarios) {
198
+ if (scenario.id === "") {
199
+ continue
200
+ }
201
+
202
+ const scenariosWithId = scenariosById.get(scenario.id) ?? []
203
+ scenariosWithId.push(scenario)
204
+ scenariosById.set(scenario.id, scenariosWithId)
205
+ }
206
+
207
+ for (const [id, scenariosWithId] of scenariosById) {
208
+ if (scenariosWithId.length <= 1) {
209
+ continue
210
+ }
211
+
212
+ const firstScenario = scenariosWithId[0]
213
+ const duplicateScenario = scenariosWithId[1]
214
+
215
+ if (firstScenario === undefined || duplicateScenario === undefined) {
216
+ continue
217
+ }
218
+
219
+ report(
220
+ context,
221
+ reportNode,
222
+ `Duplicate scenario id "${id}" at ${duplicateScenario.location}; first used at ${firstScenario.location}.`,
223
+ )
224
+ }
225
+ }
226
+
227
+ function parseImplementationMap(
228
+ context: RuleContext,
229
+ implementationsArgument: AstNode,
230
+ ): Implementation[] {
231
+ const implementations: Implementation[] = []
232
+
233
+ for (const property of (implementationsArgument["properties"] as unknown[] | undefined) ?? []) {
234
+ if (!isProperty(property)) {
235
+ report(
236
+ context,
237
+ implementationsArgument,
238
+ "implementation map entries must be property assignments.",
239
+ )
240
+ continue
241
+ }
242
+
243
+ const id = propertyNameText(property.key)
244
+
245
+ if (!id) {
246
+ report(context, property.key, "implementation ids must be string literal property names.")
247
+ continue
248
+ }
249
+
250
+ if (!isFunctionExpression(property.value)) {
251
+ report(context, property.value, `implementation "${id}" must be a function.`)
252
+ continue
253
+ }
254
+
255
+ implementations.push({
256
+ id,
257
+ keyNode: property.key,
258
+ steps: readScenarioSteps(context, property.value),
259
+ })
260
+ }
261
+
262
+ return implementations
263
+ }
264
+
265
+ function readScenarioSteps(context: RuleContext, implementation: AstNode): ImplementedStep[] {
266
+ const steps: ImplementedStep[] = []
267
+
268
+ visit(implementation, (node) => {
269
+ if (!isScenarioStepCall(node)) {
270
+ return
271
+ }
272
+
273
+ const [stepName] = (node["arguments"] as unknown[] | undefined) ?? []
274
+
275
+ if (!isStringLiteral(stepName)) {
276
+ report(context, node, "scenario.step(...) must use a string literal step name.")
277
+ return
278
+ }
279
+
280
+ steps.push({
281
+ name: stepName.value,
282
+ node: stepName,
283
+ })
284
+ })
285
+
286
+ return steps
287
+ }
288
+
289
+ function lintImplementationCoverage(
290
+ context: RuleContext,
291
+ scenarios: FeatureScenario[],
292
+ implementations: Implementation[],
293
+ ): void {
294
+ const implementationsById = new Map(
295
+ implementations.map((implementation) => [implementation.id, implementation]),
296
+ )
297
+ const scenariosById = new Map(scenarios.map((scenario) => [scenario.id, scenario]))
298
+
299
+ for (const scenario of scenarios) {
300
+ if (scenario.id === "") {
301
+ continue
302
+ }
303
+
304
+ const implementation = implementationsById.get(scenario.id)
305
+
306
+ if (!implementation) {
307
+ report(
308
+ context,
309
+ implementations[0]?.keyNode ?? contextNode(context),
310
+ `scenario "${scenario.name}" has no implementation for id "${scenario.id}".`,
311
+ )
312
+ continue
313
+ }
314
+
315
+ lintScenarioSteps(context, scenario, implementation)
316
+ }
317
+
318
+ for (const implementation of implementations) {
319
+ if (!scenariosById.has(implementation.id)) {
320
+ report(
321
+ context,
322
+ implementation.keyNode,
323
+ `implementation id "${implementation.id}" has no matching scenario.`,
324
+ )
325
+ }
326
+ }
327
+ }
328
+
329
+ function lintScenarioSteps(
330
+ context: RuleContext,
331
+ scenario: FeatureScenario,
332
+ implementation: Implementation,
333
+ ): void {
334
+ const remainingImplementedSteps = countValues(implementation.steps.map((step) => step.name))
335
+ const missingSteps: string[] = []
336
+
337
+ for (const expectedStep of scenario.steps) {
338
+ const remainingCount = remainingImplementedSteps.get(expectedStep) ?? 0
339
+
340
+ if (remainingCount > 0) {
341
+ remainingImplementedSteps.set(expectedStep, remainingCount - 1)
342
+ } else {
343
+ missingSteps.push(expectedStep)
344
+ report(
345
+ context,
346
+ implementation.keyNode,
347
+ `missing implemented step for "${scenario.id}" from ${scenario.location}: ${quote(expectedStep)}.`,
348
+ )
349
+ }
350
+ }
351
+
352
+ const remainingExpectedSteps = countValues(scenario.steps)
353
+ const unexpectedSteps: ImplementedStep[] = []
354
+
355
+ for (const implementedStep of implementation.steps) {
356
+ const remainingCount = remainingExpectedSteps.get(implementedStep.name) ?? 0
357
+
358
+ if (remainingCount > 0) {
359
+ remainingExpectedSteps.set(implementedStep.name, remainingCount - 1)
360
+ } else {
361
+ unexpectedSteps.push(implementedStep)
362
+ }
363
+ }
364
+
365
+ if (missingSteps.length === 0 && unexpectedSteps.length > 0) {
366
+ report(
367
+ context,
368
+ implementation.keyNode,
369
+ `extra implemented step(s) for "${scenario.id}" not present in ${scenario.location}: ` +
370
+ `${unexpectedSteps.map((step) => quote(step.name)).join(", ")}.`,
371
+ )
372
+ }
373
+
374
+ for (const unexpectedStep of unexpectedSteps) {
375
+ report(
376
+ context,
377
+ unexpectedStep.node,
378
+ `unexpected implemented step for "${scenario.id}": ${quote(unexpectedStep.name)}. ` +
379
+ "No step with this label exists in the feature scenario.",
380
+ )
381
+ }
382
+ }
383
+
384
+ function countValues(values: string[]): Map<string, number> {
385
+ const counts = new Map<string, number>()
386
+
387
+ for (const value of values) {
388
+ counts.set(value, (counts.get(value) ?? 0) + 1)
389
+ }
390
+
391
+ return counts
392
+ }
393
+
394
+ function isScenarioStepCall(node: AstNode): boolean {
395
+ if (node.type !== "CallExpression" || !isNode(node["callee"])) {
396
+ return false
397
+ }
398
+
399
+ const callee = node["callee"]
400
+
401
+ return (
402
+ callee.type === "MemberExpression" &&
403
+ callee["computed"] === false &&
404
+ isIdentifier(callee["object"], "scenario") &&
405
+ isIdentifier(callee["property"], "step")
406
+ )
407
+ }
408
+
409
+ function isIdentifier(value: unknown, name: string): value is AstNode {
410
+ return isNode(value) && value.type === "Identifier" && value["name"] === name
411
+ }
412
+
413
+ function isStringLiteral(value: unknown): value is AstNode & { value: string } {
414
+ return isNode(value) && value.type === "Literal" && typeof value["value"] === "string"
415
+ }
416
+
417
+ function isObjectExpression(value: unknown): value is AstNode {
418
+ return isNode(value) && value.type === "ObjectExpression"
419
+ }
420
+
421
+ function isProperty(value: unknown): value is AstNode & { key: AstNode; value: AstNode } {
422
+ return (
423
+ isNode(value) && value.type === "Property" && isNode(value["key"]) && isNode(value["value"])
424
+ )
425
+ }
426
+
427
+ function isFunctionExpression(value: unknown): boolean {
428
+ return (
429
+ isNode(value) &&
430
+ (value.type === "ArrowFunctionExpression" || value.type === "FunctionExpression")
431
+ )
432
+ }
433
+
434
+ function propertyNameText(name: AstNode): string | undefined {
435
+ if (isStringLiteral(name)) {
436
+ return name.value
437
+ }
438
+
439
+ return undefined
440
+ }
441
+
442
+ function visit(node: AstNode, callback: (node: AstNode) => void, seen = new Set<AstNode>()): void {
443
+ if (seen.has(node)) {
444
+ return
445
+ }
446
+
447
+ seen.add(node)
448
+ callback(node)
449
+
450
+ for (const [key, value] of Object.entries(node)) {
451
+ if (key === "parent" || key === "loc" || key === "range" || key === "start" || key === "end") {
452
+ continue
453
+ }
454
+
455
+ if (Array.isArray(value)) {
456
+ for (const item of value) {
457
+ if (isNode(item)) {
458
+ visit(item, callback, seen)
459
+ }
460
+ }
461
+ } else if (isNode(value)) {
462
+ visit(value, callback, seen)
463
+ }
464
+ }
465
+ }
466
+
467
+ function isNode(value: unknown): value is AstNode {
468
+ return (
469
+ typeof value === "object" &&
470
+ value !== null &&
471
+ typeof (value as { type?: unknown }).type === "string"
472
+ )
473
+ }
474
+
475
+ function report(context: RuleContext, node: AstNode, message: string): void {
476
+ context.report({ node, message })
477
+ }
478
+
479
+ function contextNode(context: RuleContext): AstNode {
480
+ return context.sourceCode?.ast as AstNode
481
+ }
482
+
483
+ function quote(value: string): string {
484
+ return `"${value}"`
485
+ }
@@ -0,0 +1,39 @@
1
+ import assert from "node:assert/strict"
2
+
3
+ import { expect } from "@playwright/test"
4
+ import { feature } from "../support/app-gherkin"
5
+
6
+ feature("../features/helloworld.feature", {
7
+ "hello.say-hello": async ({ db, page, scenario, serverUrl }) => {
8
+ await scenario.step("Given the Rust server is healthy", async () => {
9
+ await expect
10
+ .poll(async () => {
11
+ const response = await page.request.get(new URL("/health", serverUrl).toString())
12
+ return response.ok()
13
+ })
14
+ .toBe(true)
15
+ })
16
+
17
+ await scenario.step("And the visitor is on the hello page", async () => {
18
+ await page.goto("/")
19
+ })
20
+
21
+ await scenario.step("Then they see the default hello message", async () => {
22
+ await expect(page.getByRole("heading", { name: "ConnectRPC helloworld" })).toBeVisible()
23
+ await expect(page.getByText("Hello, World!")).toBeVisible()
24
+ })
25
+
26
+ await scenario.step("When they ask to greet Playwright", async () => {
27
+ await page.getByLabel("Name").fill("Playwright")
28
+ await page.getByRole("button", { name: "Say hello" }).click()
29
+ })
30
+
31
+ await scenario.step("Then they see the Playwright greeting", async () => {
32
+ await expect(page.getByText("Hello, Playwright!")).toBeVisible()
33
+ })
34
+
35
+ await scenario.step("And the Playwright input is saved", async () => {
36
+ assert.deepEqual(db.listHelloWorldInputs(), ["Playwright"])
37
+ })
38
+ },
39
+ })
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["DOM", "ES2022"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "strict": true,
8
+ "exactOptionalPropertyTypes": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "noImplicitOverride": true,
11
+ "noPropertyAccessFromIndexSignature": true,
12
+ "noImplicitReturns": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "noUncheckedSideEffectImports": true,
17
+ "verbatimModuleSyntax": true,
18
+ "moduleDetection": "force",
19
+ "allowUnreachableCode": false,
20
+ "allowUnusedLabels": false,
21
+ "noEmit": true,
22
+ "skipLibCheck": true,
23
+ "types": ["node"]
24
+ },
25
+ "include": ["playwright.config.ts", "support/**/*.ts", "tests/**/*.ts"]
26
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "noEmit": false,
8
+ "outDir": "support/dist",
9
+ "rootDir": "support"
10
+ },
11
+ "include": ["support/oxlint-plugin.ts"]
12
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "backbone",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "packageManager": "pnpm@10.22.0",
6
+ "devDependencies": {
7
+ "varlock": "^1.2.0"
8
+ }
9
+ }