@invarn/cibuild 2.3.8 → 2.4.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 (36) hide show
  1. package/dist/cli.cjs +54 -37
  2. package/dist/src/cli.js +8 -0
  3. package/dist/src/commands/fidelity-trim.d.ts +13 -0
  4. package/dist/src/commands/fidelity-trim.d.ts.map +1 -0
  5. package/dist/src/commands/fidelity-trim.js +21 -0
  6. package/dist/src/commands/fidelity-trim.test.d.ts +8 -0
  7. package/dist/src/commands/fidelity-trim.test.d.ts.map +1 -0
  8. package/dist/src/commands/fidelity-trim.test.js +79 -0
  9. package/dist/src/yaml/steps/render-post-processor.d.ts +52 -32
  10. package/dist/src/yaml/steps/render-post-processor.d.ts.map +1 -1
  11. package/dist/src/yaml/steps/render-post-processor.js +104 -205
  12. package/dist/src/yaml/steps/render-post-processor.test.d.ts +7 -10
  13. package/dist/src/yaml/steps/render-post-processor.test.d.ts.map +1 -1
  14. package/dist/src/yaml/steps/render-post-processor.test.js +80 -183
  15. package/dist/src/yaml/steps/trim-align-corpus.test.d.ts +28 -0
  16. package/dist/src/yaml/steps/trim-align-corpus.test.d.ts.map +1 -0
  17. package/dist/src/yaml/steps/trim-align-corpus.test.js +383 -0
  18. package/dist/src/yaml/steps/trim-align.d.ts +85 -0
  19. package/dist/src/yaml/steps/trim-align.d.ts.map +1 -0
  20. package/dist/src/yaml/steps/trim-align.js +348 -0
  21. package/dist/src/yaml/steps/trim-align.test.d.ts +11 -0
  22. package/dist/src/yaml/steps/trim-align.test.d.ts.map +1 -0
  23. package/dist/src/yaml/steps/trim-align.test.js +258 -0
  24. package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts +1 -1
  25. package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +1 -1
  26. package/dist/src/yaml/steps/ui-fidelity-preview-android.js +9 -0
  27. package/dist/src/yaml/steps/ui-fidelity-preview-android.test.js +4 -3
  28. package/dist/src/yaml/steps/ui-fidelity-preview.d.ts.map +1 -1
  29. package/dist/src/yaml/steps/ui-fidelity-preview.js +9 -0
  30. package/dist/src/yaml/steps/ui-fidelity-preview.test.js +13 -3
  31. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -1
  32. package/dist/src/yaml/steps/ui-fidelity-render-android.js +7 -16
  33. package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +9 -9
  34. package/dist/src/yaml/steps/ui-fidelity-render.js +1 -1
  35. package/dist/src/yaml/steps/ui-fidelity-render.test.js +25 -20
  36. package/package.json +3 -1
@@ -1,16 +1,13 @@
1
1
  /**
2
2
  * Direct tests for the shared render-output post-processor.
3
3
  *
4
- * Two layers, matching how the module is built:
5
- * - The platform-neutral dimension-reporting logic (TRIMDIMS parsing, the
6
- * logical-pt rounding primitive) is pure JS embedded in the trim-stage
7
- * script. It is evaluated out of the shipped string via
8
- * getPostProcessorInternals() and tested directly, so these run in every
9
- * CI pass.
10
- * - The CoreGraphics pixel logic (content trim, uniform-background crop, the
11
- * transparent-frame fallback, align-to-reference) cannot run under a fake
12
- * `swift`, so it is exercised over crafted PNGs with the real toolchain,
13
- * gated behind CIBUILD_UI_FIDELITY_REAL_SWIFT=1 (the same gate iOS uses).
4
+ * The dimension-reporting logic (TRIMDIMS parsing, the logical-pt rounding
5
+ * primitive) and the helper-invocation resolution are pure JS embedded in the
6
+ * trim-stage script. They are evaluated out of the shipped string via
7
+ * getPostProcessorInternals() and tested directly, so these run in every CI
8
+ * pass. The pixel logic itself lives in trim-align.ts (tested ungated in
9
+ * trim-align.test.ts); the Swift-vs-JS equivalence is guarded by the corpus
10
+ * gate in trim-align-corpus.test.ts.
14
11
  */
