@stag-build/phonebook 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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +82 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/doctor.d.ts +33 -0
  7. package/dist/commands/doctor.js +690 -0
  8. package/dist/commands/doctor.js.map +1 -0
  9. package/dist/commands/init.d.ts +27 -0
  10. package/dist/commands/init.js +415 -0
  11. package/dist/commands/init.js.map +1 -0
  12. package/dist/config.d.ts +33 -0
  13. package/dist/config.js +21 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/engines/android.d.ts +70 -0
  16. package/dist/engines/android.js +265 -0
  17. package/dist/engines/android.js.map +1 -0
  18. package/dist/engines/git.d.ts +4 -0
  19. package/dist/engines/git.js +13 -0
  20. package/dist/engines/git.js.map +1 -0
  21. package/dist/engines/ios.d.ts +55 -0
  22. package/dist/engines/ios.js +195 -0
  23. package/dist/engines/ios.js.map +1 -0
  24. package/dist/errors.d.ts +12 -0
  25. package/dist/errors.js +69 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/gradle/catalog.d.ts +110 -0
  28. package/dist/gradle/catalog.js +413 -0
  29. package/dist/gradle/catalog.js.map +1 -0
  30. package/dist/ios/snapshotTestClass.d.ts +59 -0
  31. package/dist/ios/snapshotTestClass.js +195 -0
  32. package/dist/ios/snapshotTestClass.js.map +1 -0
  33. package/dist/manifest.d.ts +32 -0
  34. package/dist/manifest.js +23 -0
  35. package/dist/manifest.js.map +1 -0
  36. package/dist/mcp/server.d.ts +1 -0
  37. package/dist/mcp/server.js +226 -0
  38. package/dist/mcp/server.js.map +1 -0
  39. package/dist/naming.d.ts +18 -0
  40. package/dist/naming.js +34 -0
  41. package/dist/naming.js.map +1 -0
  42. package/dist/scan/android.d.ts +7 -0
  43. package/dist/scan/android.js +222 -0
  44. package/dist/scan/android.js.map +1 -0
  45. package/dist/scan/hints.d.ts +25 -0
  46. package/dist/scan/hints.js +338 -0
  47. package/dist/scan/hints.js.map +1 -0
  48. package/dist/scan/ios.d.ts +7 -0
  49. package/dist/scan/ios.js +201 -0
  50. package/dist/scan/ios.js.map +1 -0
  51. package/dist/scan/types.d.ts +52 -0
  52. package/dist/scan/types.js +7 -0
  53. package/dist/scan/types.js.map +1 -0
  54. package/dist/site/build.d.ts +17 -0
  55. package/dist/site/build.js +0 -0
  56. package/dist/site/build.js.map +1 -0
  57. package/dist/versions.d.ts +71 -0
  58. package/dist/versions.js +334 -0
  59. package/dist/versions.js.map +1 -0
  60. package/package.json +54 -0
