@sun-asterisk/sungen 2.4.6 → 2.5.1

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 (206) hide show
  1. package/README.md +88 -7
  2. package/dist/cli/commands/add.d.ts.map +1 -1
  3. package/dist/cli/commands/add.js +109 -9
  4. package/dist/cli/commands/add.js.map +1 -1
  5. package/dist/cli/commands/figma.d.ts +11 -0
  6. package/dist/cli/commands/figma.d.ts.map +1 -0
  7. package/dist/cli/commands/figma.js +178 -0
  8. package/dist/cli/commands/figma.js.map +1 -0
  9. package/dist/cli/commands/generate.d.ts.map +1 -1
  10. package/dist/cli/commands/generate.js +2 -0
  11. package/dist/cli/commands/generate.js.map +1 -1
  12. package/dist/cli/index.js +4 -2
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/generators/gherkin-parser/index.d.ts +1 -0
  15. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  16. package/dist/generators/gherkin-parser/index.js +3 -0
  17. package/dist/generators/gherkin-parser/index.js.map +1 -1
  18. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +29 -1
  19. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  20. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +21 -1
  21. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  22. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js +11 -2
  23. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  24. package/dist/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
  25. package/dist/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
  26. package/dist/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
  27. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  28. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
  29. package/dist/generators/test-generator/code-generator.d.ts +2 -0
  30. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  31. package/dist/generators/test-generator/code-generator.js +109 -12
  32. package/dist/generators/test-generator/code-generator.js.map +1 -1
  33. package/dist/generators/test-generator/step-mapper.d.ts +1 -0
  34. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  35. package/dist/generators/test-generator/step-mapper.js +1 -1
  36. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  37. package/dist/generators/test-generator/template-engine.d.ts +29 -1
  38. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  39. package/dist/generators/test-generator/template-engine.js +11 -2
  40. package/dist/generators/test-generator/template-engine.js.map +1 -1
  41. package/dist/generators/test-generator/utils/data-resolver.d.ts +11 -2
  42. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  43. package/dist/generators/test-generator/utils/data-resolver.js +36 -25
  44. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  45. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +7 -0
  46. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -0
  47. package/dist/generators/test-generator/utils/runtime-data-transformer.js +42 -0
  48. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -0
  49. package/dist/generators/types.d.ts +1 -0
  50. package/dist/generators/types.d.ts.map +1 -1
  51. package/dist/generators/types.js.map +1 -1
  52. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  53. package/dist/orchestrator/ai-rules-updater.js +2 -0
  54. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  55. package/dist/orchestrator/figma/figma-scaffolder-helpers.d.ts +33 -0
  56. package/dist/orchestrator/figma/figma-scaffolder-helpers.d.ts.map +1 -0
  57. package/dist/orchestrator/figma/figma-scaffolder-helpers.js +135 -0
  58. package/dist/orchestrator/figma/figma-scaffolder-helpers.js.map +1 -0
  59. package/dist/orchestrator/figma/figma-scaffolder-types.d.ts +25 -0
  60. package/dist/orchestrator/figma/figma-scaffolder-types.d.ts.map +1 -0
  61. package/dist/orchestrator/figma/figma-scaffolder-types.js +7 -0
  62. package/dist/orchestrator/figma/figma-scaffolder-types.js.map +1 -0
  63. package/dist/orchestrator/figma/figma-scaffolder.d.ts +23 -0
  64. package/dist/orchestrator/figma/figma-scaffolder.d.ts.map +1 -0
  65. package/dist/orchestrator/figma/figma-scaffolder.js +212 -0
  66. package/dist/orchestrator/figma/figma-scaffolder.js.map +1 -0
  67. package/dist/orchestrator/figma/node-path-collapser.d.ts +16 -0
  68. package/dist/orchestrator/figma/node-path-collapser.d.ts.map +1 -0
  69. package/dist/orchestrator/figma/node-path-collapser.js +37 -0
  70. package/dist/orchestrator/figma/node-path-collapser.js.map +1 -0
  71. package/dist/orchestrator/figma/spec-figma-renderer.d.ts +44 -0
  72. package/dist/orchestrator/figma/spec-figma-renderer.d.ts.map +1 -0
  73. package/dist/orchestrator/figma/spec-figma-renderer.js +45 -0
  74. package/dist/orchestrator/figma/spec-figma-renderer.js.map +1 -0
  75. package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts +23 -0
  76. package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts.map +1 -0
  77. package/dist/orchestrator/figma/spec-figma-section-renderers.js +47 -0
  78. package/dist/orchestrator/figma/spec-figma-section-renderers.js.map +1 -0
  79. package/dist/orchestrator/project-initializer.d.ts +9 -0
  80. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  81. package/dist/orchestrator/project-initializer.js +74 -10
  82. package/dist/orchestrator/project-initializer.js.map +1 -1
  83. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +56 -11
  84. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +30 -17
  85. package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +4 -3
  86. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +34 -2
  87. package/dist/orchestrator/templates/ai-instructions/claude-config.md +12 -2
  88. package/dist/orchestrator/templates/ai-instructions/claude-skill-figma-source.md +151 -0
  89. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +66 -13
  90. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +93 -23
  91. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +2 -0
  92. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +53 -9
  93. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +21 -16
  94. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +4 -3
  95. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +34 -2
  96. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +12 -2
  97. package/dist/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +151 -0
  98. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +151 -0
  99. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +86 -13
  100. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +61 -0
  101. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +105 -28
  102. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +20 -0
  103. package/dist/orchestrator/templates/specs-base.d.ts +12 -1
  104. package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
  105. package/dist/orchestrator/templates/specs-base.js +47 -5
  106. package/dist/orchestrator/templates/specs-base.js.map +1 -1
  107. package/dist/orchestrator/templates/specs-base.ts +65 -7
  108. package/dist/orchestrator/templates/specs-test-data.d.ts +14 -0
  109. package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -0
  110. package/dist/orchestrator/templates/specs-test-data.js +100 -0
  111. package/dist/orchestrator/templates/specs-test-data.js.map +1 -0
  112. package/dist/orchestrator/templates/specs-test-data.ts +66 -0
  113. package/dist/tools/figma/figma-auth.d.ts +36 -0
  114. package/dist/tools/figma/figma-auth.d.ts.map +1 -0
  115. package/dist/tools/figma/figma-auth.js +182 -0
  116. package/dist/tools/figma/figma-auth.js.map +1 -0
  117. package/dist/tools/figma/figma-cache.d.ts +45 -0
  118. package/dist/tools/figma/figma-cache.d.ts.map +1 -0
  119. package/dist/tools/figma/figma-cache.js +191 -0
  120. package/dist/tools/figma/figma-cache.js.map +1 -0
  121. package/dist/tools/figma/figma-client-types.d.ts +112 -0
  122. package/dist/tools/figma/figma-client-types.d.ts.map +1 -0
  123. package/dist/tools/figma/figma-client-types.js +7 -0
  124. package/dist/tools/figma/figma-client-types.js.map +1 -0
  125. package/dist/tools/figma/figma-errors.d.ts +49 -0
  126. package/dist/tools/figma/figma-errors.d.ts.map +1 -0
  127. package/dist/tools/figma/figma-errors.js +105 -0
  128. package/dist/tools/figma/figma-errors.js.map +1 -0
  129. package/dist/tools/figma/figma-image-downloader.d.ts +25 -0
  130. package/dist/tools/figma/figma-image-downloader.d.ts.map +1 -0
  131. package/dist/tools/figma/figma-image-downloader.js +128 -0
  132. package/dist/tools/figma/figma-image-downloader.js.map +1 -0
  133. package/dist/tools/figma/figma-node-filter.d.ts +26 -0
  134. package/dist/tools/figma/figma-node-filter.d.ts.map +1 -0
  135. package/dist/tools/figma/figma-node-filter.js +164 -0
  136. package/dist/tools/figma/figma-node-filter.js.map +1 -0
  137. package/dist/tools/figma/figma-rest-client.d.ts +24 -0
  138. package/dist/tools/figma/figma-rest-client.d.ts.map +1 -0
  139. package/dist/tools/figma/figma-rest-client.js +154 -0
  140. package/dist/tools/figma/figma-rest-client.js.map +1 -0
  141. package/dist/tools/figma/figma-url-parser.d.ts +18 -0
  142. package/dist/tools/figma/figma-url-parser.d.ts.map +1 -0
  143. package/dist/tools/figma/figma-url-parser.js +51 -0
  144. package/dist/tools/figma/figma-url-parser.js.map +1 -0
  145. package/dist/utils/exec-file-no-throw.d.ts +20 -0
  146. package/dist/utils/exec-file-no-throw.d.ts.map +1 -0
  147. package/dist/utils/exec-file-no-throw.js +36 -0
  148. package/dist/utils/exec-file-no-throw.js.map +1 -0
  149. package/package.json +1 -1
  150. package/src/cli/commands/add.ts +80 -9
  151. package/src/cli/commands/figma.ts +162 -0
  152. package/src/cli/commands/generate.ts +2 -0
  153. package/src/cli/index.ts +4 -2
  154. package/src/generators/gherkin-parser/index.ts +4 -0
  155. package/src/generators/test-generator/adapters/adapter-interface.ts +12 -1
  156. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +14 -2
  157. package/src/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
  158. package/src/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
  159. package/src/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
  160. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  161. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
  162. package/src/generators/test-generator/code-generator.ts +122 -13
  163. package/src/generators/test-generator/step-mapper.ts +2 -2
  164. package/src/generators/test-generator/template-engine.ts +28 -2
  165. package/src/generators/test-generator/utils/data-resolver.ts +45 -27
  166. package/src/generators/test-generator/utils/runtime-data-transformer.ts +51 -0
  167. package/src/generators/types.ts +1 -0
  168. package/src/orchestrator/ai-rules-updater.ts +2 -0
  169. package/src/orchestrator/figma/figma-scaffolder-helpers.ts +126 -0
  170. package/src/orchestrator/figma/figma-scaffolder-types.ts +26 -0
  171. package/src/orchestrator/figma/figma-scaffolder.ts +209 -0
  172. package/src/orchestrator/figma/node-path-collapser.ts +38 -0
  173. package/src/orchestrator/figma/spec-figma-renderer.ts +80 -0
  174. package/src/orchestrator/figma/spec-figma-section-renderers.ts +46 -0
  175. package/src/orchestrator/project-initializer.ts +84 -10
  176. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +56 -11
  177. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +30 -17
  178. package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +4 -3
  179. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +34 -2
  180. package/src/orchestrator/templates/ai-instructions/claude-config.md +12 -2
  181. package/src/orchestrator/templates/ai-instructions/claude-skill-figma-source.md +151 -0
  182. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +66 -13
  183. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +93 -23
  184. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +2 -0
  185. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +53 -9
  186. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +21 -16
  187. package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +4 -3
  188. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +34 -2
  189. package/src/orchestrator/templates/ai-instructions/copilot-config.md +12 -2
  190. package/src/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +151 -0
  191. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +151 -0
  192. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +86 -13
  193. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +61 -0
  194. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +105 -28
  195. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +20 -0
  196. package/src/orchestrator/templates/specs-base.ts +65 -7
  197. package/src/orchestrator/templates/specs-test-data.ts +66 -0
  198. package/src/tools/figma/figma-auth.ts +161 -0
  199. package/src/tools/figma/figma-cache.ts +184 -0
  200. package/src/tools/figma/figma-client-types.ts +125 -0
  201. package/src/tools/figma/figma-errors.ts +127 -0
  202. package/src/tools/figma/figma-image-downloader.ts +112 -0
  203. package/src/tools/figma/figma-node-filter.ts +198 -0
  204. package/src/tools/figma/figma-rest-client.ts +183 -0
  205. package/src/tools/figma/figma-url-parser.ts +55 -0
  206. package/src/utils/exec-file-no-throw.ts +45 -0
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { ParsedFeature, ParsedScenario, ParsedStep } from '../gherkin-parser';
4
4
  import { StepMapper } from './step-mapper';