15
12
  export {};
16
13
  //# sourceMappingURL=render-post-processor.test.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render-post-processor.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/render-post-processor.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
1
+ {"version":3,"file":"render-post-processor.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/render-post-processor.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -1,22 +1,15 @@
1
1
  /**
2
2
  * Direct tests for the shared render-output post-processor.
3
3
  *
4
- * Two layers, matching how the module is built:
5
- * - The platform-neutral dimension-reporting logic (TRIMDIMS parsing, the
6
- * logical-pt rounding primitive) is pure JS embedded in the trim-stage
7
- * script. It is evaluated out of the shipped string via
8
- * getPostProcessorInternals() and tested directly, so these run in every
9
- * CI pass.
10
- * - The CoreGraphics pixel logic (content trim, uniform-background crop, the
11
- * transparent-frame fallback, align-to-reference) cannot run under a fake
12
- * `swift`, so it is exercised over crafted PNGs with the real toolchain,
13
- * gated behind CIBUILD_UI_FIDELITY_REAL_SWIFT=1 (the same gate iOS uses).
4
+ * The dimension-reporting logic (TRIMDIMS parsing, the logical-pt rounding
5
+ * primitive) and the helper-invocation resolution are pure JS embedded in the
6
+ * trim-stage script. They are evaluated out of the shipped string via
7
+ * getPostProcessorInternals() and tested directly, so these run in every CI
8
+ * pass. The pixel logic itself lives in trim-align.ts (tested ungated in
9
+ * trim-align.test.ts); the Swift-vs-JS equivalence is guarded by the corpus
10
+ * gate in trim-align-corpus.test.ts.
14
11
  */
15
- import { spawnSync } from 'node:child_process';
16
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
17
- import { tmpdir } from 'node:os';
18
- import { join } from 'node:path';
19
- import { getPostProcessorInternals, TRIM_HELPER_SWIFT_SOURCE, } from './render-post-processor.js';
12
+ import { getPostProcessorInternals, RENDER_SCRIPT_TRIM_STAGE, } from './render-post-processor.js';
20
13
  describe('dimension reporting (TRIMDIMS parsing)', () => {
21
14
  const { parseTrimDims } = getPostProcessorInternals();
22
15
  test('parses a well-formed TRIMDIMS line into rendered/reference/source px', () => {
@@ -63,176 +56,80 @@ describe('logical-pt rounding (roundTwo)', () => {
63
56
  expect(roundTwo(100 / 3)).toBe(33.33);
64
57
  });
65
58
  });
66
- // The CoreGraphics bounding-box/resize logic cannot run under a fake `swift`,
67
- // so it is exercised over crafted PNGs with the real toolchain. This is the
68
- // regression guard for the bug where a wrapper that fills its canvas with an
69
- // opaque .background(...) was not trimmed to its content: the renderer forces
70
- // the view into a fixed device frame whose margins are transparent, so the
71
- // helper must detect a solid background from the corners of the opaque region,
72
- // not the raw frame.
73
- describe('trim helper pixel logic (real toolchain)', () => {
74
- const realSwiftTest = process.env.CIBUILD_UI_FIDELITY_REAL_SWIFT === '1' ? test : test.skip;
75
- const { parseTrimDims } = getPostProcessorInternals();
76
- const dirs = [];
77
- afterAll(() => {
78
- for (const dir of dirs) {
79
- try {
80
- rmSync(dir, { recursive: true, force: true });
81
- }
82
- catch {
83
- // best effort
84
- }
85
- }
86
- });
87
- // PNG IHDR carries width/height as big-endian uint32 at byte offsets 16/20.
88
- function pngSize(filePath) {
89
- const buf = readFileSync(filePath);
90
- return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
91
- }
92
- // A parameterized fixture maker: paints opaque rectangles (colors in 0..1)
93
- // onto an otherwise transparent canvas, so a single Swift program can craft
94
- // every input shape the bounding-box logic must handle.
95
- const MAKE_FIXTURE_SWIFT = [
96
- 'import Foundation',
97
- 'import ImageIO',
98
- 'import CoreGraphics',
99
- 'import UniformTypeIdentifiers',
100
- 'let out = CommandLine.arguments[1]',
101
- 'let spec = try! JSONSerialization.jsonObject(with: CommandLine.arguments[2].data(using: .utf8)!) as! [String: Any]',
102
- 'let w = spec["w"] as! Int, h = spec["h"] as! Int',
103
- 'let cs = CGColorSpaceCreateDeviceRGB()',
104
- 'let c = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8, bytesPerRow: 0, space: cs, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!',
105
- 'c.clear(CGRect(x: 0, y: 0, width: w, height: h))',
106
- 'func num(_ v: Any) -> Double { return Double("\\(v)")! }',
107
- 'for r in spec["rects"] as! [[String: Any]] {',
108
- ' c.setFillColor(red: num(r["r"]!), green: num(r["g"]!), blue: num(r["b"]!), alpha: num(r["a"]!))',
109
- ' c.fill(CGRect(x: num(r["x"]!), y: num(r["y"]!), width: num(r["w"]!), height: num(r["h"]!)))',
110
- '}',
111
- 'let d = CGImageDestinationCreateWithURL(URL(fileURLWithPath: out) as CFURL, UTType.png.identifier as CFString, 1, nil)!',
112
- 'CGImageDestinationAddImage(d, c.makeImage()!, nil)',
113
- '_ = CGImageDestinationFinalize(d)',
114
- ].join('\n') + '\n';
115
- const RED = { r: 1, g: 0, b: 0, a: 1 };
116
- const BLUE = { r: 0, g: 0, b: 1, a: 1 };
117
- const WHITE = { r: 1, g: 1, b: 1, a: 1 };
118
- const PINK = { r: 1, g: 240 / 255, b: 238 / 255, a: 1 };
119
- // Prints "<r> <g> <b> <a>" (0..255) for one pixel, so a test can tell padding
120
- // (transparent) apart from content (opaque) in an aligned image.
121
- const PROBE_SWIFT = [
122
- 'import Foundation',
123
- 'import ImageIO',
124
- 'import CoreGraphics',
125
- 'let src = CGImageSourceCreateWithURL(URL(fileURLWithPath: CommandLine.arguments[1]) as CFURL, nil)!',
126
- 'let img = CGImageSourceCreateImageAtIndex(src, 0, nil)!',
127
- 'let x = Int(CommandLine.arguments[2])!, y = Int(CommandLine.arguments[3])!',
128
- 'let w = img.width, h = img.height',
129
- 'var data = [UInt8](repeating: 0, count: w * h * 4)',
130
- 'let cs = CGColorSpaceCreateDeviceRGB()',
131
- 'let c = CGContext(data: &data, width: w, height: h, bitsPerComponent: 8, bytesPerRow: w * 4, space: cs, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!',
132
- 'c.draw(img, in: CGRect(x: 0, y: 0, width: w, height: h))',
133
- 'let i = y * w * 4 + x * 4',
134
- 'print("\\(data[i]) \\(data[i+1]) \\(data[i+2]) \\(data[i+3])")',
135
- ].join('\n') + '\n';
136
- function scene() {
137
- const dir = mkdtempSync(join(tmpdir(), 'ui-fidelity-postproc-'));
138
- dirs.push(dir);
139
- writeFileSync(join(dir, 'Make.swift'), MAKE_FIXTURE_SWIFT);
140
- writeFileSync(join(dir, 'Trim.swift'), TRIM_HELPER_SWIFT_SOURCE);
141
- writeFileSync(join(dir, 'Probe.swift'), PROBE_SWIFT);
142
- return dir;
59
+ // The generated scripts run under whatever node executes the step (a system
60
+ // node, the pkg-built ci binary's bootstrap, or a preview harness), so the
61
+ // stage must resolve how to reach the ci CLI's fidelity-trim helper at run
62
+ // time. The resolution order is load-bearing: an explicit CIBUILD_CLI_JS
63
+ // (preview harnesses, tests) wins, then the pkg self-exec (the production
64
+ // runner path), then a `ci` on PATH.
65
+ describe('trim stage helper invocation resolution', () => {
66
+ function internalsWith(processRef) {
67
+ return getPostProcessorInternals({ processRef });
143
68
  }
144
- // Alpha (0..255) of one pixel in a PNG.
145
- function alphaAt(dir, filePath, x, y) {
146
- const probe = spawnSync('swift', [join(dir, 'Probe.swift'), filePath, String(x), String(y)], {
147
- encoding: 'utf-8',
69
+ test('an explicit CIBUILD_CLI_JS entrypoint wins and runs under this node', () => {
70
+ const { trimHelperInvocation } = internalsWith({
71
+ env: { CIBUILD_CLI_JS: '/opt/cibuild/cli.cjs' },
72
+ execPath: '/usr/local/bin/node',
148
73
  });
149
- expect(probe.status).toBe(0);
150
- return Number(probe.stdout.trim().split(/\s+/)[3]);
151
- }
152
- function craft(dir, name, w, h, rects) {
153
- const out = join(dir, name);
154
- const make = spawnSync('swift', [join(dir, 'Make.swift'), out, JSON.stringify({ w, h, rects })], { encoding: 'utf-8' });
155
- expect(make.stderr + '').toBe('');
156
- expect(make.status).toBe(0);
157
- return out;
158
- }
159
- function trim(dir, input, output, aligned, reference) {
160
- const args = [join(dir, 'Trim.swift'), input, output];
161
- if (aligned && reference)
162
- args.push(aligned, reference);
163
- const result = spawnSync('swift', args, { encoding: 'utf-8' });
164
- expect(result.status).toBe(0);
165
- return result;
166
- }
167
- realSwiftTest('trims a transparent frame down to its single content rectangle', () => {
168
- const dir = scene();
169
- const input = craft(dir, 'in.png', 200, 200, [{ x: 30, y: 50, w: 60, h: 40, ...RED }]);
170
- const out = join(dir, 'out.png');
171
- trim(dir, input, out);
172
- expect(pngSize(out)).toEqual({ width: 60, height: 40 });
173
- }, 600_000);
174
- realSwiftTest('crops an opaque uniform-background wrapper to its content card', () => {
175
- const dir = scene();
176
- // 420x320 transparent canvas, 375x200 white wrapper, 343x168 pink card.
177
- const input = craft(dir, 'in.png', 420, 320, [
178
- { x: 20, y: 40, w: 375, h: 200, ...WHITE },
179
- { x: 36, y: 56, w: 343, h: 168, ...PINK },
180
- ]);
181
- const out = join(dir, 'out.png');
182
- trim(dir, input, out);
183
- // Lands on the content card, NOT the white wrapper (375x200) or the raw
184
- // canvas (420x320).
185
- expect(pngSize(out)).toEqual({ width: 343, height: 168 });
186
- }, 600_000);
187
- realSwiftTest('trims only transparency when the opaque corners disagree (fallback)', () => {
188
- const dir = scene();
189
- // Two differently-colored halves: the opaque bounding box is 80x80 but its
190
- // corners are not one uniform color, so no background is detected and only
191
- // the transparent margin is removed.
192
- const input = craft(dir, 'in.png', 120, 120, [
193
- { x: 20, y: 20, w: 40, h: 80, ...RED },
194
- { x: 60, y: 20, w: 40, h: 80, ...BLUE },
195
- ]);
196
- const out = join(dir, 'out.png');
197
- trim(dir, input, out);
198
- expect(pngSize(out)).toEqual({ width: 80, height: 80 });
199
- }, 600_000);
200
- realSwiftTest('writes a reference-sized aligned image and reports all three dimensions', () => {
201
- const dir = scene();
202
- const input = craft(dir, 'in.png', 200, 200, [{ x: 30, y: 50, w: 60, h: 40, ...RED }]);
203
- const reference = craft(dir, 'ref.png', 100, 50, [{ x: 0, y: 0, w: 100, h: 50, ...BLUE }]);
204
- const out = join(dir, 'out.png');
205
- const aligned = join(dir, 'aligned.png');
206
- const result = trim(dir, input, out, aligned, reference);
207
- // The trimmed render is the content; the aligned copy is fit onto a
208
- // reference-sized canvas (preserving aspect) for a 1:1 overlay.
209
- expect(pngSize(out)).toEqual({ width: 60, height: 40 });
210
- expect(pngSize(aligned)).toEqual({ width: 100, height: 50 });
211
- // The TRIMDIMS line reports rendered (trimmed) / reference / pre-trim
212
- // source px, parsed by the same helper the runtime uses.
213
- expect(parseTrimDims(result.stdout)).toEqual({
214
- rendered: [60, 40],
215
- reference: [100, 50],
216
- source: [200, 200],
74
+ expect(trimHelperInvocation(['in.png', 'out.png'])).toEqual({
75
+ cmd: '/usr/local/bin/node',
76
+ args: ['/opt/cibuild/cli.cjs', 'fidelity-trim', 'in.png', 'out.png'],
77
+ });
78
+ });
79
+ test('under the pkg bootstrap the ci binary re-executes itself', () => {
80
+ const { trimHelperInvocation } = internalsWith({
81
+ env: {},
82
+ pkg: {},
83
+ execPath: '/opt/homebrew/bin/ci',
84
+ });
85
+ expect(trimHelperInvocation(['in.png', 'out.png', 'aligned.png', 'ref.png'])).toEqual({
86
+ cmd: '/opt/homebrew/bin/ci',
87
+ args: ['fidelity-trim', 'in.png', 'out.png', 'aligned.png', 'ref.png'],
217
88
  });
218
- }, 600_000);
219
- realSwiftTest('fits a wide render into a square reference without distortion (transparent letterbox)', () => {
220
- const dir = scene();
221
- // Content fills its canvas: a 2:1 render (80x40), all opaque red.
222
- const input = craft(dir, 'in.png', 80, 40, [{ x: 0, y: 0, w: 80, h: 40, ...RED }]);
223
- // Square 1:1 reference (50x50) — a deliberately different aspect ratio.
224
- const reference = craft(dir, 'ref.png', 50, 50, [{ x: 0, y: 0, w: 50, h: 50, ...BLUE }]);
225
- const out = join(dir, 'out.png');
226
- const aligned = join(dir, 'aligned.png');
227
- trim(dir, input, out, aligned, reference);
228
- // Aligned stays exactly reference-sized (overlay pair holds).
229
- expect(pngSize(aligned)).toEqual({ width: 50, height: 50 });
230
- // Aspect preserved: the 2:1 content fits by width (50x25) and is centered,
231
- // so both letterbox bands are transparent while the center is opaque
232
- // content. A stretch-to-fit would make every one of these opaque.
233
- expect(alphaAt(dir, aligned, 25, 3)).toBe(0); // top band
234
- expect(alphaAt(dir, aligned, 25, 46)).toBe(0); // bottom band
235
- expect(alphaAt(dir, aligned, 25, 25)).toBe(255); // content (center)
236
- }, 600_000);
89
+ });
90
+ test('falls back to a ci on PATH', () => {
91
+ const { trimHelperInvocation } = internalsWith({
92
+ env: {},
93
+ execPath: '/usr/local/bin/node',
94
+ });
95
+ expect(trimHelperInvocation(['in.png', 'out.png'])).toEqual({
96
+ cmd: 'ci',
97
+ args: ['fidelity-trim', 'in.png', 'out.png'],
98
+ });
99
+ });
100
+ test('runTrimHelper reports a spawn error as status 127 with the message appended', () => {
101
+ const calls = [];
102
+ const fakeCp = {
103
+ spawnSync: (...args) => {
104
+ calls.push(args);
105
+ return { status: null, stdout: '', stderr: '', error: new Error('ENOENT: ci not found') };
106
+ },
107
+ };
108
+ const { runTrimHelper } = getPostProcessorInternals({
109
+ cp: fakeCp,
110
+ processRef: {
111
+ env: {},
112
+ execPath: '/usr/local/bin/node',
113
+ stdout: { write: () => true },
114
+ stderr: { write: () => true },
115
+ },
116
+ });
117
+ const result = runTrimHelper(['in.png', 'out.png']);
118
+ expect(result.status).toBe(127);
119
+ expect(result.stderr).toContain('ENOENT: ci not found');
120
+ expect(calls).toHaveLength(1);
121
+ expect(calls[0][0]).toBe('ci');
122
+ });
123
+ });
124
+ // The whole point of the port (fidelity-linux 06): the shipped trim stage must
125
+ // not reference the platform toolchain in any form — no helper source, no
126
+ // interpreter spawn — so the Android render path can run on Linux workers.
127
+ describe('trim stage — no platform-toolchain invocation remains', () => {
128
+ test('the stage script has no swift reference at all', () => {
129
+ expect(RENDER_SCRIPT_TRIM_STAGE.toLowerCase()).not.toContain('swift');
130
+ });
131
+ test('the stage spawns the fidelity-trim helper instead', () => {
132
+ expect(RENDER_SCRIPT_TRIM_STAGE).toContain("'fidelity-trim'");
133
+ });
237
134
  });
238
135
  //# sourceMappingURL=render-post-processor.test.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Corpus gate for the pure-JS trim/align port (fidelity-linux 06).
3
+ *
4
+ * Runs the RETIRED CoreGraphics/Swift helper (kept verbatim below as the
5
+ * equivalence oracle — it no longer ships) and the JS port over the same
6
+ * inputs and requires:
7
+ *
8
+ * - pixel-identical TRIMMED outputs (decoded RGBA compare) — HARD. The
9
+ * trimmed image is what feeds render digests and the fidelity history.
10
+ * - identical TRIMDIMS lines — HARD.
11
+ * - identical exit codes on failure inputs — HARD.
12
+ * - pixel-identical ALIGNED outputs whenever the Swift path did not resample
13
+ * (trimmed size == reference size, integer placement) — HARD. Where
14
+ * CoreGraphics resampled, its proprietary `.high` interpolation is not
15
+ * bit-reproducible outside CoreGraphics; those cases assert canvas
16
+ * geometry and report the pixel deltas in the run summary instead.
17
+ *
18
+ * Inputs: crafted scenes covering every bounding-box branch (including
19
+ * anti-aliased/semi-transparent edges, where the premultiplied-analysis
20
+ * subtlety lives), plus every real PNG pair found under
21
+ * CIBUILD_TRIM_CORPUS_DIR (rendered screens, Figma-extracted references,
22
+ * alpha-heavy assets), each run as no-reference, self-reference, and
23
+ * cross-reference cases.
24
+ *
25
+ * Gated behind CIBUILD_UI_FIDELITY_REAL_SWIFT=1 (needs a real `swift`).
26
+ */
27
+ export {};
28
+ //# sourceMappingURL=trim-align-corpus.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trim-align-corpus.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/trim-align-corpus.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG"}