@@ -0,0 +1,265 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { copyFile, mkdir, readdir, writeFile } from 'node:fs/promises';
4
+ import { join, resolve } from 'node:path';
5
+ import { diagnoseGradleFailure } from '../errors.js';
6
+ import { SCHEMA_VERSION } from '../manifest.js';
7
+ import { parsePreviewName } from '../naming.js';
8
+ import { gitInfo } from './git.js';
9
+ export const EMPTY_PREVIEWS_MESSAGE = 'No previews were recorded. Common causes: (1) packages = listOf(...) in generateComposePreviewRobolectricTests ' +
10
+ 'does not match your app package; (2) includePrivatePreviews = true is missing and your @Preview functions are ' +
11
+ 'private; (3) the module has no @Preview functions. Run `phonebook doctor` to check 1 and 2.';
12
+ /**
13
+ * Guards against `generate` silently succeeding with zero recorded previews.
14
+ * Throws unless `allowEmpty` is set (the `--allow-empty` CLI flag), in which
15
+ * case the caller downgrades this to a warning and still writes the manifest.
16
+ * Pure/exported so it can be unit-tested without spawning Gradle.
17
+ */
18
+ export function checkEmptyEntries(entryCount, allowEmpty) {
19
+ if (entryCount === 0 && !allowEmpty) {
20
+ throw new Error(EMPTY_PREVIEWS_MESSAGE);
21
+ }
22
+ }
23
+ /**
24
+ * Runs Roborazzi (with ComposablePreviewScanner-generated tests) via Gradle and
25
+ * harvests the recorded PNGs into a Phonebook bundle.
26
+ */
27
+ export async function generateAndroid(config, projectDir, outputDir, options = {}) {
28
+ const modules = config.android?.modules ?? [':app'];
29
+ const variant = config.android?.variant ?? 'debug';
30
+ const variantCap = variant[0].toUpperCase() + variant.slice(1);
31
+ const tasks = modules.map((m) => `${m}:recordRoborazzi${variantCap}`);
32
+ await runGradle(projectDir, tasks, options.quiet ?? false);
33
+ const imagesDir = join(outputDir, 'images');
34
+ await mkdir(imagesDir, { recursive: true });
35
+ const entries = [];
36
+ for (const module of modules) {
37
+ const moduleDir = join(projectDir, ...module.split(':').filter(Boolean));
38
+ const roborazziDir = join(moduleDir, 'build', 'outputs', 'roborazzi');
39
+ let files = [];
40
+ try {
41
+ // Recursive: previews named "Component/State" are written into subdirectories.
42
+ files = (await readdir(roborazziDir, { recursive: true }))
43
+ .map(String)
44
+ .filter((f) => f.endsWith('.png'));
45
+ }
46
+ catch {
47
+ throw new Error(`No Roborazzi output at ${roborazziDir}. Is the Roborazzi plugin with ` +
48
+ `generateComposePreviewRobolectricTests enabled in ${module}?`);
49
+ }
50
+ for (const file of files) {
51
+ const meta = parseRoborazziFileName(file);
52
+ const hash = createHash('sha256');
53
+ hash.update(module + file);
54
+ const imageName = `${hash.digest('hex').slice(0, 16)}.png`;
55
+ await copyFile(join(roborazziDir, file), join(imagesDir, imageName));
56
+ // A dark-uiMode preview named e.g. UserCardDarkPreview is the Dark state
57
+ // of UserCard, not a separate component.
58
+ let functionName = meta.functionName;
59
+ let displayName = meta.displayName;
60
+ if (meta.theme === 'dark' && !displayName) {
61
+ const stripped = functionName.replace(/(?:Dark|Night)(Preview)?$/, '$1');
62
+ if (stripped !== functionName)
63
+ functionName = stripped;
64
+ displayName = 'Dark';
65
+ }
66
+ const { component, state } = parsePreviewName(functionName, displayName);
67
+ entries.push({
68
+ component,
69
+ state,
70
+ module,
71
+ ...(meta.sourceFile ? { sourceFile: meta.sourceFile } : {}),
72
+ previewName: meta.fqn,
73
+ image: `images/${imageName}`,
74
+ ...(meta.theme ? { theme: meta.theme } : {}),
75
+ ...(meta.tags && meta.tags.length > 0 ? { tags: meta.tags } : {}),
76
+ });
77
+ }
78
+ }
79
+ entries.sort((a, b) => a.component.localeCompare(b.component) || a.state.localeCompare(b.state));
80
+ const allowEmpty = options.allowEmpty ?? false;
81
+ checkEmptyEntries(entries.length, allowEmpty);
82
+ if (entries.length === 0 && allowEmpty) {
83
+ console.warn(`warning: ${EMPTY_PREVIEWS_MESSAGE}`);
84
+ }
85
+ const manifest = {
86
+ schemaVersion: SCHEMA_VERSION,
87
+ platform: 'android',
88
+ app: {
89
+ name: config.appName,
90
+ ...(await gitInfo(projectDir)),
91
+ generatedAt: new Date().toISOString(),
92
+ },
93
+ entries,
94
+ };
95
+ await writeFile(join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
96
+ return manifest;
97
+ }
98
+ /**
99
+ * Roborazzi's machine-generated marker chunks are ALL-CAPS alnum, except that
100
+ * size markers carry a lowercase "dp" unit: WITH_BACKGROUND, UI_MODE_NIGHT_YES,
101
+ * PIXEL_4_XL, but also W360dp / H96dp (from widthDp/heightDp attributes).
102
+ * A user's name word like "Green" (initial cap + lowercase) never matches.
103
+ */
104
+ const MARKER_CHUNK = '[A-Z0-9]+(?:dp)?';
105
+ const MACHINE_MARKER = new RegExp(`^${MARKER_CHUNK}(?:_${MARKER_CHUNK})+$`);
106
+ /**
107
+ * Roborazzi sometimes glues a machine-generated marker run directly onto a
108
+ * user-chosen display-name token within the same dot-segment, e.g.
109
+ * "Landscape_WIDTH_891DP_HEIGHT_411DP_ORIENTATION_LANDSCAPE" (device-spec
110
+ * annotation glued to the "Landscape" state). Unlike MACHINE_MARKER, this
111
+ * only matches a *trailing* run, leaving a leading name intact.
112
+ */
113
+ const GLUED_MARKER_SUFFIX = new RegExp(`(?:_${MARKER_CHUNK})+$`);
114
+ /**
115
+ * Roborazzi writes the space in @Preview(name = "On Green") as an underscore.
116
+ * Restore spaces in mixed-case name tokens; a deliberate all-caps state like
117
+ * NIGHT_MODE has no lowercase and is left untouched.
118
+ */
119
+ function restoreNameSpaces(token) {
120
+ return /[a-z]/.test(token) ? token.replace(/_/g, ' ') : token;
121
+ }
122
+ /**
123
+ * Strips a trailing glued marker run (see GLUED_MARKER_SUFFIX) off `token`,
124
+ * returning the remaining name plus the stripped run as a single tag. Returns
125
+ * undefined when there is no glued suffix, or when stripping it would leave
126
+ * an empty or all-caps-only remainder (a deliberate state like "NIGHT_MODE"
127
+ * has no lowercase to strip down to, so it is left intact).
128
+ */
129
+ function stripGluedMarker(token) {
130
+ const match = token.match(GLUED_MARKER_SUFFIX);
131
+ if (!match || match.index === undefined)
132
+ return undefined;
133
+ const remainder = token.slice(0, match.index);
134
+ if (remainder.length === 0 || !/[a-z]/.test(remainder))
135
+ return undefined;
136
+ return { name: restoreNameSpaces(remainder), tag: match[0].slice(1) };
137
+ }
138
+ /**
139
+ * Roborazzi + ComposablePreviewScanner names recorded images
140
+ * `<package>.<FileKt>.<PreviewFunction>[.<preview display name>].png`, where a
141
+ * display name containing "/" becomes real subdirectories on disk (verified
142
+ * against Roborazzi 1.72.0):
143
+ * dev.stag.sample.PrimaryButtonKt.PrimaryButtonEnabledPreview.Button/Enabled.png
144
+ * dev.stag.sample.UserCardKt.UserCardPreview.png
145
+ * dev.stag.sample.UserCardKt.UserCardDarkPreview.NIGHT.png (uiMode night)
146
+ *
147
+ * Roborazzi also appends machine-generated ALL-CAPS markers derived from
148
+ * `@Preview` attributes (verified against a real app whose previews are all
149
+ * `@Preview(showBackground = true)`, with no explicit name):
150
+ * com.om.spotifyuiapp...HomeContentKt.HomeContent.WITH_BACKGROUND.png
151
+ * These are not a user-chosen display name and must not become the state;
152
+ * they are collected into `tags` instead. NIGHT/NOTNIGHT (uiMode) become
153
+ * `theme` rather than a tag, as before.
154
+ *
155
+ * `file` is the png path relative to the roborazzi output dir ("/"-separated).
156
+ */
157
+ export function parseRoborazziFileName(file) {
158
+ const base = file.replace(/\.png$/, '').replace(/\\/g, '/');
159
+ const fqn = base;
160
+ // Path segments beyond the first come from a "/" in the preview display name.
161
+ const [head, ...restPath] = base.split('/');
162
+ const dotParts = head.split('.');
163
+ // The segment ending in "Kt" is the file class; the next one is the function.
164
+ let fnIndex = dotParts.findIndex((p) => p.endsWith('Kt')) + 1;
165
+ if (fnIndex <= 0 || fnIndex >= dotParts.length)
166
+ fnIndex = dotParts.length - 1;
167
+ const functionName = dotParts[fnIndex];
168
+ const fileClass = dotParts[fnIndex - 1];
169
+ const sourceFile = fileClass?.endsWith('Kt') && fnIndex >= 1
170
+ ? [...dotParts.slice(0, fnIndex - 1), `${fileClass.slice(0, -2)}.kt`].join('/')
171
+ : undefined;
172
+ // Each remaining "level" (the head's trailing dot segments, then one level
173
+ // per "/" path segment) may itself carry multiple dot-separated tokens: a
174
+ // real display-name token plus Roborazzi's machine-generated markers.
175
+ const levels = [dotParts.slice(fnIndex + 1), ...restPath.map((p) => p.split('.'))];
176
+ let theme;
177
+ const tags = [];
178
+ const nameLevels = [];
179
+ for (const level of levels) {
180
+ const nameTokens = [];
181
+ for (const token of level) {
182
+ if (token === 'NIGHT')
183
+ theme = 'dark';
184
+ else if (token === 'NOTNIGHT')
185
+ theme = 'light';
186
+ else if (MACHINE_MARKER.test(token))
187
+ tags.push(token);
188
+ else {
189
+ const glued = stripGluedMarker(token);
190
+ if (glued) {
191
+ nameTokens.push(glued.name);
192
+ tags.push(glued.tag);
193
+ }
194
+ else {
195
+ nameTokens.push(restoreNameSpaces(token));
196
+ }
197
+ }
198
+ }
199
+ if (nameTokens.length > 0)
200
+ nameLevels.push(nameTokens.join('.'));
201
+ }
202
+ const displayName = nameLevels.length > 0 ? nameLevels.join('/') : undefined;
203
+ return {
204
+ fqn,
205
+ functionName,
206
+ displayName,
207
+ theme,
208
+ sourceFile,
209
+ ...(tags.length > 0 ? { tags } : {}),
210
+ };
211
+ }
212
+ /**
213
+ * Runs Gradle. When `quiet` is false (the CLI default), output streams
214
+ * straight through to this process's stdout/stderr as it arrives (so a human
215
+ * can watch the build) while a rolling tail is kept alongside it for error
216
+ * translation. When `quiet` is true (used by the MCP server, whose stdout is
217
+ * the JSON-RPC channel and must never carry build output), only the tail is
218
+ * kept and nothing is echoed live; the tail is written to stderr once if the
219
+ * build fails, unless `dumpTailOnFailure` is set to false.
220
+ *
221
+ * `onOutput`, when provided, is called once on exit (success or failure) with
222
+ * the full, untruncated captured output — useful for callers that want to
223
+ * write a complete log file rather than relying on the 200-line tail.
224
+ */
225
+ export function runGradle(projectDir, tasks, quiet, options = {}) {
226
+ return new Promise((res, rej) => {
227
+ const gradlew = resolve(projectDir, process.platform === 'win32' ? 'gradlew.bat' : 'gradlew');
228
+ const child = spawn(gradlew, [...tasks, '--stacktrace'], {
229
+ cwd: projectDir,
230
+ stdio: ['ignore', 'pipe', 'pipe'],
231
+ });
232
+ let tail = [];
233
+ const full = [];
234
+ const onData = (target) => (data) => {
235
+ if (!quiet)
236
+ target.write(data);
237
+ const chunkLines = data.toString('utf8').split('\n');
238
+ tail.push(...chunkLines);
239
+ if (tail.length > 200)
240
+ tail = tail.slice(-200);
241
+ full.push(...chunkLines);
242
+ };
243
+ child.stdout?.on('data', onData(process.stdout));
244
+ child.stderr?.on('data', onData(process.stderr));
245
+ child.on('error', (err) => rej(new Error(`Failed to run ${gradlew}: ${err.message}`)));
246
+ child.on('exit', (code) => {
247
+ options.onOutput?.(full.join('\n'));
248
+ if (code === 0) {
249
+ res();
250
+ return;
251
+ }
252
+ const tailText = tail.join('\n');
253
+ const diagnosis = diagnoseGradleFailure(tailText);
254
+ if (diagnosis.length > 0) {
255
+ process.stderr.write(diagnosis.map((line) => `phonebook: ${line}`).join('\n') + '\n');
256
+ }
257
+ const dumpTailOnFailure = options.dumpTailOnFailure ?? true;
258
+ if (quiet && dumpTailOnFailure && tail.length > 0)
259
+ process.stderr.write(tailText + '\n');
260
+ const message = [`Gradle failed (exit ${code}) running: ${tasks.join(' ')}`, ...diagnosis].join('\n');
261
+ rej(new Error(message));
262
+ });
263
+ });
264
+ }
265
+ //# sourceMappingURL=android.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"android.js","sourceRoot":"","sources":["../../src/engines/android.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ /** Best-effort git metadata for the manifest; absent outside a git repo. */
2
+ export declare function gitInfo(projectDir: string): Promise<{
3
+ commit?: string;
4
+ }>;
@@ -0,0 +1,13 @@
1
+ import { spawn } from 'node:child_process';
2
+ /** Best-effort git metadata for the manifest; absent outside a git repo. */
3
+ export async function gitInfo(projectDir) {
4
+ const commit = await new Promise((res) => {
5
+ const child = spawn('git', ['rev-parse', 'HEAD'], { cwd: projectDir });
6
+ let out = '';
7
+ child.stdout.on('data', (d) => (out += d));
8
+ child.on('error', () => res(undefined));
9
+ child.on('exit', (code) => res(code === 0 ? out.trim() : undefined));
10
+ });
11
+ return commit ? { commit } : {};
12
+ }
13
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/engines/git.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,UAAkB;IAC9C,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,CAAqB,CAAC,GAAG,EAAE,EAAE;QAC3D,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC;QACvE,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;QACxC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAClC,CAAC"}
@@ -0,0 +1,55 @@
1
+ import type { PhonebookConfig } from '../config.js';
2
+ import { type Manifest, type ManifestEntry } from '../manifest.js';
3
+ /** Builds the error/warning text for a `generate` run that exported zero snapshots. Exported for tests. */
4
+ export declare function buildEmptySnapshotsMessage(exportDir: string, scheme: string): string;
5
+ /**
6
+ * Resolves the -only-testing:Target/Class argument so `generate` runs just the
7
+ * snapshot test class instead of the app's whole test suite — an unrelated
8
+ * failing unit test must not kill screenshot generation (seen in a real repo
9
+ * whose own tests were flaky). Explicit config wins; "" disables the filter.
10
+ * Exported for tests.
11
+ */
12
+ export declare function resolveOnlyTesting(config: PhonebookConfig, projectDir: string): Promise<string | undefined>;
13
+ /**
14
+ * Runs the SnapshotPreviews-backed XCTest target via xcodebuild on a simulator
15
+ * and harvests the exported PNG + JSON sidecar pairs into a Phonebook bundle.
16
+ *
17
+ * SnapshotPreviews exports to the directory given by the SNAPSHOTS_EXPORT_DIR
18
+ * env var in the test-runner process; xcodebuild forwards any TEST_RUNNER_-
19
+ * prefixed variable (prefix stripped) into that process.
20
+ */
21
+ export declare function generateIos(config: PhonebookConfig, projectDir: string, outputDir: string, options?: {
22
+ quiet?: boolean;
23
+ allowEmpty?: boolean;
24
+ }): Promise<Manifest>;
25
+ /** Shape of the JSON sidecar SnapshotPreviews writes next to each PNG. */
26
+ interface SnapshotSidecar {
27
+ display_name?: string;
28
+ group?: string;
29
+ context?: {
30
+ preview?: {
31
+ container_display_name?: string;
32
+ preferred_color_scheme?: string;
33
+ };
34
+ simulator?: {
35
+ device_name?: string;
36
+ };
37
+ };
38
+ }
39
+ /** Exported for tests. Maps one PNG + sidecar to a manifest entry (minus image path). */
40
+ export declare function mapSidecar(png: string, sidecar: SnapshotSidecar): Omit<ManifestEntry, 'image'>;
41
+ /**
42
+ * Runs xcodebuild. When `quiet` is false (the CLI default), output streams
43
+ * straight to this process's stdout/stderr as it arrives (so a human can
44
+ * watch the build) while a rolling ~200-line tail is kept alongside it for
45
+ * error translation. When `quiet` is true (used by the MCP server, whose
46
+ * stdout is the JSON-RPC channel and must never carry build output), only the
47
+ * tail is kept and nothing is echoed live; the tail is written to stderr once
48
+ * if the build fails.
49
+ *
50
+ * On a non-zero exit, the tail is run through `diagnoseXcodebuildFailure` —
51
+ * any matched diagnosis lines are printed to stderr (`phonebook: `-prefixed)
52
+ * and folded into the rejected Error's message, mirroring `runGradle`.
53
+ */
54
+ export declare function runXcodebuild(cwd: string, args: string[], env: Record<string, string>, quiet: boolean): Promise<void>;
55
+ export {};
@@ -0,0 +1,195 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { copyFile, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
6
+ import { diagnoseXcodebuildFailure } from '../errors.js';
7
+ import { SCHEMA_VERSION } from '../manifest.js';
8
+ import { parsePreviewName, spaceCamelCase } from '../naming.js';
9
+ import { gitInfo } from './git.js';
10
+ import { findSnapshotTestSubclass, findSnapshottingTestsTargets, readPbxprojText } from '../ios/snapshotTestClass.js';
11
+ /** Builds the error/warning text for a `generate` run that exported zero snapshots. Exported for tests. */
12
+ export function buildEmptySnapshotsMessage(exportDir, scheme) {
13
+ return (`xcodebuild succeeded but no snapshots were exported to ${exportDir}. ` +
14
+ `Does the "${scheme}" scheme include a SnapshotPreviews test target, and are your #Preview macros not all ` +
15
+ 'filtered out (e.g. by a snapshotPreviews() override that excludes them)?');
16
+ }
17
+ /**
18
+ * Resolves the -only-testing:Target/Class argument so `generate` runs just the
19
+ * snapshot test class instead of the app's whole test suite — an unrelated
20
+ * failing unit test must not kill screenshot generation (seen in a real repo
21
+ * whose own tests were flaky). Explicit config wins; "" disables the filter.
22
+ * Exported for tests.
23
+ */
24
+ export async function resolveOnlyTesting(config, projectDir) {
25
+ const configured = config.ios?.onlyTesting;
26
+ if (configured !== undefined)
27
+ return configured === '' ? undefined : configured;
28
+ try {
29
+ const [subclass, pbxproj] = await Promise.all([
30
+ findSnapshotTestSubclass(projectDir),
31
+ readPbxprojText(projectDir, config.ios?.project ? join(projectDir, config.ios.project) : undefined),
32
+ ]);
33
+ if (!subclass || !pbxproj)
34
+ return undefined;
35
+ const targets = findSnapshottingTestsTargets(pbxproj);
36
+ if (targets.length !== 1)
37
+ return undefined;
38
+ return `${targets[0].name}/${subclass.className}`;
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ /**
45
+ * Runs the SnapshotPreviews-backed XCTest target via xcodebuild on a simulator
46
+ * and harvests the exported PNG + JSON sidecar pairs into a Phonebook bundle.
47
+ *
48
+ * SnapshotPreviews exports to the directory given by the SNAPSHOTS_EXPORT_DIR
49
+ * env var in the test-runner process; xcodebuild forwards any TEST_RUNNER_-
50
+ * prefixed variable (prefix stripped) into that process.
51
+ */
52
+ export async function generateIos(config, projectDir, outputDir, options = {}) {
53
+ const ios = config.ios;
54
+ if (!ios?.scheme)
55
+ throw new Error('phonebook.config.json: "ios.scheme" is required');
56
+ if (!ios.project && !ios.workspace) {
57
+ throw new Error('phonebook.config.json: one of "ios.project" or "ios.workspace" is required');
58
+ }
59
+ const simulator = ios.simulator ?? 'iPhone 17 Pro';
60
+ const exportDir = await mkdtemp(join(tmpdir(), 'phonebook-snapshots-'));
61
+ try {
62
+ const args = [
63
+ 'test',
64
+ ...(ios.workspace ? ['-workspace', ios.workspace] : ['-project', ios.project]),
65
+ '-scheme',
66
+ ios.scheme,
67
+ '-destination',
68
+ `platform=iOS Simulator,name=${simulator}`,
69
+ ];
70
+ const onlyTesting = await resolveOnlyTesting(config, projectDir);
71
+ if (onlyTesting)
72
+ args.push(`-only-testing:${onlyTesting}`);
73
+ await runXcodebuild(projectDir, args, {
74
+ TEST_RUNNER_SNAPSHOTS_EXPORT_DIR: exportDir,
75
+ TEST_RUNNER_SNAPSHOTS_RUNNING_FOR_PREVIEWS: '1',
76
+ }, options.quiet ?? false);
77
+ const imagesDir = join(outputDir, 'images');
78
+ await mkdir(imagesDir, { recursive: true });
79
+ const pngs = (await readdir(exportDir)).filter((f) => f.endsWith('.png')).sort();
80
+ if (pngs.length === 0) {
81
+ const message = buildEmptySnapshotsMessage(exportDir, ios.scheme);
82
+ if (!(options.allowEmpty ?? false)) {
83
+ throw new Error(message);
84
+ }
85
+ console.warn(`warning: ${message}`);
86
+ }
87
+ const entries = [];
88
+ for (const png of pngs) {
89
+ const sidecar = await readSidecar(join(exportDir, png.replace(/\.png$/, '.json')));
90
+ const meta = mapSidecar(png, sidecar);
91
+ const hash = createHash('sha256');
92
+ hash.update(png);
93
+ const imageName = `${hash.digest('hex').slice(0, 16)}.png`;
94
+ await copyFile(join(exportDir, png), join(imagesDir, imageName));
95
+ entries.push({ ...meta, image: `images/${imageName}` });
96
+ }
97
+ entries.sort((a, b) => a.component.localeCompare(b.component) || a.state.localeCompare(b.state));
98
+ const manifest = {
99
+ schemaVersion: SCHEMA_VERSION,
100
+ platform: 'ios',
101
+ app: {
102
+ name: config.appName,
103
+ generatedAt: new Date().toISOString(),
104
+ ...(await gitInfo(projectDir)),
105
+ },
106
+ entries,
107
+ };
108
+ await writeFile(join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
109
+ return manifest;
110
+ }
111
+ finally {
112
+ await rm(exportDir, { recursive: true, force: true });
113
+ }
114
+ }
115
+ async function readSidecar(path) {
116
+ try {
117
+ return JSON.parse(await readFile(path, 'utf8'));
118
+ }
119
+ catch {
120
+ return {};
121
+ }
122
+ }
123
+ /** Exported for tests. Maps one PNG + sidecar to a manifest entry (minus image path). */
124
+ export function mapSidecar(png, sidecar) {
125
+ const container = sidecar.context?.preview?.container_display_name;
126
+ // Unnamed previews get an auto display name like "At line #14" — not a state.
127
+ const rawName = sidecar.display_name?.trim();
128
+ const displayName = rawName && !/^At line #\d+$/.test(rawName) ? rawName : undefined;
129
+ const fallback = container ? container.replace(/\s+/g, '') : png.replace(/\.png$/, '');
130
+ const { component, state } = parsePreviewName(fallback, displayName);
131
+ const scheme = sidecar.context?.preview?.preferred_color_scheme;
132
+ const group = sidecar.group; // e.g. "PhonebookSample/UserCard.swift"
133
+ const module = group?.includes('/') ? group.slice(0, group.indexOf('/')) : undefined;
134
+ return {
135
+ component: component || spaceCamelCase(fallback),
136
+ state,
137
+ module: module ?? 'app',
138
+ sourceFile: group,
139
+ previewName: rawName ?? png,
140
+ theme: scheme === 'dark' ? 'dark' : scheme === 'light' ? 'light' : undefined,
141
+ device: sidecar.context?.simulator?.device_name,
142
+ };
143
+ }
144
+ /**
145
+ * Runs xcodebuild. When `quiet` is false (the CLI default), output streams
146
+ * straight to this process's stdout/stderr as it arrives (so a human can
147
+ * watch the build) while a rolling ~200-line tail is kept alongside it for
148
+ * error translation. When `quiet` is true (used by the MCP server, whose
149
+ * stdout is the JSON-RPC channel and must never carry build output), only the
150
+ * tail is kept and nothing is echoed live; the tail is written to stderr once
151
+ * if the build fails.
152
+ *
153
+ * On a non-zero exit, the tail is run through `diagnoseXcodebuildFailure` —
154
+ * any matched diagnosis lines are printed to stderr (`phonebook: `-prefixed)
155
+ * and folded into the rejected Error's message, mirroring `runGradle`.
156
+ */
157
+ export function runXcodebuild(cwd, args, env, quiet) {
158
+ return new Promise((resolvePromise, reject) => {
159
+ const child = spawn('xcodebuild', args, {
160
+ cwd: resolve(cwd),
161
+ stdio: ['ignore', 'pipe', 'pipe'],
162
+ env: { ...process.env, ...env },
163
+ });
164
+ let tail = [];
165
+ const onData = (target) => (data) => {
166
+ if (!quiet)
167
+ target.write(data);
168
+ tail.push(...data.toString('utf8').split('\n'));
169
+ if (tail.length > 200)
170
+ tail = tail.slice(-200);
171
+ };
172
+ child.stdout?.on('data', onData(process.stdout));
173
+ child.stderr?.on('data', onData(process.stderr));
174
+ child.on('error', reject);
175
+ child.on('close', (code) => {
176
+ if (code === 0) {
177
+ resolvePromise();
178
+ return;
179
+ }
180
+ const tailText = tail.join('\n');
181
+ const diagnosis = diagnoseXcodebuildFailure(tailText);
182
+ if (diagnosis.length > 0) {
183
+ process.stderr.write(diagnosis.map((line) => `phonebook: ${line}`).join('\n') + '\n');
184
+ }
185
+ if (quiet && tail.length > 0)
186
+ process.stderr.write(tailText + '\n');
187
+ const message = [
188
+ `xcodebuild failed (exit ${code}) running: xcodebuild ${args.join(' ')}`,
189
+ ...diagnosis,
190
+ ].join('\n');
191
+ reject(new Error(message));
192
+ });
193
+ });
194
+ }
195
+ //# sourceMappingURL=ios.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ios.js","sourceRoot":"","sources":["../../src/engines/ios.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Signature matching over raw Gradle (or xcodebuild) output, turning known
3
+ * failure patterns into human diagnosis lines. Zero lines means "no known
4
+ * signature matched" — the raw output should still be shown to the user.
5
+ */
6
+ export declare function diagnoseGradleFailure(output: string): string[];
7
+ /**
8
+ * Signature matching over raw xcodebuild output, turning known failure
9
+ * patterns into human diagnosis lines. Zero lines means "no known signature
10
+ * matched" — the raw output should still be shown to the user.
11
+ */
12
+ export declare function diagnoseXcodebuildFailure(output: string): string[];