5
5
  import { TestGeneratorAdapter, adapterRegistry } from './adapters';
6
+ import { transformToRuntimeData } from './utils/runtime-data-transformer';
6
7
 
7
8
  /**
8
9
  * Filter base scenario steps for @extend: only keep Given→When steps.
@@ -27,6 +28,33 @@ function filterBaseStepsForExtend(steps: ParsedStep[]): ParsedStep[] {
27
28
  return result;
28
29
  }
29
30
 
31
+ /**
32
+ * Extract @cleanup:* tags into autoCleanup config string for test.use()
33
+ * @cleanup:overlay @cleanup:forms → 'overlay: true, forms: true'
34
+ */
35
+ function extractCleanupConfig(tags: string[]): string | undefined {
36
+ const validKeys = ['overlay', 'forms', 'scroll', 'storage'];
37
+ const entries = tags
38
+ .filter(t => t.startsWith('@cleanup:'))
39
+ .map(t => t.replace('@cleanup:', ''))
40
+ .filter(key => {
41
+ if (!validKeys.includes(key)) {
42
+ console.warn(`⚠ Unknown @cleanup:${key} — valid options: ${validKeys.join(', ')}`);
43
+ return false;
44
+ }
45
+ return true;
46
+ });
47
+ if (entries.length === 0) return undefined;
48
+ return entries.map(key => `${key}: true`).join(', ');
49
+ }
50
+
51
+ /**
52
+ * Check for @screenshot:on-failure tag
53
+ */
54
+ function hasScreenshotOnFailure(tags: string[]): boolean {
55
+ return tags.includes('@screenshot:on-failure');
56
+ }
57
+
30
58
  /**
31
59
  * Extract auth role from tags
32
60
  * @auth:admin → 'admin'
@@ -106,7 +134,7 @@ export class CodeGenerator {
106
134
  // Steps registry built per feature during generateTestCode(); used by countSteps()
107
135
  private stepsRegistry = new Map<string, ParsedScenario>();
108
136
 
109
- constructor(options: { useAI?: boolean; verbose?: boolean; framework?: string; baseURL?: string; screenName?: string } = {}) {
137
+ constructor(options: { useAI?: boolean; verbose?: boolean; framework?: string; baseURL?: string; screenName?: string; runtimeData?: boolean } = {}) {
110
138
  this.options = options;
111
139
  this.screenName = options.screenName;
112
140
  this.stepMapper = new StepMapper(options);
@@ -155,14 +183,19 @@ export class CodeGenerator {
155
183
  : path.join(outputDir, fileName);
156
184
 
157
185
  // Generate imports using adapter
158
- const imports = this.adapter.renderImports();
186
+ const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData });
159
187
 
160
188
  // Generate test code (async now to support AI mapping)
161
189
  const testCode = await this.generateTestCode(feature);
162
190
 
163
191
  // Combine and collapse any runs of 3+ newlines down to 2 (one blank line max)
164
192
  const raw = `${imports}\n\n${testCode}`;
165
- const code = raw.replace(/\n{3,}/g, '\n\n');
193
+ let code = raw.replace(/\n{3,}/g, '\n\n');
194
+
195
+ // Runtime data: replace __SUNGEN_TD_ markers with testData.get() calls
196
+ if (this.options.runtimeData) {
197
+ code = transformToRuntimeData(code);
198
+ }
166
199
 
167
200
  return {
168
201
  featureName: feature.name,
@@ -180,6 +213,7 @@ export class CodeGenerator {
180
213
  private countSteps(feature: ParsedFeature): number {
181
214
  let total = 0;
182
215
  for (const scenario of feature.scenarios) {
216
+ if (scenario.stepsName || scenario.hookType) continue;
183
217
  if (scenario.extendsName) {
184
218
  const base = this.stepsRegistry.get(scenario.extendsName);
185
219
  total += (base ? base.steps.length : 0) + scenario.steps.length;
@@ -206,17 +240,30 @@ export class CodeGenerator {
206
240
  * Ensure specs/base.ts exists in the output directory
207
241
  */
