@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.
- package/dist/cli.cjs +54 -37
- package/dist/src/cli.js +8 -0
- package/dist/src/commands/fidelity-trim.d.ts +13 -0
- package/dist/src/commands/fidelity-trim.d.ts.map +1 -0
- package/dist/src/commands/fidelity-trim.js +21 -0
- package/dist/src/commands/fidelity-trim.test.d.ts +8 -0
- package/dist/src/commands/fidelity-trim.test.d.ts.map +1 -0
- package/dist/src/commands/fidelity-trim.test.js +79 -0
- package/dist/src/yaml/steps/render-post-processor.d.ts +52 -32
- package/dist/src/yaml/steps/render-post-processor.d.ts.map +1 -1
- package/dist/src/yaml/steps/render-post-processor.js +104 -205
- package/dist/src/yaml/steps/render-post-processor.test.d.ts +7 -10
- package/dist/src/yaml/steps/render-post-processor.test.d.ts.map +1 -1
- package/dist/src/yaml/steps/render-post-processor.test.js +80 -183
- package/dist/src/yaml/steps/trim-align-corpus.test.d.ts +28 -0
- package/dist/src/yaml/steps/trim-align-corpus.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/trim-align-corpus.test.js +383 -0
- package/dist/src/yaml/steps/trim-align.d.ts +85 -0
- package/dist/src/yaml/steps/trim-align.d.ts.map +1 -0
- package/dist/src/yaml/steps/trim-align.js +348 -0
- package/dist/src/yaml/steps/trim-align.test.d.ts +11 -0
- package/dist/src/yaml/steps/trim-align.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/trim-align.test.js +258 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts +1 -1
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-preview-android.js +9 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.js +4 -3
- package/dist/src/yaml/steps/ui-fidelity-preview.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-preview.js +9 -0
- package/dist/src/yaml/steps/ui-fidelity-preview.test.js +13 -3
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.js +7 -16
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +9 -9
- package/dist/src/yaml/steps/ui-fidelity-render.js +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +25 -20
- package/package.json +3 -1
package/dist/src/cli.js
CHANGED
|
@@ -77,6 +77,14 @@ Examples:
|
|
|
77
77
|
`);
|
|
78
78
|
process.exit(0);
|
|
79
79
|
}
|
|
80
|
+
// Handle fidelity-trim — the internal trim/align helper spawned by the
|
|
81
|
+
// generated UI-fidelity render scripts (kept flag-free and fast: it must
|
|
82
|
+
// behave identically whether invoked as `ci`, the pkg binary re-exec, or
|
|
83
|
+
// `node dist/cli.cjs`).
|
|
84
|
+
if (args[0] === "fidelity-trim") {
|
|
85
|
+
const { handleFidelityTrimCommand } = await import("./commands/fidelity-trim.js");
|
|
86
|
+
process.exit(handleFidelityTrimCommand(args.slice(1)));
|
|
87
|
+
}
|
|
80
88
|
// Handle envman subcommand — delegates to envman CLI
|
|
81
89
|
if (args[0] === "envman") {
|
|
82
90
|
// Re-write process.argv so envman sees its own args
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ci fidelity-trim <in.png> <out.png> [<aligned.png> <reference.png>]`
|
|
3
|
+
*
|
|
4
|
+
* Internal command: the trim/align helper the generated UI-fidelity render
|
|
5
|
+
* scripts (iOS + Android) spawn after each screen renders. It crops a render
|
|
6
|
+
* to its content bounding box in place, optionally writes a reference-sized
|
|
7
|
+
* aligned copy, and prints a `TRIMDIMS` line — the exact argv/stdout/exit-code
|
|
8
|
+
* contract of the retired embedded Swift (CoreGraphics) helper, now pure JS so
|
|
9
|
+
* the same bytes run on macOS and Linux workers.
|
|
10
|
+
*/
|
|
11
|
+
/** Runs the helper and returns its exit status (0 on success). */
|
|
12
|
+
export declare function handleFidelityTrimCommand(args: string[]): number;
|
|
13
|
+
//# sourceMappingURL=fidelity-trim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fidelity-trim.d.ts","sourceRoot":"","sources":["../../../src/commands/fidelity-trim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,kEAAkE;AAClE,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAKhE"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ci fidelity-trim <in.png> <out.png> [<aligned.png> <reference.png>]`
|
|
3
|
+
*
|
|
4
|
+
* Internal command: the trim/align helper the generated UI-fidelity render
|
|
5
|
+
* scripts (iOS + Android) spawn after each screen renders. It crops a render
|
|
6
|
+
* to its content bounding box in place, optionally writes a reference-sized
|
|
7
|
+
* aligned copy, and prints a `TRIMDIMS` line — the exact argv/stdout/exit-code
|
|
8
|
+
* contract of the retired embedded Swift (CoreGraphics) helper, now pure JS so
|
|
9
|
+
* the same bytes run on macOS and Linux workers.
|
|
10
|
+
*/
|
|
11
|
+
import { runTrimAlign } from '../yaml/steps/trim-align.js';
|
|
12
|
+
/** Runs the helper and returns its exit status (0 on success). */
|
|
13
|
+
export function handleFidelityTrimCommand(args) {
|
|
14
|
+
const result = runTrimAlign(args);
|
|
15
|
+
if (result.stdout)
|
|
16
|
+
process.stdout.write(result.stdout);
|
|
17
|
+
if (result.stderr)
|
|
18
|
+
process.stderr.write(result.stderr);
|
|
19
|
+
return result.status;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=fidelity-trim.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `ci fidelity-trim` command — the process seam over the pure
|
|
3
|
+
* trim/align core. The generated fidelity render scripts (iOS + Android) spawn
|
|
4
|
+
* this instead of the retired Swift helper, so the argv contract, TRIMDIMS
|
|
5
|
+
* stdout line, and exit codes are load-bearing.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=fidelity-trim.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fidelity-trim.test.d.ts","sourceRoot":"","sources":["../../../src/commands/fidelity-trim.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `ci fidelity-trim` command — the process seam over the pure
|
|
3
|
+
* trim/align core. The generated fidelity render scripts (iOS + Android) spawn
|
|
4
|
+
* this instead of the retired Swift helper, so the argv contract, TRIMDIMS
|
|
5
|
+
* stdout line, and exit codes are load-bearing.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { PNG } from 'pngjs';
|
|
11
|
+
import { describe, test, expect, afterAll, jest } from '@jest/globals';
|
|
12
|
+
import { handleFidelityTrimCommand } from './fidelity-trim.js';
|
|
13
|
+
const dirs = [];
|
|
14
|
+
function scene() {
|
|
15
|
+
const dir = mkdtempSync(join(tmpdir(), 'fidelity-trim-cmd-'));
|
|
16
|
+
dirs.push(dir);
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
for (const dir of dirs) {
|
|
21
|
+
try {
|
|
22
|
+
rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// best effort
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
function craftOpaqueRect(w, h, x, y, rw, rh) {
|
|
30
|
+
const png = new PNG({ width: w, height: h });
|
|
31
|
+
for (let yy = y; yy < y + rh; yy++) {
|
|
32
|
+
for (let xx = x; xx < x + rw; xx++) {
|
|
33
|
+
const i = (yy * w + xx) * 4;
|
|
34
|
+
png.data[i] = 255;
|
|
35
|
+
png.data[i + 3] = 255;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return PNG.sync.write(png);
|
|
39
|
+
}
|
|
40
|
+
describe('handleFidelityTrimCommand', () => {
|
|
41
|
+
test('trims in place, prints TRIMDIMS to stdout, returns 0', () => {
|
|
42
|
+
const dir = scene();
|
|
43
|
+
const input = join(dir, 'in.png');
|
|
44
|
+
writeFileSync(input, craftOpaqueRect(100, 80, 10, 20, 30, 40));
|
|
45
|
+
const out = join(dir, 'out.png');
|
|
46
|
+
const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
47
|
+
const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
48
|
+
try {
|
|
49
|
+
const status = handleFidelityTrimCommand([input, out]);
|
|
50
|
+
expect(status).toBe(0);
|
|
51
|
+
const printed = stdout.mock.calls.map((c) => String(c[0])).join('');
|
|
52
|
+
expect(printed).toBe('TRIMDIMS 30 40 0 0 100 80\n');
|
|
53
|
+
expect(stderr).not.toHaveBeenCalled();
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
stdout.mockRestore();
|
|
57
|
+
stderr.mockRestore();
|
|
58
|
+
}
|
|
59
|
+
const trimmed = PNG.sync.read(readFileSync(out));
|
|
60
|
+
expect([trimmed.width, trimmed.height]).toEqual([30, 40]);
|
|
61
|
+
});
|
|
62
|
+
test('propagates the helper exit code and stderr on failure', () => {
|
|
63
|
+
const dir = scene();
|
|
64
|
+
const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
65
|
+
const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
66
|
+
try {
|
|
67
|
+
const status = handleFidelityTrimCommand([join(dir, 'missing.png'), join(dir, 'out.png')]);
|
|
68
|
+
expect(status).toBe(65);
|
|
69
|
+
const printed = stderr.mock.calls.map((c) => String(c[0])).join('');
|
|
70
|
+
expect(printed).toContain('could not read image at');
|
|
71
|
+
expect(stdout).not.toHaveBeenCalled();
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
stdout.mockRestore();
|
|
75
|
+
stderr.mockRestore();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=fidelity-trim.test.js.map
|
|
@@ -6,15 +6,20 @@
|
|
|
6
6
|
* reference-sized aligned copy, and the reported dimensions (rendered px /
|
|
7
7
|
* logical pt / reference px). It operates on PNGs regardless of how they were
|
|
8
8
|
* produced -- a SwiftUI `ImageRenderer` render or a Compose/Roborazzi render --
|
|
9
|
-
* so crop/align/dimension behavior cannot drift between platforms.
|
|
10
|
-
* paths run on the macOS runner fleet, so the pixel work is one CoreGraphics
|
|
11
|
-
* helper invoked in Swift's script-interpreter mode; the surrounding JS
|
|
12
|
-
* orchestration is spliced into each platform's generated runtime script.
|
|
9
|
+
* so crop/align/dimension behavior cannot drift between platforms.
|
|
13
10
|
*
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* The pixel work is the pure-JS trim/align helper in trim-align.ts, invoked
|
|
12
|
+
* through the ci binary (`ci fidelity-trim`) -- the one artifact present on
|
|
13
|
+
* every backend -- so iOS and Android run the SAME bytes on macOS and Linux
|
|
14
|
+
* alike. It replaced an embedded CoreGraphics/ImageIO helper that ran via
|
|
15
|
+
* `swift` in script-interpreter mode and therefore could not run on Linux
|
|
16
|
+
* workers (fidelity-linux 06); the bounding-box, fit-pad, and TRIMDIMS
|
|
17
|
+
* semantics are a faithful port of that helper, guarded by a pixel-identity
|
|
18
|
+
* corpus gate (trim-align-corpus.test.ts).
|
|
19
|
+
*
|
|
20
|
+
* This module is consumed by `ui-fidelity-render.ts` (iOS) and
|
|
21
|
+
* `ui-fidelity-render-android.ts`; the surrounding JS orchestration is spliced
|
|
22
|
+
* into each platform's generated runtime script.
|
|
18
23
|
*/
|
|
19
24
|
/**
|
|
20
25
|
* Runtime support for trimming the rendered output (package_source variants).
|
|
@@ -25,25 +30,15 @@
|
|
|
25
30
|
* non-transparent pixels, so the rendered artifact is the component itself,
|
|
26
31
|
* tight -- no canvas, no background ambiguity for the human comparing screens.
|
|
27
32
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* never fails an otherwise good render. This is prep, not scoring: it produces
|
|
33
|
-
* a tighter image for the human's judgment and computes nothing about
|
|
34
|
-
* fidelity.
|
|
35
|
-
*/
|
|
36
|
-
/**
|
|
37
|
-
* The CoreGraphics trim/align helper source, run via
|
|
38
|
-
* `swift <Trim.swift> <in> <out> <aligned> <ref>` (script-interpreter mode).
|
|
39
|
-
* Crops a PNG to its content bounding box (transparent or uniform-background
|
|
40
|
-
* margins), writes a reference-sized aligned copy, and prints a TRIMDIMS line.
|
|
33
|
+
* Best-effort: a trim failure is logged (warn-and-continue) and the untrimmed
|
|
34
|
+
* render is kept -- a cosmetic post-step never fails an otherwise good render.
|
|
35
|
+
* This is prep, not scoring: it produces a tighter image for the human's
|
|
36
|
+
* judgment and computes nothing about fidelity.
|
|
41
37
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
38
|
+
* NOTE: the comments inside the stage string ship inside generated runtime
|
|
39
|
+
* scripts. Keep them free of platform-toolchain references -- there is a unit
|
|
40
|
+
* gate asserting the stage never mentions the old interpreter.
|
|
45
41
|
*/
|
|
46
|
-
export declare const TRIM_HELPER_SWIFT_SOURCE: string;
|
|
47
42
|
export declare const RENDER_SCRIPT_TRIM_STAGE: string;
|
|
48
43
|
/** A rendered/reference/source dimension pair in pixels, as TRIMDIMS reports. */
|
|
49
44
|
export type TrimDimsPair = [number, number];
|
|
@@ -53,21 +48,46 @@ export interface TrimDims {
|
|
|
53
48
|
reference: TrimDimsPair;
|
|
54
49
|
source: TrimDimsPair;
|
|
55
50
|
}
|
|
51
|
+
/** How the stage will spawn the fidelity-trim helper. */
|
|
52
|
+
export interface TrimHelperInvocation {
|
|
53
|
+
cmd: string;
|
|
54
|
+
args: string[];
|
|
55
|
+
}
|
|
56
56
|
/**
|
|
57
|
-
* The
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* The platform-neutral helpers embedded in the trim-stage script.
|
|
58
|
+
* `parseTrimDims` reads the helper's TRIMDIMS stdout line; `roundTwo` is the
|
|
59
|
+
* logical-pt rounding primitive; `trimHelperInvocation` resolves how the
|
|
60
|
+
* generated script reaches `ci fidelity-trim` at run time, and
|
|
61
|
+
* `runTrimHelper` spawns it.
|
|
60
62
|
*/
|
|
61
63
|
export interface PostProcessorInternals {
|
|
62
64
|
parseTrimDims(stdout: string): TrimDims | null;
|
|
63
65
|
roundTwo(value: number): number;
|
|
66
|
+
trimHelperInvocation(args: string[]): TrimHelperInvocation;
|
|
67
|
+
runTrimHelper(args: string[]): {
|
|
68
|
+
status: number | null;
|
|
69
|
+
stdout: string;
|
|
70
|
+
stderr: string;
|
|
71
|
+
};
|
|
64
72
|
}
|
|
65
73
|
/**
|
|
66
74
|
* Evaluates the embedded trim-stage source and returns its pure helpers, so
|
|
67
75
|
* unit tests exercise exactly the code that ships inside the runtime script
|
|
68
|
-
* (mirrors getRenderScriptInternals in ui-fidelity-render.ts).
|
|
69
|
-
*
|
|
70
|
-
*
|
|
76
|
+
* (mirrors getRenderScriptInternals in ui-fidelity-render.ts). `cp` and
|
|
77
|
+
* `process` are injectable so the invocation/spawn seams are testable; the
|
|
78
|
+
* other trim-stage functions reference runtime globals (fs, path, ...) only
|
|
79
|
+
* when called, so defining them here is harmless.
|
|
80
|
+
*/
|
|
81
|
+
export declare function getPostProcessorInternals(overrides?: {
|
|
82
|
+
cp?: unknown;
|
|
83
|
+
processRef?: unknown;
|
|
84
|
+
}): PostProcessorInternals;
|
|
85
|
+
/**
|
|
86
|
+
* Resolves the on-disk JS entrypoint of this cibuild installation's CLI
|
|
87
|
+
* (dist/cli.cjs), for callers that spawn a generated render script directly
|
|
88
|
+
* (the preview harnesses) and must hand it a working CIBUILD_CLI_JS. Returns
|
|
89
|
+
* null when no built CLI exists nearby (e.g. running from src under jest) --
|
|
90
|
+
* the script then falls back to `ci` on PATH.
|
|
71
91
|
*/
|
|
72
|
-
export declare function
|
|
92
|
+
export declare function resolveCliJsPath(): string | null;
|
|
73
93
|
//# sourceMappingURL=render-post-processor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-post-processor.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/render-post-processor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"render-post-processor.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/render-post-processor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,wBAAwB,EAAE,MA+GtC,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5C,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAAC;IAC3D,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1F;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,CAAC,EAAE;IACpD,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,sBAAsB,CASzB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAqBhD"}
|
|
@@ -6,16 +6,24 @@
|
|
|
6
6
|
* reference-sized aligned copy, and the reported dimensions (rendered px /
|
|
7
7
|
* logical pt / reference px). It operates on PNGs regardless of how they were
|
|
8
8
|
* produced -- a SwiftUI `ImageRenderer` render or a Compose/Roborazzi render --
|
|
9
|
-
* so crop/align/dimension behavior cannot drift between platforms.
|
|
10
|
-
* paths run on the macOS runner fleet, so the pixel work is one CoreGraphics
|
|
11
|
-
* helper invoked in Swift's script-interpreter mode; the surrounding JS
|
|
12
|
-
* orchestration is spliced into each platform's generated runtime script.
|
|
9
|
+
* so crop/align/dimension behavior cannot drift between platforms.
|
|
13
10
|
*
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* The pixel work is the pure-JS trim/align helper in trim-align.ts, invoked
|
|
12
|
+
* through the ci binary (`ci fidelity-trim`) -- the one artifact present on
|
|
13
|
+
* every backend -- so iOS and Android run the SAME bytes on macOS and Linux
|
|
14
|
+
* alike. It replaced an embedded CoreGraphics/ImageIO helper that ran via
|
|
15
|
+
* `swift` in script-interpreter mode and therefore could not run on Linux
|
|
16
|
+
* workers (fidelity-linux 06); the bounding-box, fit-pad, and TRIMDIMS
|
|
17
|
+
* semantics are a faithful port of that helper, guarded by a pixel-identity
|
|
18
|
+
* corpus gate (trim-align-corpus.test.ts).
|
|
19
|
+
*
|
|
20
|
+
* This module is consumed by `ui-fidelity-render.ts` (iOS) and
|
|
21
|
+
* `ui-fidelity-render-android.ts`; the surrounding JS orchestration is spliced
|
|
22
|
+
* into each platform's generated runtime script.
|
|
18
23
|
*/
|
|
24
|
+
import { existsSync } from 'node:fs';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
19
27
|
/**
|
|
20
28
|
* Runtime support for trimming the rendered output (package_source variants).
|
|
21
29
|
*
|
|
@@ -25,197 +33,17 @@
|
|
|
25
33
|
* non-transparent pixels, so the rendered artifact is the component itself,
|
|
26
34
|
* tight -- no canvas, no background ambiguity for the human comparing screens.
|
|
27
35
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* never fails an otherwise good render. This is prep, not scoring: it produces
|
|
33
|
-
* a tighter image for the human's judgment and computes nothing about
|
|
34
|
-
* fidelity.
|
|
35
|
-
*/
|
|
36
|
-
/**
|
|
37
|
-
* The CoreGraphics trim/align helper source, run via
|
|
38
|
-
* `swift <Trim.swift> <in> <out> <aligned> <ref>` (script-interpreter mode).
|
|
39
|
-
* Crops a PNG to its content bounding box (transparent or uniform-background
|
|
40
|
-
* margins), writes a reference-sized aligned copy, and prints a TRIMDIMS line.
|
|
36
|
+
* Best-effort: a trim failure is logged (warn-and-continue) and the untrimmed
|
|
37
|
+
* render is kept -- a cosmetic post-step never fails an otherwise good render.
|
|
38
|
+
* This is prep, not scoring: it produces a tighter image for the human's
|
|
39
|
+
* judgment and computes nothing about fidelity.
|
|
41
40
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* NOTE: the comments inside the stage string ship inside generated runtime
|
|
42
|
+
* scripts. Keep them free of platform-toolchain references -- there is a unit
|
|
43
|
+
* gate asserting the stage never mentions the old interpreter.
|
|
45
44
|
*/
|
|
46
|
-
export const TRIM_HELPER_SWIFT_SOURCE = [
|
|
47
|
-
'import Foundation',
|
|
48
|
-
'import ImageIO',
|
|
49
|
-
'import CoreGraphics',
|
|
50
|
-
'import UniformTypeIdentifiers',
|
|
51
|
-
'',
|
|
52
|
-
'func loadCGImage(_ path: String) -> CGImage? {',
|
|
53
|
-
' guard let src = CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, nil),',
|
|
54
|
-
' let img = CGImageSourceCreateImageAtIndex(src, 0, nil) else { return nil }',
|
|
55
|
-
' return img',
|
|
56
|
-
'}',
|
|
57
|
-
'',
|
|
58
|
-
'func writePNG(_ image: CGImage, to path: String) -> Bool {',
|
|
59
|
-
' guard let dest = CGImageDestinationCreateWithURL(URL(fileURLWithPath: path) as CFURL, UTType.png.identifier as CFString, 1, nil) else { return false }',
|
|
60
|
-
' CGImageDestinationAddImage(dest, image, nil)',
|
|
61
|
-
' return CGImageDestinationFinalize(dest)',
|
|
62
|
-
'}',
|
|
63
|
-
'',
|
|
64
|
-
'// Draw `image` centered on a transparent reference-sized canvas, scaled to fit',
|
|
65
|
-
'// while PRESERVING its aspect ratio. Unlike `resize` (which stretches to exact',
|
|
66
|
-
'// dimensions and distorts proportions when the aspect ratios differ), this',
|
|
67
|
-
'// keeps the rendered shape honest: an aspect mismatch shows as transparent',
|
|
68
|
-
'// letterbox/pillarbox padding rather than a squished image, while the canvas',
|
|
69
|
-
'// stays exactly reference-sized so the pair still overlays 1:1.',
|
|
70
|
-
'func fitPad(_ image: CGImage, toWidth w: Int, height h: Int) -> CGImage? {',
|
|
71
|
-
' if w <= 0 || h <= 0 { return nil }',
|
|
72
|
-
' let bytesPerRow = w * 4',
|
|
73
|
-
' let space = CGColorSpaceCreateDeviceRGB()',
|
|
74
|
-
' guard let ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: space, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }',
|
|
75
|
-
' ctx.clear(CGRect(x: 0, y: 0, width: w, height: h))',
|
|
76
|
-
' ctx.interpolationQuality = .high',
|
|
77
|
-
' let scale = min(Double(w) / Double(image.width), Double(h) / Double(image.height))',
|
|
78
|
-
' let drawW = Double(image.width) * scale',
|
|
79
|
-
' let drawH = Double(image.height) * scale',
|
|
80
|
-
' let originX = (Double(w) - drawW) / 2.0',
|
|
81
|
-
' let originY = (Double(h) - drawH) / 2.0',
|
|
82
|
-
' ctx.draw(image, in: CGRect(x: originX, y: originY, width: drawW, height: drawH))',
|
|
83
|
-
' return ctx.makeImage()',
|
|
84
|
-
'}',
|
|
85
|
-
'',
|
|
86
|
-
'// Bounding box of content: pixels that are neither transparent nor part of a',
|
|
87
|
-
'// uniform solid background. Two passes, because the renderer forces the view',
|
|
88
|
-
'// into a fixed device frame whose margins are transparent: (1) bound the',
|
|
89
|
-
'// non-transparent pixels, then (2) detect a uniform opaque background from the',
|
|
90
|
-
'// CORNERS OF THAT OPAQUE BOX -- not the raw frame, whose corners are the',
|
|
91
|
-
'// transparent device margin -- and bound everything that is not that',
|
|
92
|
-
'// background. A wrapper that fills the canvas with .background(.white) is thus',
|
|
93
|
-
'// trimmed to its real content. When the opaque corners disagree, only',
|
|
94
|
-
'// transparency is trimmed. Top-left image coordinates.',
|
|
95
|
-
'func contentBoundingBox(_ image: CGImage) -> CGRect? {',
|
|
96
|
-
' let w = image.width, h = image.height',
|
|
97
|
-
' if w == 0 || h == 0 { return nil }',
|
|
98
|
-
' let bytesPerRow = w * 4',
|
|
99
|
-
' var data = [UInt8](repeating: 0, count: bytesPerRow * h)',
|
|
100
|
-
' let space = CGColorSpaceCreateDeviceRGB()',
|
|
101
|
-
' guard let ctx = CGContext(data: &data, width: w, height: h, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: space, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }',
|
|
102
|
-
' ctx.draw(image, in: CGRect(x: 0, y: 0, width: w, height: h))',
|
|
103
|
-
' // Tight tolerance: exact solid fills match, near-colored content does not.',
|
|
104
|
-
' let tolerance = 4',
|
|
105
|
-
' func sample(_ x: Int, _ y: Int) -> (Int, Int, Int, Int) {',
|
|
106
|
-
' let i = y * bytesPerRow + x * 4',
|
|
107
|
-
' return (Int(data[i]), Int(data[i + 1]), Int(data[i + 2]), Int(data[i + 3]))',
|
|
108
|
-
' }',
|
|
109
|
-
' // Pass 1: bounding box of all non-transparent pixels.',
|
|
110
|
-
' var aMinX = w, aMinY = h, aMaxX = -1, aMaxY = -1',
|
|
111
|
-
' for y in 0..<h {',
|
|
112
|
-
' let rowBase = y * bytesPerRow',
|
|
113
|
-
' for x in 0..<w {',
|
|
114
|
-
' if data[rowBase + x * 4 + 3] > 0 {',
|
|
115
|
-
' if x < aMinX { aMinX = x }',
|
|
116
|
-
' if x > aMaxX { aMaxX = x }',
|
|
117
|
-
' if y < aMinY { aMinY = y }',
|
|
118
|
-
' if y > aMaxY { aMaxY = y }',
|
|
119
|
-
' }',
|
|
120
|
-
' }',
|
|
121
|
-
' }',
|
|
122
|
-
' if aMaxX < 0 { return nil }',
|
|
123
|
-
' let opaqueBox = CGRect(x: aMinX, y: aMinY, width: aMaxX - aMinX + 1, height: aMaxY - aMinY + 1)',
|
|
124
|
-
' // Detect a uniform opaque background from the corners of the opaque box.',
|
|
125
|
-
' let corners = [sample(aMinX, aMinY), sample(aMaxX, aMinY), sample(aMinX, aMaxY), sample(aMaxX, aMaxY)]',
|
|
126
|
-
' var background: (Int, Int, Int, Int)? = corners[0]',
|
|
127
|
-
' for c in corners {',
|
|
128
|
-
' if let bg = background {',
|
|
129
|
-
' if c.3 == 0 || abs(c.0 - bg.0) > tolerance || abs(c.1 - bg.1) > tolerance || abs(c.2 - bg.2) > tolerance || abs(c.3 - bg.3) > tolerance {',
|
|
130
|
-
' background = nil',
|
|
131
|
-
' }',
|
|
132
|
-
' }',
|
|
133
|
-
' }',
|
|
134
|
-
' // No uniform background: the opaque box is the content.',
|
|
135
|
-
' guard let bg = background else { return opaqueBox }',
|
|
136
|
-
' // Pass 2: within the opaque box, bound everything that is not the background.',
|
|
137
|
-
' var minX = aMaxX + 1, minY = aMaxY + 1, maxX = aMinX - 1, maxY = aMinY - 1',
|
|
138
|
-
' for y in aMinY...aMaxY {',
|
|
139
|
-
' let rowBase = y * bytesPerRow',
|
|
140
|
-
' for x in aMinX...aMaxX {',
|
|
141
|
-
' let idx = rowBase + x * 4',
|
|
142
|
-
' let a = Int(data[idx + 3])',
|
|
143
|
-
' if a == 0 { continue }',
|
|
144
|
-
' let r = Int(data[idx]), g = Int(data[idx + 1]), b = Int(data[idx + 2])',
|
|
145
|
-
' if abs(r - bg.0) <= tolerance && abs(g - bg.1) <= tolerance && abs(b - bg.2) <= tolerance && abs(a - bg.3) <= tolerance { continue }',
|
|
146
|
-
' if x < minX { minX = x }',
|
|
147
|
-
' if x > maxX { maxX = x }',
|
|
148
|
-
' if y < minY { minY = y }',
|
|
149
|
-
' if y > maxY { maxY = y }',
|
|
150
|
-
' }',
|
|
151
|
-
' }',
|
|
152
|
-
' // All background, no distinct content: keep the opaque box, never crop to nothing.',
|
|
153
|
-
' if maxX < minX { return opaqueBox }',
|
|
154
|
-
' return CGRect(x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1)',
|
|
155
|
-
'}',
|
|
156
|
-
'',
|
|
157
|
-
'let arguments = CommandLine.arguments',
|
|
158
|
-
'guard arguments.count >= 3 else {',
|
|
159
|
-
' FileHandle.standardError.write(Data("usage: Trim <in.png> <out.png>".utf8))',
|
|
160
|
-
' exit(64)',
|
|
161
|
-
'}',
|
|
162
|
-
'let inputPath = arguments[1]',
|
|
163
|
-
'let outputPath = arguments[2]',
|
|
164
|
-
'guard let image = loadCGImage(inputPath) else {',
|
|
165
|
-
' FileHandle.standardError.write(Data(("could not read image at " + inputPath).utf8))',
|
|
166
|
-
' exit(65)',
|
|
167
|
-
'}',
|
|
168
|
-
'guard let box = contentBoundingBox(image) else {',
|
|
169
|
-
' FileHandle.standardError.write(Data("no content to trim (image is uniform)".utf8))',
|
|
170
|
-
' exit(66)',
|
|
171
|
-
'}',
|
|
172
|
-
'guard let cropped = image.cropping(to: box) else {',
|
|
173
|
-
' FileHandle.standardError.write(Data("could not crop image".utf8))',
|
|
174
|
-
' exit(67)',
|
|
175
|
-
'}',
|
|
176
|
-
'let croppedWidth = cropped.width',
|
|
177
|
-
'let croppedHeight = cropped.height',
|
|
178
|
-
'guard writePNG(cropped, to: outputPath) else {',
|
|
179
|
-
' FileHandle.standardError.write(Data(("could not write trimmed PNG to " + outputPath).utf8))',
|
|
180
|
-
' exit(68)',
|
|
181
|
-
'}',
|
|
182
|
-
'',
|
|
183
|
-
'// Optional aligned output: the trimmed image fit onto a reference-sized',
|
|
184
|
-
'// canvas, preserving aspect ratio (transparent letterbox/pillarbox padding',
|
|
185
|
-
'// when aspect ratios differ). A reference-sized, undistorted overlay pair.',
|
|
186
|
-
'// Best-effort; dimensions are reported either way.',
|
|
187
|
-
'var referenceWidth = 0',
|
|
188
|
-
'var referenceHeight = 0',
|
|
189
|
-
'if arguments.count >= 5 {',
|
|
190
|
-
' let alignedPath = arguments[3]',
|
|
191
|
-
' let referencePath = arguments[4]',
|
|
192
|
-
' if let reference = loadCGImage(referencePath) {',
|
|
193
|
-
' referenceWidth = reference.width',
|
|
194
|
-
' referenceHeight = reference.height',
|
|
195
|
-
' if let aligned = fitPad(cropped, toWidth: referenceWidth, height: referenceHeight) {',
|
|
196
|
-
' _ = writePNG(aligned, to: alignedPath)',
|
|
197
|
-
' }',
|
|
198
|
-
' }',
|
|
199
|
-
'}',
|
|
200
|
-
'',
|
|
201
|
-
'// Report: rendered (trimmed) px, reference px (0 0 when unavailable), then',
|
|
202
|
-
'// the pre-trim source px so the caller can log how much margin was removed.',
|
|
203
|
-
'print("TRIMDIMS " + String(croppedWidth) + " " + String(croppedHeight) + " " + String(referenceWidth) + " " + String(referenceHeight) + " " + String(image.width) + " " + String(image.height))'
|
|
204
|
-
].join('\n') + '\n';
|
|
205
45
|
export const RENDER_SCRIPT_TRIM_STAGE = '\n// ---- render trimming stage (package_source variants) ----\n' +
|
|
206
|
-
'var TRIM_HELPER_SWIFT = ' +
|
|
207
|
-
JSON.stringify(TRIM_HELPER_SWIFT_SOURCE) +
|
|
208
|
-
';\n' +
|
|
209
46
|
String.raw `
|
|
210
|
-
// Write the helper into the harness dir once (idempotent across screens).
|
|
211
|
-
function ensureTrimHelper(harnessDir) {
|
|
212
|
-
var helperPath = path.join(harnessDir, 'Trim.swift');
|
|
213
|
-
if (!fs.existsSync(helperPath)) {
|
|
214
|
-
fs.writeFileSync(helperPath, TRIM_HELPER_SWIFT);
|
|
215
|
-
}
|
|
216
|
-
return helperPath;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
47
|
// Round to two decimals, keeping the value a plain number.
|
|
220
48
|
function roundTwo(value) {
|
|
221
49
|
return Math.round(value * 100) / 100;
|
|
@@ -244,11 +72,48 @@ function parseTrimDims(stdout) {
|
|
|
244
72
|
return null;
|
|
245
73
|
}
|
|
246
74
|
|
|
75
|
+
// Resolve how to reach the ci CLI's fidelity-trim helper from inside this
|
|
76
|
+
// generated script. Three homes, in order:
|
|
77
|
+
// 1. CIBUILD_CLI_JS -- an explicit JS entrypoint (preview harnesses, tests),
|
|
78
|
+
// run under the same node executing this script.
|
|
79
|
+
// 2. The pkg-built ci binary re-executing itself -- the production runner
|
|
80
|
+
// path, where this script runs under the pkg bootstrap and
|
|
81
|
+
// process.execPath IS the ci binary.
|
|
82
|
+
// 3. A ci on PATH (brew/npm installs).
|
|
83
|
+
function trimHelperInvocation(args) {
|
|
84
|
+
if (process.env.CIBUILD_CLI_JS) {
|
|
85
|
+
return {
|
|
86
|
+
cmd: process.execPath,
|
|
87
|
+
args: [process.env.CIBUILD_CLI_JS, 'fidelity-trim'].concat(args),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (process.pkg) {
|
|
91
|
+
return { cmd: process.execPath, args: ['fidelity-trim'].concat(args) };
|
|
92
|
+
}
|
|
93
|
+
return { cmd: 'ci', args: ['fidelity-trim'].concat(args) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Run the trim/align helper (pure JS inside the ci binary; the argv, TRIMDIMS
|
|
97
|
+
// stdout line, and exit codes are the retired CoreGraphics helper's contract).
|
|
98
|
+
function runTrimHelper(args) {
|
|
99
|
+
var invocation = trimHelperInvocation(args);
|
|
100
|
+
var result = cp.spawnSync(invocation.cmd, invocation.args, {
|
|
101
|
+
encoding: 'utf-8',
|
|
102
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
103
|
+
});
|
|
104
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
105
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
106
|
+
if (result.error) {
|
|
107
|
+
result.status = result.status == null ? 127 : result.status;
|
|
108
|
+
result.stderr = (result.stderr || '') + String(result.error.message || result.error);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
247
113
|
// Crop the rendered PNG in place to its content bounding box, write an aligned
|
|
248
114
|
// copy resized to the reference, and record the dimensions on the entry.
|
|
249
115
|
// Best-effort: a failure is logged and the untrimmed render is kept.
|
|
250
|
-
function trimRenderedImage(entry, renderedPath
|
|
251
|
-
var helperPath = ensureTrimHelper(harnessDir);
|
|
116
|
+
function trimRenderedImage(entry, renderedPath) {
|
|
252
117
|
var alignedRelative = 'ui-fidelity/aligned/' + entry.screen + '.png';
|
|
253
118
|
var alignedPath = path.resolve(artifactsDir, alignedRelative);
|
|
254
119
|
var referencePath =
|
|
@@ -260,7 +125,7 @@ function trimRenderedImage(entry, renderedPath, harnessDir) {
|
|
|
260
125
|
} catch (mkdirError) {
|
|
261
126
|
// best effort -- the helper reports a missing output dir itself
|
|
262
127
|
}
|
|
263
|
-
var result =
|
|
128
|
+
var result = runTrimHelper([renderedPath, renderedPath, alignedPath, referencePath]);
|
|
264
129
|
if (result.status !== 0) {
|
|
265
130
|
logError(
|
|
266
131
|
'trim failed for ' + renderedPath +
|
|
@@ -291,13 +156,47 @@ function trimRenderedImage(entry, renderedPath, harnessDir) {
|
|
|
291
156
|
/**
|
|
292
157
|
* Evaluates the embedded trim-stage source and returns its pure helpers, so
|
|
293
158
|
* unit tests exercise exactly the code that ships inside the runtime script
|
|
294
|
-
* (mirrors getRenderScriptInternals in ui-fidelity-render.ts).
|
|
295
|
-
*
|
|
296
|
-
*
|
|
159
|
+
* (mirrors getRenderScriptInternals in ui-fidelity-render.ts). `cp` and
|
|
160
|
+
* `process` are injectable so the invocation/spawn seams are testable; the
|
|
161
|
+
* other trim-stage functions reference runtime globals (fs, path, ...) only
|
|
162
|
+
* when called, so defining them here is harmless.
|
|
163
|
+
*/
|
|
164
|
+
export function getPostProcessorInternals(overrides) {
|
|
165
|
+
const factory = new Function('cp', 'process', RENDER_SCRIPT_TRIM_STAGE +
|
|
166
|
+
'\nreturn { parseTrimDims: parseTrimDims, roundTwo: roundTwo, ' +
|
|
167
|
+
'trimHelperInvocation: trimHelperInvocation, runTrimHelper: runTrimHelper };');
|
|
168
|
+
return factory(overrides?.cp, overrides?.processRef ?? process);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Resolves the on-disk JS entrypoint of this cibuild installation's CLI
|
|
172
|
+
* (dist/cli.cjs), for callers that spawn a generated render script directly
|
|
173
|
+
* (the preview harnesses) and must hand it a working CIBUILD_CLI_JS. Returns
|
|
174
|
+
* null when no built CLI exists nearby (e.g. running from src under jest) --
|
|
175
|
+
* the script then falls back to `ci` on PATH.
|
|
297
176
|
*/
|
|
298
|
-
export function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
177
|
+
export function resolveCliJsPath() {
|
|
178
|
+
let moduleDir;
|
|
179
|
+
try {
|
|
180
|
+
moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const candidates = [
|
|
186
|
+
// Bundled: import.meta.url is shimmed to dist/cli.cjs itself.
|
|
187
|
+
join(moduleDir, 'cli.cjs'),
|
|
188
|
+
// Compiled layout: dist/src/yaml/steps/*.js -> dist/cli.cjs.
|
|
189
|
+
join(moduleDir, '..', '..', '..', 'cli.cjs'),
|
|
190
|
+
];
|
|
191
|
+
for (const candidate of candidates) {
|
|
192
|
+
try {
|
|
193
|
+
if (existsSync(candidate))
|
|
194
|
+
return candidate;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// keep looking
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
302
201
|
}
|
|
303
202
|
//# sourceMappingURL=render-post-processor.js.map
|