208
242
  ensureBaseFile(outputDir: string): void {
243
+ const templatesRoot = path.join(__dirname, '..', '..', '..', 'orchestrator', 'templates');
244
+
209
245
  const basePath = path.join(outputDir, 'base.ts');
210
- if (fs.existsSync(basePath)) return;
246
+ if (!fs.existsSync(basePath)) {
247
+ const templatePath = path.join(templatesRoot, 'specs-base.ts');
248
+ if (fs.existsSync(templatePath)) {
249
+ const baseDir = path.dirname(basePath);
250
+ if (!fs.existsSync(baseDir)) {
251
+ fs.mkdirSync(baseDir, { recursive: true });
252
+ }
253
+ fs.copyFileSync(templatePath, basePath);
254
+ console.log('✓ Created: specs/base.ts');
255
+ }
256
+ }
211
257
 
212
- const templatePath = path.join(__dirname, '..', '..', '..', 'orchestrator', 'templates', 'specs-base.ts');
213
- if (fs.existsSync(templatePath)) {
214
- const baseDir = path.dirname(basePath);
215
- if (!fs.existsSync(baseDir)) {
216
- fs.mkdirSync(baseDir, { recursive: true });
258
+ if (this.options.runtimeData) {
259
+ const testDataPath = path.join(outputDir, 'test-data.ts');
260
+ if (!fs.existsSync(testDataPath)) {
261
+ const templatePath = path.join(templatesRoot, 'specs-test-data.ts');
262
+ if (fs.existsSync(templatePath)) {
263
+ fs.copyFileSync(templatePath, testDataPath);
264
+ console.log('✓ Created: specs/test-data.ts');
265
+ }
217
266
  }
218
- fs.copyFileSync(templatePath, basePath);
219
- console.log('✓ Created: specs/base.ts');
220
267
  }
221
268
  }
222
269
 
@@ -233,12 +280,14 @@ export class CodeGenerator {
233
280
 
234
281
  // Derive screen name from source file path when not explicitly set
235
282
  // qa/screens/{screenName}/features/{featureName}.feature -> screenName
283
+ let effectiveScreenName = this.screenName;
236
284
  if (!this.screenName && feature.sourceFile) {
237
285
  const sourceDir = path.dirname(feature.sourceFile);
238
286
  const parts = sourceDir.split(path.sep);
239
287
  const screensIndex = parts.indexOf('screens');
240
288
  if (screensIndex >= 0 && screensIndex < parts.length - 2) {
241
- this.stepMapper.setScreenContext(parts[screensIndex + 1]);
289
+ effectiveScreenName = parts[screensIndex + 1];
290
+ this.stepMapper.setScreenContext(effectiveScreenName);
242
291
  }
243
292
  }
244
293
 
@@ -261,13 +310,35 @@ export class CodeGenerator {
261
310
  this.stepsRegistry.set(scenario.stepsName, scenario);
262
311
  }
263
312
  }
264
-
313
+
314
+ // Pre-pass: extract hook scenarios (@beforeAll, @afterEach, @afterAll)
315
+ const hookScenarios = new Map<string, ParsedScenario>();
316
+ for (const scenario of feature.scenarios) {
317
+ if (scenario.hookType) {
318
+ if (hookScenarios.has(scenario.hookType)) {
319
+ console.warn(`⚠ Duplicate @${scenario.hookType} hook — last definition wins`);
320
+ }
321
+ hookScenarios.set(scenario.hookType, scenario);
322
+ }
323
+ }
324
+
265
325
  // Generate background if exists
266
326
  let background: string | undefined;
267
327
  if (feature.background) {
268
328
  background = await this.generateBeforeEach(feature.background);
269
329
  }
270
330
 
331
+ // Generate hook blocks
332
+ const beforeAll = hookScenarios.has('beforeAll')
333
+ ? await this.generateHook(hookScenarios.get('beforeAll')!, 'beforeAll')
334
+ : undefined;
335
+ const afterEach = hookScenarios.has('afterEach')
336
+ ? await this.generateHook(hookScenarios.get('afterEach')!, 'afterEach')
337
+ : undefined;
338
+ const afterAll = hookScenarios.has('afterAll')
339
+ ? await this.generateHook(hookScenarios.get('afterAll')!, 'afterAll')
340
+ : undefined;
341
+
271
342
  // Generate all scenarios with feature tags for inheritance
272
343
  // Skip scenarios tagged with @manual
273
344
  // Track auth role per scenario for grouping
@@ -280,6 +351,11 @@ export class CodeGenerator {
280
351
  continue;
281
352
  }
282
353
 
354
+ // Skip hook scenarios — already generated as hook blocks above
355
+ if (scenario.hookType) {
356
+ continue;
357
+ }
358
+
283
359
  // Resolve auth tags for @extend scenarios (same logic as generateScenario)
284
360
  let authFeatureTags = feature.tags || [];
285
361
  if (scenario.extendsName) {
@@ -327,12 +403,24 @@ export class CodeGenerator {
327
403
  ? authGroups[0].authRole
328
404
  : undefined;
329
405
 
406
+ // Extract @cleanup:* tags for autoCleanup fixture config
407
+ const cleanupConfig = extractCleanupConfig(feature.tags || []);
408
+ const screenshotOnFailure = hasScreenshotOnFailure(feature.tags || []);
409
+
330
410
  // Use adapter to render the complete test file structure
331
411
  return this.adapter.renderTestFile({
332
412
  imports: '', // Not used in template as it's rendered separately
333
413
  featureName: feature.name,
334
414
  featureDescription: feature.description,
335
415
  background,
416
+ beforeAll,
417
+ afterEach,
418
+ afterAll,
419
+ cleanupConfig,
420
+ screenshotOnFailure,
421
+ runtimeData: this.options.runtimeData,
422
+ screenName: effectiveScreenName,
423
+ featureFileName: featureName,
336
424
  scenarios: needsGrouping ? [] : scenarios,
337
425
  authGroups: needsGrouping ? authGroups : undefined,
338
426
  singleAuthRole,
@@ -356,6 +444,27 @@ export class CodeGenerator {
356
444
  });
357
445
  }
358
446
 
447
+ private async generateHook(
448
+ scenario: ParsedScenario,
449
+ hookType: 'beforeAll' | 'afterEach' | 'afterAll'
450
+ ): Promise<string> {
451
+ const steps: Array<{ comment?: string; code: string }> = [];
452
+ for (const step of scenario.steps) {
453
+ const mapped = await Promise.resolve(this.stepMapper.mapStep(step));
454
+ steps.push({
455
+ comment: mapped.comment,
456
+ code: this.indentCode(mapped.code, 4),
457
+ });
458
+ }
459
+
460
+ const renderMap = {
461
+ beforeAll: () => this.adapter.renderBeforeAll({ steps }),
462
+ afterEach: () => this.adapter.renderAfterEach({ steps }),
463
+ afterAll: () => this.adapter.renderAfterAll({ steps }),
464
+ };
465
+ return renderMap[hookType]();
466
+ }
467
+
359
468
  private async generateScenario(
360
469
  scenario: ParsedScenario,
361
470
  hasBackground: boolean,
@@ -35,7 +35,7 @@ export class StepMapper {
35
35
  private inRowScope: boolean = false;
36
36
  private rowScopeTable: string = '';
37
37
 
38
- constructor(options: { verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string } = {}) {
38
+ constructor(options: { verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string; runtimeData?: boolean } = {}) {
39
39
  this.verbose = options.verbose ?? false;
40
40
  this.baseURL = options.baseURL || null; // null means path-only navigation
41
41
  this.featureName = options.featureName;
@@ -46,7 +46,7 @@ export class StepMapper {
46
46
  this.templateEngine = new TemplateEngine(playwrightTemplatesDir);
47
47
  this.patternRegistry = new PatternRegistry();
48
48
  this.selectorResolver = new SelectorResolver(undefined, options.screenName);
49
- this.dataResolver = new DataResolver(undefined, options.screenName);
49
+ this.dataResolver = new DataResolver(undefined, options.screenName, options.runtimeData);
50
50
 
51
51
  if (this.verbose) {
52
52
  console.log(` [StepMapper] ${this.patternRegistry.getPatternCount()} patterns loaded`);
@@ -229,8 +229,8 @@ export class TemplateEngine {
229
229
  this.baseContext = {};
230
230
  }
231
231
 
232
- renderImports(): string {
233
- return this.render('imports', {});
232
+ renderImports(options?: { runtimeData?: boolean }): string {
233
+ return this.render('imports', { runtimeData: options?.runtimeData });
234
234
  }
235
235
 
236
236
  renderTestFile(data: {
@@ -238,6 +238,14 @@ export class TemplateEngine {
238
238
  featureName: string;
239
239
  featureDescription?: string;
240
240
  background?: string;
241
+ beforeAll?: string;
242
+ afterEach?: string;
243
+ afterAll?: string;
244
+ cleanupConfig?: string;
245
+ screenshotOnFailure?: boolean;
246
+ runtimeData?: boolean;
247
+ screenName?: string;
248
+ featureFileName?: string;
241
249
  scenarios: string[];
242
250
  authGroups?: Array<{ authRole?: string; scenarios: string[] }>;
243
251
  singleAuthRole?: string;
@@ -251,6 +259,24 @@ export class TemplateEngine {
251
259
  return this.render('before-each', data);
252
260
  }
253
261
 
262
+ renderBeforeAll(data: {
263
+ steps: Array<{ comment?: string; code: string }>;
264
+ }): string {
265
+ return this.render('before-all', data);
266
+ }
267
+
268
+ renderAfterEach(data: {
269
+ steps: Array<{ comment?: string; code: string }>;
270
+ }): string {
271
+ return this.render('after-each', data);
272
+ }
273
+
274
+ renderAfterAll(data: {
275
+ steps: Array<{ comment?: string; code: string }>;
276
+ }): string {
277
+ return this.render('after-all', data);
278
+ }
279
+
254
280
  renderScenario(data: {
255
281
  scenarioName: string;
256
282
  steps: Array<{ comment?: string; code: string }>;
@@ -5,8 +5,11 @@ import { readYamlIfExists } from '../../../utils/yaml-io';
5
5
 
6
6
  /**
7
7
  * DataResolver - Resolves data references to actual values
8
- * at generation time (not runtime)
9
- *
8
+ *
9
+ * Two modes:
10
+ * - Compile-time (default): resolves YAML values and bakes them into generated code
11
+ * - Runtime (runtimeMode=true): returns markers that post-processor converts to testData.get() calls
12
+ *
10
13
  * Supports override files with priority:
11
14
  * 1. .override.yaml (highest - user customizations)
12
15
  * 2. -override.yaml (legacy - backward compat)
@@ -16,10 +19,12 @@ export class DataResolver {
16
19
  private dataCache = new Map<string, any>();
17
20
  private testDataDir: string;
18
21
  private screenName?: string;
22
+ private runtimeMode: boolean;
19
23
 
20
- constructor(testDataDir?: string, screenName?: string) {
24
+ constructor(testDataDir?: string, screenName?: string, runtimeMode: boolean = false) {
21
25
  this.testDataDir = testDataDir || path.join(process.cwd(), 'qa', 'test-data');
22
26
  this.screenName = screenName;
27
+ this.runtimeMode = runtimeMode;
23
28
  }
24
29
 
25
30
  /**
@@ -36,34 +41,18 @@ export class DataResolver {
36
41
  * @returns The resolved value
37
42
  */
38
43
  resolveData(dataRef: string, featureName?: string): string {
39
- // Split into parts: email.valid -> [email, valid]
40
- // Use only dot (.) as separator
41
- const parts = dataRef.split('.');
42
-
43
- if (parts.length < 1) {
44
- throw new Error(`Invalid data reference: ${dataRef}. Expected format: path.to.value`);
44
+ if (this.runtimeMode) {
45
+ this.validateDataRef(dataRef, featureName);
46
+ return DataResolver.encodeMarker(dataRef);
45
47
  }
46
48
 
47
- // If featureName provided, use it as the file name
48
- // Otherwise, use first part as filename (backward compatibility)
49
- let fileName: string;
50
- let valuePath: string[];
51
-
52
- if (featureName) {
53
- fileName = featureName;
54
- valuePath = parts;
55
- } else {
56
- if (parts.length < 2) {
57
- throw new Error(`Invalid data reference: ${dataRef}. Expected format: file.path.to.value or provide featureName`);
58
- }
59
- fileName = parts[0];
60
- valuePath = parts.slice(1);
61
- }
49
+ return this.resolveValue(dataRef, featureName);
50
+ }
62
51
 
63
- // Load data file
52
+ private resolveValue(dataRef: string, featureName?: string): string {
53
+ const { fileName, valuePath } = this.parseDataRef(dataRef, featureName);
64
54
  const data = this.loadDataFile(fileName);
65
55
 
66
- // Navigate to the value
67
56
  let current = data;
68
57
  for (const key of valuePath) {
69
58
  if (current && typeof current === 'object' && key in current) {
@@ -75,7 +64,6 @@ export class DataResolver {
75
64
  }
76
65
  }
77
66
 
78
- // Return as string
79
67
  if (typeof current === 'string') {
80
68
  return current;
81
69
  } else if (typeof current === 'number' || typeof current === 'boolean') {
@@ -87,6 +75,36 @@ export class DataResolver {
87
75
  }
88
76
  }
89
77
 
78
+ private validateDataRef(dataRef: string, featureName?: string): void {
79
+ // Same navigation as resolveValue — validates the key path exists at compile time
80
+ this.resolveValue(dataRef, featureName);
81
+ }
82
+
83
+ private parseDataRef(dataRef: string, featureName?: string): { fileName: string; valuePath: string[] } {
84
+ const parts = dataRef.split('.');
85
+
86
+ if (parts.length < 1) {
87
+ throw new Error(`Invalid data reference: ${dataRef}. Expected format: path.to.value`);
88
+ }
89
+
90
+ if (featureName) {
91
+ return { fileName: featureName, valuePath: parts };
92
+ }
93
+
94
+ if (parts.length < 2) {
95
+ throw new Error(`Invalid data reference: ${dataRef}. Expected format: file.path.to.value or provide featureName`);
96
+ }
97
+ return { fileName: parts[0], valuePath: parts.slice(1) };
98
+ }
99
+
100
+ static encodeMarker(ref: string): string {
101
+ return `__SUNGEN_TD_${ref.replace(/\./g, '_D_')}__`;
102
+ }
103
+
104
+ static decodeMarker(encoded: string): string {
105
+ return encoded.replace(/^__SUNGEN_TD_/, '').replace(/__$/, '').replace(/_D_/g, '.');
106
+ }
107
+
90
108
  /**
91
109
  * Load data file from disk (with caching)
92
110
  * Searches new screen-based directory structure only
@@ -0,0 +1,51 @@
1
+ const MARKER_PATTERN = /__SUNGEN_TD_([A-Za-z0-9_]+)__/;
2
+
3
+ /**
4
+ * Replace __SUNGEN_TD_ markers with testData.get() calls in generated code.
5
+ * Three passes: comments, string literals, then regex literals.
6
+ */
7
+ export function transformToRuntimeData(code: string): string {
8
+ // Pass 0: Comments — replace markers in // comments with decoded key name
9
+ // Prevents Pass 2 from misinterpreting // comment markers as regex delimiters
10
+ code = code.replace(
11
+ /\/\/(.*)__SUNGEN_TD_([A-Za-z0-9_]+)__(.*)/g,
12
+ (_, before, enc, after) => `//${before}${decodeKey(enc)}${after}`
13
+ );
14
+
15
+ // Pass 1: String literal context — handles both whole-string and embedded markers
16
+ // 'marker' → testData.get('key')
17
+ // 'prefix__marker__suffix' → `prefix${testData.get('key')}suffix`
18
+ code = code.replace(
19
+ /(['"])((?:(?!\1).)*?)__SUNGEN_TD_([A-Za-z0-9_]+)__((?:(?!\1).)*?)\1/g,
20
+ (_, _quote, prefix, enc, suffix) => {
21
+ const key = decodeKey(enc);
22
+ if (!prefix && !suffix) {
23
+ return `testData.get('${key}')`;
24
+ }
25
+ return `\`${prefix}\${testData.get('${key}')}${suffix}\``;
26
+ }
27
+ );
28
+
29
+ // Pass 2: Regex literal context — /prefix__marker__suffix/ → new RegExp(`...`)
30
+ // Only matches within a single line (no newlines in prefix/suffix)
31
+ code = code.replace(
32
+ /\/((?:[^/\\\n]|\\.)*?)__SUNGEN_TD_([A-Za-z0-9_]+)__((?:[^/\\\n]|\\.)*?)\/([gimsuy]*)/g,
33
+ (_, prefix, enc, suffix, flags) => {
34
+ const key = decodeKey(enc);
35
+ const ref = `testData.get('${key}')`;
36
+ const flagStr = flags ? `, '${flags}'` : '';
37
+ if (!prefix && !suffix) return `new RegExp(${ref}${flagStr})`;
38
+ return `new RegExp(\`${prefix}\${${ref}}${suffix}\`${flagStr})`;
39
+ }
40
+ );
41
+
42
+ return code;
43
+ }
44
+
45
+ export function hasRuntimeDataMarkers(code: string): boolean {
46
+ return MARKER_PATTERN.test(code);
47
+ }
48
+
49
+ function decodeKey(encoded: string): string {
50
+ return encoded.replace(/_D_/g, '.');
51
+ }
@@ -170,6 +170,7 @@ export interface GenerateOptions {
170
170
  all?: boolean; // Generate all screens
171
171
  force?: boolean; // Force re-generation
172
172
  skipCache?: boolean; // Skip cache lookup
173
+ runtimeData?: boolean; // Runtime data loading (default: true). False = compile-time hardcoding.
173
174
  }
174
175
 
175
176
  export interface ValidateOptions {
@@ -41,6 +41,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
41
41
  ['claude-skill-capture-figma.md', '.claude/skills/sungen-capture-figma/SKILL.md'],
42
42
  ['claude-skill-capture-local.md', '.claude/skills/sungen-capture-local/SKILL.md'],
43
43
  ['claude-skill-capture-live.md', '.claude/skills/sungen-capture-live/SKILL.md'],
44
+ ['claude-skill-figma-source.md', '.claude/skills/sungen-figma-source/SKILL.md'],
44
45
 
45
46
  // Skills — GitHub Copilot
46
47
  ['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
@@ -55,6 +56,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
55
56
  ['github-skill-sungen-capture-figma.md', '.github/skills/sungen-capture-figma/SKILL.md'],
56
57
  ['github-skill-sungen-capture-local.md', '.github/skills/sungen-capture-local/SKILL.md'],
57
58
  ['github-skill-sungen-capture-live.md', '.github/skills/sungen-capture-live/SKILL.md'],
59
+ ['github-skill-sungen-figma-source.md', '.github/skills/sungen-figma-source/SKILL.md'],
58
60
  ];
59
61
 
60
62
  export class AIRulesUpdater {
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Internal helpers for figma-scaffolder.ts.
3
+ * Extracted to keep the main orchestration file under 200 LOC.
4
+ */
5
+
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import type { Ora } from 'ora';
9
+ import { downloadToPath } from '../../tools/figma/figma-image-downloader';
10
+ import * as FigmaCache from '../../tools/figma/figma-cache';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Name utilities
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /** Sanitize a Figma frame/variant name to a safe filename (lowercase, dashes). */
17
+ export function sanitizeName(name: string): string {
18
+ return name
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9]+/g, '-')
21
+ .replace(/^-+|-+$/g, '');
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Version / stub helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Read generator version from root package.json. Falls back to "sungen". */
29
+ export function readGeneratorVersion(cwd: string): string {
30
+ try {
31
+ const pkgPath = path.join(cwd, 'package.json');
32
+ if (fs.existsSync(pkgPath)) {
33
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { version?: string };
34
+ if (pkg.version) return `sungen v${pkg.version}`;
35
+ }
36
+ } catch {
37
+ // fall through
38
+ }
39
+ return 'sungen';
40
+ }
41
+
42
+ /** Minimal spec.md stub pointing users to spec_figma.md. */
43
+ export function minimalSpecMd(screenName: string): string {
44
+ return `# ${screenName} Screen Specification
45
+
46
+ <!-- This file is the human-authored source of truth. -->
47
+ <!-- spec_figma.md contains auto-generated Figma data — copy useful sections here. -->
48
+
49
+ ## Overview
50
+ - **URL Path:** /${screenName}
51
+ - **Auth Required:** no
52
+ - **Platform:** web
53
+
54
+ ## Sections
55
+
56
+ <!-- Add sections, fields, validation rules, and business rules here. -->
57
+ `;
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Image download orchestration
62
+ // ---------------------------------------------------------------------------
63
+
64
+ export interface DownloadImagesOptions {
65
+ cwd: string;
66
+ fileKey: string;
67
+ versionId: string;
68
+ uiDir: string;
69
+ imageNodeIds: string[];
70
+ mainNodeId: string;
71
+ variantNames: string[];
72
+ /** Pre-warmed cache entries (nodeId → bytes). */
73
+ cachedImages: Map<string, Buffer>;
74
+ /** URL map from getImageUrls response (nodeId → signed URL). */
75
+ imageUrls: Record<string, string | null>;
76
+ frameBaseName: string;
77
+ scaleKind: `${number}x`;
78
+ spinner: Ora;
79
+ }
80
+
81
+ /**
82
+ * Download (or copy from cache) images for each node ID.
83
+ * Returns relative paths (e.g. "ui/login-frame.png") for each successfully written file.
84
+ */
85
+ export async function downloadImages(opts: DownloadImagesOptions): Promise<string[]> {
86
+ const imagePaths: string[] = [];
87
+
88
+ for (let i = 0; i < opts.imageNodeIds.length; i++) {
89
+ const nodeId = opts.imageNodeIds[i];
90
+ const isMain = nodeId === opts.mainNodeId;
91
+ const nameSuffix = isMain
92
+ ? opts.frameBaseName
93
+ : sanitizeName(opts.variantNames[i - 1] ?? `variant-${i}`);
94
+ const destPath = path.join(opts.uiDir, `${nameSuffix}.png`);
95
+ const relPath = `ui/${nameSuffix}.png`;
96
+
97
+ // Cache hit path
98
+ if (opts.cachedImages.has(nodeId)) {
99
+ opts.spinner.start(`Writing cached image: ${nameSuffix}.png`);
100
+ fs.writeFileSync(destPath, opts.cachedImages.get(nodeId)!);
101
+ imagePaths.push(relPath);
102
+ opts.spinner.succeed(` ui/${nameSuffix}.png (cached)`);
103
+ continue;
104
+ }
105
+
106
+ const url = opts.imageUrls[nodeId];
107
+ if (!url) {
108
+ opts.spinner.warn(` No image URL for node ${nodeId} — skipping`);
109
+ continue;
110
+ }
111
+
112
+ opts.spinner.start(`Downloading ui/${nameSuffix}.png…`);
113
+ try {
114
+ await downloadToPath(url, destPath);
115
+ // Cache downloaded bytes for future runs
116
+ const bytes = fs.readFileSync(destPath);
117
+ FigmaCache.put(opts.cwd, opts.fileKey, opts.versionId, nodeId, opts.scaleKind, bytes);
118
+ imagePaths.push(relPath);
119
+ opts.spinner.succeed(` ui/${nameSuffix}.png`);
120
+ } catch {
121
+ opts.spinner.warn(` Failed to download ui/${nameSuffix}.png — skipping`);
122
+ }
123
+ }
124
+
125
+ return imagePaths;
126
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared input/output types for FigmaScaffolder.
3
+ * Kept separate to avoid growing figma-client-types.ts beyond its scope.
4
+ */
5
+
6
+ export interface FigmaScaffolderOptions {
7
+ /** Screen name (e.g. "login", "dashboard"). */
8
+ screenName: string;
9
+ /** Full Figma share URL. */
10
+ figmaUrl: string;
11
+ /** Absolute or cwd-relative working directory. Default: process.cwd(). */
12
+ cwd?: string;
13
+ /** When true, bypass cache and re-fetch from Figma API. Default: false. */
14
+ refresh?: boolean;
15
+ /** Render scale for PNG export. Default: 2. */
16
+ scale?: number;
17
+ }
18
+
19
+ export interface FigmaScaffolderResult {
20
+ /** Absolute path to the written spec_figma.md. */
21
+ specFigmaPath: string;
22
+ /** Absolute paths to downloaded PNG files. */
23
+ imagePaths: string[];
24
+ /** True when spec.md was newly created (was not already present). */
25
+ specMdCreated: boolean;
26
+ }