@felipe-software/react-native-gpui 0.0.0-placeholder → 1.0.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 (72) hide show
  1. package/.reanimated-prebuilt/react-native-reanimated.mjs +29373 -0
  2. package/LICENSE +21 -0
  3. package/README.md +658 -1
  4. package/cli/bin.ts +379 -0
  5. package/cli/bundle.ts +32 -0
  6. package/cli/commands/do.ts +213 -0
  7. package/cli/commands/flow.ts +283 -0
  8. package/cli/commands/get.ts +584 -0
  9. package/cli/commands/shot.ts +312 -0
  10. package/cli/commands/trace.ts +293 -0
  11. package/cli/host.ts +714 -0
  12. package/cli/hotReload.ts +210 -0
  13. package/cli/run.ts +66 -0
  14. package/cli/selectors.ts +205 -0
  15. package/dist/Animated.d.ts +120 -0
  16. package/dist/ChatList.d.ts +28 -0
  17. package/dist/Dimensions.d.ts +31 -0
  18. package/dist/StyleSheet.d.ts +18 -0
  19. package/dist/Window.d.ts +33 -0
  20. package/dist/apis.d.ts +165 -0
  21. package/dist/app-state.d.ts +27 -0
  22. package/dist/chat-list-types.d.ts +136 -0
  23. package/dist/colors.d.ts +32 -0
  24. package/dist/commands.d.ts +230 -0
  25. package/dist/components.d.ts +419 -0
  26. package/dist/hotModules.d.ts +1 -0
  27. package/dist/index.d.ts +31 -0
  28. package/dist/index.development.js +14876 -0
  29. package/dist/index.js +12 -0
  30. package/dist/keyboard.d.ts +140 -0
  31. package/dist/native-menu.d.ts +7 -0
  32. package/dist/platform-driver.d.ts +32 -0
  33. package/dist/platform-services.d.ts +35 -0
  34. package/dist/portal.d.ts +43 -0
  35. package/dist/raf.d.ts +4 -0
  36. package/dist/reanimated/colors-processor-shim.d.ts +10 -0
  37. package/dist/reanimated/reanimated-host.d.ts +10 -0
  38. package/dist/reanimated/seam.d.ts +34 -0
  39. package/dist/reanimated/ui-entry.d.ts +2 -0
  40. package/dist/reanimated/worklet-channel.d.ts +10 -0
  41. package/dist/reanimated/worklet-runtime.d.ts +466 -0
  42. package/dist/reanimated/worklets.d.ts +199 -0
  43. package/dist/reconciler.d.ts +102 -0
  44. package/dist/refresh.d.ts +1 -0
  45. package/dist/render.d.ts +37 -0
  46. package/dist/runtime.d.ts +235 -0
  47. package/dist/surfaces.d.ts +51 -0
  48. package/dist/types.d.ts +276 -0
  49. package/dist/ui-runtime.js +42276 -0
  50. package/dist/window-context.d.ts +7 -0
  51. package/dist/window-host.d.ts +41 -0
  52. package/dist/wire-delta.d.ts +20 -0
  53. package/package.json +177 -1
  54. package/scripts/bundle-app.mjs +88 -0
  55. package/scripts/conformance-utils.mjs +293 -0
  56. package/scripts/hot-update-alias-plugin.mjs +88 -0
  57. package/scripts/native-package-exports-plugin.mjs +235 -0
  58. package/scripts/pixel.mjs +152 -0
  59. package/scripts/png.mjs +198 -0
  60. package/scripts/react-refresh-swc.mjs +35 -0
  61. package/scripts/reanimated-bun-plugin.mjs +188 -0
  62. package/scripts/service-binary.d.mts +27 -0
  63. package/scripts/service-binary.mjs +100 -0
  64. package/src/reanimated/colors-processor-shim.ts +119 -0
  65. package/src/reanimated/reanimated-host.ts +69 -0
  66. package/src/reanimated/rngpui-reanimated-prebuilt.d.ts +7 -0
  67. package/src/reanimated/seam.ts +566 -0
  68. package/src/reanimated/ui-entry.ts +80 -0
  69. package/src/reanimated/worklet-channel.ts +64 -0
  70. package/src/reanimated/worklet-runtime.ts +2569 -0
  71. package/src/reanimated/worklets.ts +1334 -0
  72. package/src/refresh.ts +33 -0
@@ -0,0 +1,7 @@
1
+ export type WindowContextValue = {
2
+ windowId: number;
3
+ width: number;
4
+ height: number;
5
+ focused: boolean;
6
+ };
7
+ export declare const WindowContext: import("react").Context<WindowContextValue | null>;
@@ -0,0 +1,41 @@
1
+ import { type Container } from "./reconciler";
2
+ export type WindowKind = "normal" | "dialog" | "popup";
3
+ export type WindowPosition = "center" | "cursor" | {
4
+ x: number;
5
+ y: number;
6
+ relativeTo?: "parent" | "screen" | "cursor";
7
+ };
8
+ export type WindowCloseReason = "system" | "escape" | "blur" | "error";
9
+ export type WindowSpec = {
10
+ title: string;
11
+ width: number;
12
+ height: number;
13
+ minWidth?: number;
14
+ minHeight?: number;
15
+ position: WindowPosition;
16
+ kind: WindowKind;
17
+ resizable?: boolean;
18
+ alwaysOnTop?: boolean;
19
+ closeOnBlur?: boolean;
20
+ closeOnEscape?: boolean;
21
+ testID?: string;
22
+ };
23
+ export type WindowHostEvent = {
24
+ windowId: number;
25
+ event: "open" | "resize" | "close" | "focus" | "blur";
26
+ width?: number;
27
+ height?: number;
28
+ reason?: WindowCloseReason;
29
+ };
30
+ export interface WindowHost {
31
+ readonly windowId: number;
32
+ readonly container: Container;
33
+ open(spec: WindowSpec, onEvent: (event: WindowHostEvent) => void): void;
34
+ update(spec: WindowSpec): void;
35
+ focus(): void;
36
+ close(): void;
37
+ readonly closed: boolean;
38
+ }
39
+ export declare function dispatchWindowEvent(event: WindowHostEvent): void;
40
+ export declare function openWindowIds(): number[];
41
+ export declare function createWindowHost(width: number, height: number): WindowHost;
@@ -0,0 +1,20 @@
1
+ import type { SerializedNode } from "./runtime";
2
+ export type WireSourceCache = {
3
+ fileIds: Map<string, number>;
4
+ announcedIds: Set<number>;
5
+ nextId: number;
6
+ };
7
+ export declare function createWireSourceCache(): WireSourceCache;
8
+ export declare const sharedWireSources: WireSourceCache;
9
+ export declare function sameTopLevelTree(a: SerializedNode, b: SerializedNode): boolean;
10
+ export type WireDeltaStats = {
11
+ refs: number;
12
+ full: number;
13
+ };
14
+ type BigFields = {
15
+ text?: string;
16
+ src?: string;
17
+ };
18
+ export type BigFieldCache = Map<number, BigFields>;
19
+ export declare function toWireDelta(node: SerializedNode, sent: WeakSet<SerializedNode>, big: BigFieldCache, stats?: WireDeltaStats, sourceCache?: WireSourceCache): SerializedNode;
20
+ export {};
package/package.json CHANGED
@@ -1 +1,177 @@
1
- {"name":"@felipe-software/react-native-gpui","version":"0.0.0-placeholder","description":"Placeholder so npm trusted publishing can be configured. Real releases are published from CI.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/felipe-software/react-native-gpui.git"}}
1
+ {
2
+ "name": "@felipe-software/react-native-gpui",
3
+ "version": "1.0.1",
4
+ "description": "Write React Native components, render them natively with GPUI (Zed's GPU UI framework).",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./scripts/*": "./scripts/*",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "bin": {
18
+ "rngpui": "./cli/bin.ts"
19
+ },
20
+ "files": [
21
+ ".reanimated-prebuilt",
22
+ "cli",
23
+ "dist",
24
+ "scripts/bundle-app.mjs",
25
+ "scripts/conformance-utils.mjs",
26
+ "scripts/hot-update-alias-plugin.mjs",
27
+ "scripts/native-package-exports-plugin.mjs",
28
+ "scripts/pixel.mjs",
29
+ "scripts/png.mjs",
30
+ "scripts/react-refresh-swc.mjs",
31
+ "scripts/reanimated-bun-plugin.mjs",
32
+ "scripts/service-binary.mjs",
33
+ "scripts/service-binary.d.mts",
34
+ "src/reanimated",
35
+ "src/refresh.ts",
36
+ "README.md"
37
+ ],
38
+ "scripts": {
39
+ "build": "npm run prebuild:reanimated && npm run build:rust && npm run build:native && npm run build:ts",
40
+ "build:rust": "node scripts/build-rust.mjs",
41
+ "build:native": "node scripts/build-native.mjs",
42
+ "build:clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
43
+ "build:types": "tsc --emitDeclarationOnly",
44
+ "build:bundle": "bun build src/index.ts --outfile dist/index.js --target node --format esm --production --define:__DEV__=false --external react --external react/jsx-runtime --external react/jsx-dev-runtime && bun build src/index.ts --outfile dist/index.development.js --target node --format esm --define:__DEV__=true --define:process.env.NODE_ENV='\"development\"' --external react --external react/jsx-runtime --external react/jsx-dev-runtime && bun scripts/build-ui-runtime.mjs --force",
45
+ "build:ts": "npm run build:clean && npm run build:types && npm run build:bundle",
46
+ "typecheck": "tsc --noEmit",
47
+ "test": "bun run scripts/test-suite.mjs",
48
+ "test:unit": "bun run scripts/animated-driver-unit.mjs && bun run scripts/keyboard-navigation-unit.mjs && bun run scripts/surfaces-unit.mjs && bun run scripts/native-package-exports-unit.mjs && bun run scripts/wire-delta-unit.mjs && bun run scripts/worklet-runtime-unit.mjs && bun run scripts/appearance-serialize-unit.tsx && bun run scripts/semantic-serialize-unit.tsx && bun run scripts/pixel.test.mjs",
49
+ "test:animated-driver": "bun run scripts/animated-driver-unit.mjs",
50
+ "test:keyboard-navigation": "bun run scripts/keyboard-navigation-unit.mjs",
51
+ "test:surfaces": "bun run scripts/surfaces-unit.mjs",
52
+ "kitchen": "node scripts/run-example.mjs examples/kitchen-sink.tsx --interactive",
53
+ "conformance:animation": "node scripts/run-example.mjs examples/animation-conformance.tsx --timeout-ms 8000",
54
+ "conformance:animation:diff": "bun run scripts/animation-frame-diff.mjs",
55
+ "conformance:anim-overlay": "node scripts/anim-overlay-conformance.mjs",
56
+ "conformance:reanimated": "node scripts/reanimated-conformance.mjs",
57
+ "conformance:reanimated:scroll": "bun run scripts/reanimated-scroll-conformance.ts",
58
+ "conformance:offthread-stall": "node scripts/offthread-stall-conformance.mjs",
59
+ "conformance:native-scroll-react-stall": "node scripts/native-scroll-react-stall-conformance.mjs",
60
+ "conformance:dialog-reanimated": "bun run scripts/dialog-reanimated-conformance.mjs",
61
+ "conformance:sustained-reanimated": "bun run scripts/sustained-reanimated-conformance.mjs",
62
+ "prebuild:reanimated": "bun run scripts/prebuild-reanimated.mjs",
63
+ "conformance:webview:render": "bun run scripts/webview-render-conformance.mjs",
64
+ "conformance:webview:overlay": "bun run scripts/webview-overlay-conformance.mjs",
65
+ "conformance:card-corner-shadow": "bun run scripts/card-corner-shadow-conformance.mjs",
66
+ "conformance:opacity": "bun run scripts/check-opacity-conformance.mjs",
67
+ "conformance:transform": "bun run scripts/check-transform-conformance.mjs",
68
+ "conformance:opacity-ramp": "bun run scripts/check-opacity-ramp-conformance.mjs",
69
+ "conformance:diagnostics": "node scripts/diagnostics-conformance.mjs",
70
+ "conformance:context-menu": "node scripts/context-menu-conformance.mjs",
71
+ "conformance:file-picker": "bun run scripts/file-picker-conformance.ts",
72
+ "conformance:display-none": "bun run scripts/display-none-conformance.ts",
73
+ "conformance:inspector": "node scripts/inspector-conformance.mjs",
74
+ "conformance:hit-test": "node scripts/hit-test-conformance.mjs",
75
+ "conformance:occlusion": "node scripts/overlay-occlusion-conformance.mjs",
76
+ "conformance:chat-list": "node scripts/chat-list-conformance.mjs",
77
+ "conformance:multi-window": "node scripts/multi-window-conformance.mjs",
78
+ "perf:chat-list": "node scripts/chat-list-stress.mjs",
79
+ "conformance:input": "node scripts/input-conformance-driver.mjs",
80
+ "conformance:input-runtime": "bun run scripts/input-runtime-conformance.ts",
81
+ "probe:input-latency": "bun run scripts/input-latency-probe.ts",
82
+ "conformance:input:manual": "node scripts/run-example.mjs examples/input-conformance.tsx --interactive",
83
+ "conformance:list-group": "node scripts/list-group-conformance-driver.mjs",
84
+ "conformance:legend-100k": "bun run scripts/legend-list-100k-conformance.mjs",
85
+ "conformance:box-model": "RNGPUI_NO_ACTIVATE=1 RNGPUI_TEST_MODE=1 node scripts/run-example.mjs examples/box-model-conformance.tsx --timeout-ms 8000",
86
+ "conformance:negative-margin": "RNGPUI_NO_ACTIVATE=1 RNGPUI_TEST_MODE=1 node scripts/run-example.mjs examples/negative-margin-conformance.tsx --timeout-ms 8000",
87
+ "conformance:measure": "RNGPUI_NO_ACTIVATE=1 RNGPUI_TEST_MODE=1 node scripts/run-example.mjs examples/measure-conformance.tsx --timeout-ms 8000",
88
+ "conformance:native-layout": "RNGPUI_NO_ACTIVATE=1 RNGPUI_TEST_MODE=1 node scripts/run-example.mjs examples/native-layout-conformance.tsx --timeout-ms 8000",
89
+ "conformance:native-layout-animation": "RNGPUI_NO_ACTIVATE=1 RNGPUI_TEST_MODE=1 node scripts/run-example.mjs examples/native-layout-animation-conformance.tsx --timeout-ms 8000",
90
+ "conformance:portal": "node scripts/run-example.mjs examples/portal-conformance.tsx --interactive",
91
+ "conformance:raf-pacing": "RNGPUI_NO_ACTIVATE=1 RNGPUI_TEST_MODE=1 node scripts/run-example.mjs examples/raf-pacing-conformance.tsx --timeout-ms 15000",
92
+ "conformance:presentation-pacing": "bun run scripts/presentation-pacing-conformance.mjs --require-120hz",
93
+ "conformance:rounded-overflow": "bun run scripts/rounded-overflow-conformance.mjs",
94
+ "conformance:text-lines": "bun run scripts/text-lines-conformance.mjs",
95
+ "pixel-diff": "bun run scripts/pixel-diff.mjs",
96
+ "pixel-sample": "bun run scripts/pixel-sample.mjs",
97
+ "png-grid": "bun run scripts/png-grid.mjs",
98
+ "test:pixel": "bun run scripts/pixel.test.mjs",
99
+ "superconductor": "node scripts/run-example.mjs examples/superconductor.tsx --interactive",
100
+ "rngpui": "bun run cli/bin.ts",
101
+ "conformance:describe": "node scripts/describe-conformance.mjs",
102
+ "conformance:drive": "node scripts/drive-conformance.mjs",
103
+ "conformance:scroll-performance": "bun run scripts/scroll-performance-conformance.mjs",
104
+ "conformance:reload": "node scripts/reload-conformance.mjs",
105
+ "conformance:hot-reload": "node scripts/hot-reload-conformance.mjs",
106
+ "conformance:hot-reload-cli": "node scripts/hot-reload-cli-conformance.mjs",
107
+ "conformance:startup": "node scripts/startup-conformance.mjs",
108
+ "conformance:local-storage": "node scripts/local-storage-conformance.mjs",
109
+ "conformance:platform-services": "node scripts/platform-services-conformance.mjs",
110
+ "conformance:http-cache": "node scripts/http-cache-conformance.mjs",
111
+ "conformance:animated-image": "node scripts/animated-image-conformance.mjs",
112
+ "npm:set-version": "node scripts/npm-packages.mjs set-version",
113
+ "npm:stage-platform": "node scripts/npm-packages.mjs stage-platform",
114
+ "npm:pack": "node scripts/npm-packages.mjs pack"
115
+ },
116
+ "os": [
117
+ "darwin",
118
+ "linux"
119
+ ],
120
+ "cpu": [
121
+ "arm64",
122
+ "x64"
123
+ ],
124
+ "keywords": [
125
+ "react-native",
126
+ "gpui",
127
+ "zed",
128
+ "rust",
129
+ "desktop",
130
+ "macos",
131
+ "linux",
132
+ "wayland",
133
+ "x11",
134
+ "react-reconciler"
135
+ ],
136
+ "license": "MIT",
137
+ "peerDependencies": {
138
+ "react": ">=19.0.0"
139
+ },
140
+ "dependencies": {
141
+ "@swc/core": "1.15.40",
142
+ "react-refresh": "0.14.2",
143
+ "react-reconciler": "^0.31.0"
144
+ },
145
+ "devDependencies": {
146
+ "@babel/core": "^7.26.0",
147
+ "@babel/plugin-syntax-jsx": "^7.25.9",
148
+ "@babel/plugin-transform-async-generator-functions": "^7.26.0",
149
+ "@babel/plugin-transform-flow-strip-types": "^7.26.0",
150
+ "@babel/preset-typescript": "^7.26.0",
151
+ "@legendapp/list": "3.3.3",
152
+ "@types/node": "25.9.1",
153
+ "@types/react": "^19.0.0",
154
+ "@types/react-reconciler": "^0.31.0",
155
+ "esbuild": "^0.25.0",
156
+ "react": "^19.0.0",
157
+ "react-native-reanimated": "4.3.0",
158
+ "react-native-worklets": "0.8.1",
159
+ "typescript": "^5.7.0"
160
+ },
161
+ "repository": {
162
+ "type": "git",
163
+ "url": "git+https://github.com/felipe-software/react-native-gpui.git"
164
+ },
165
+ "homepage": "https://github.com/felipe-software/react-native-gpui#readme",
166
+ "bugs": {
167
+ "url": "https://github.com/felipe-software/react-native-gpui/issues"
168
+ },
169
+ "publishConfig": {
170
+ "access": "public"
171
+ },
172
+ "optionalDependencies": {
173
+ "@felipe-software/react-native-gpui-linux-x64": "1.0.1",
174
+ "@felipe-software/react-native-gpui-linux-arm64": "1.0.1",
175
+ "@felipe-software/react-native-gpui-darwin-arm64": "1.0.1"
176
+ }
177
+ }
@@ -0,0 +1,88 @@
1
+ // Bundle a single self-contained JS file for the embedded JavaScriptCore runtime (no module
2
+ // system, no Bun/node runtime). React + react-reconciler are bundled IN so the
3
+ // source bundle is self-contained.
4
+ //
5
+ // bun scripts/bundle-app.mjs [entry] [out.js]
6
+ //
7
+ // Bun is used only as the dev bundler here; the output runs under JavaScriptCore.
8
+ import { dirname, resolve, sep } from 'node:path'
9
+ import { existsSync } from 'node:fs'
10
+ import { spawnSync } from 'node:child_process'
11
+ import { reanimatedBunPlugin } from './reanimated-bun-plugin.mjs'
12
+ import { rngpuiHotUpdateAliasPlugin } from './hot-update-alias-plugin.mjs'
13
+ import { createReactRefreshSwcTransform } from './react-refresh-swc.mjs'
14
+ import { nativePackageExportsPlugin } from './native-package-exports-plugin.mjs'
15
+ import { isRepoCheckout } from './service-binary.mjs'
16
+
17
+ const root = resolve(import.meta.dirname, '..')
18
+ const args = process.argv.slice(2).filter((a) => !a.startsWith('--'))
19
+ const entry = args[0] ? resolve(args[0]) : resolve(root, 'examples/engine-smoke.tsx')
20
+ const outJs = args[1] ? resolve(args[1]) : '/tmp/rngpui-bundle.js'
21
+ const projectRoot = resolve(process.env.RNGPUI_PROJECT_ROOT || dirname(entry))
22
+ const mode = process.env.NODE_ENV || 'development'
23
+ const hotUpdate = process.env.RNGPUI_HOT_UPDATE === '1'
24
+ const refreshBootstrap = resolve(root, 'src/refresh.ts')
25
+ const refreshTransform = mode === 'development' ? createRefreshTransform() : null
26
+ const libraryEntry = libraryEntryFor(mode)
27
+
28
+ const result = await Bun.build({
29
+ entrypoints: [entry],
30
+ target: 'browser', // neutral: no node/bun builtins, self-contained
31
+ format: 'iife', // runs as a self-contained script
32
+ conditions: ['react-native'],
33
+ define: {
34
+ 'process.env.NODE_ENV': JSON.stringify(mode),
35
+ __DEV__: mode === 'development' ? 'true' : 'false',
36
+ 'import.meta.url': JSON.stringify('file://' + entry),
37
+ 'process.env.RNGPUI_INPUT_FIXTURE_APPEARANCE': JSON.stringify(
38
+ process.env.RNGPUI_INPUT_FIXTURE_APPEARANCE || '',
39
+ ),
40
+ },
41
+ plugins: [
42
+ ...(hotUpdate ? [rngpuiHotUpdateAliasPlugin()] : []),
43
+ reanimatedBunPlugin({ rngTsRoot: root, resolveRoot: projectRoot, libraryEntry, transformSource: refreshTransform }),
44
+ nativePackageExportsPlugin({ root: projectRoot, name: 'rngpui native package exports' }),
45
+ ],
46
+ sourcemap: 'none',
47
+ throw: false,
48
+ })
49
+
50
+ if (!result.success) {
51
+ console.error('[bundle-app] build failed:')
52
+ for (const log of result.logs) console.error(log)
53
+ process.exit(1)
54
+ }
55
+
56
+ const code = await result.outputs.find((o) => o.kind === 'entry-point').text()
57
+ await Bun.write(outJs, code)
58
+ console.log(`[bundle-app] ${entry}`)
59
+ console.log(`[bundle-app] wrote ${outJs} (${(code.length / 1024).toFixed(0)} KB, NODE_ENV=${mode})`)
60
+
61
+ // every app bundle needs the worklet/UI runtime bundle staged next to the service
62
+ // binary. mtime-cached, so usually a no-op. Installed packages ship it prebuilt.
63
+ if (isRepoCheckout(root) && !hotUpdate && !entry.endsWith('/reanimated/ui-entry.ts')) {
64
+ const ui = spawnSync(process.execPath, ['scripts/build-ui-runtime.mjs'], { cwd: root, stdio: 'inherit' })
65
+ if (ui.status !== 0) {
66
+ console.error('[bundle-app] build-ui-runtime failed')
67
+ process.exit(ui.status || 1)
68
+ }
69
+ }
70
+
71
+ function libraryEntryFor(bundleMode) {
72
+ const source = resolve(root, 'src/index.ts')
73
+ if (existsSync(source)) return source
74
+ const development = resolve(root, 'dist/index.development.js')
75
+ if (bundleMode === 'development' && existsSync(development)) return development
76
+ return resolve(root, 'dist/index.js')
77
+ }
78
+
79
+ function createRefreshTransform() {
80
+ const transform = createReactRefreshSwcTransform()
81
+ return async (source, { filename, isTs, isJsx }) => {
82
+ if (filename === refreshBootstrap || filename.includes(`${sep}node_modules${sep}`)) return source
83
+ const code = await transform(source, { filename, isTs, isJsx })
84
+ if (filename !== entry) return code
85
+ if (!hotUpdate) return `import ${JSON.stringify(refreshBootstrap)};\n${code}`
86
+ return `if (globalThis.__rngpuiRefreshReady !== true) throw new Error("RNGPUI Fast Refresh runtime is not installed");\n${code}`
87
+ }
88
+ }
@@ -0,0 +1,293 @@
1
+ import { join, resolve } from "node:path";
2
+ import { homedir, tmpdir } from "node:os";
3
+ import { execFile, execFileSync } from "node:child_process";
4
+ import { existsSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
+
6
+ export const cuaDriver =
7
+ process.env.CUA_DRIVER ||
8
+ (existsSync(join(homedir(), ".local/bin/cua-driver")) ? join(homedir(), ".local/bin/cua-driver") : "cua-driver");
9
+
10
+ export function conformanceEnv(extra = {}) {
11
+ return {
12
+ ...process.env,
13
+ RNGPUI_NO_ACTIVATE: "1",
14
+ RNGPUI_TEST_MODE: "1",
15
+ ...extra,
16
+ };
17
+ }
18
+
19
+ export async function waitForServicePid(pidPath, { timeoutMs = 5000, isFixtureExited } = {}) {
20
+ const deadline = Date.now() + timeoutMs;
21
+ let last = "";
22
+ while (Date.now() < deadline) {
23
+ if (existsSync(pidPath)) {
24
+ const pid = Number(readFileSync(pidPath, "utf8").trim());
25
+ if (Number.isFinite(pid) && pid > 0) {
26
+ try {
27
+ process.kill(pid, 0);
28
+ return pid;
29
+ } catch (error) {
30
+ last = error instanceof Error ? error.message : String(error);
31
+ }
32
+ }
33
+ }
34
+ if (isFixtureExited?.()) break;
35
+ await sleep(25);
36
+ }
37
+ throw new Error(`timed out waiting for service pid at ${pidPath}${last ? `: ${last}` : ""}`);
38
+ }
39
+
40
+ // The budget bounds SILENCE, not wall time. `progress` returns a token (the
41
+ // service log's size) and any change resets the clock, so a fixture merely
42
+ // starved by concurrent work keeps its time while a wedged one still dies
43
+ // promptly. An absolute deadline here is really a claim about how fast the
44
+ // machine is, and it is wrong on a busy one.
45
+ //
46
+ // A fixture that has EXITED is reported as such rather than as a missing window.
47
+ // The old code broke the loop on isFixtureExited and then threw the generic
48
+ // "GPUI window was not found", so a self-terminating example (one that runs its
49
+ // assertions and quits, like box-model-conformance) was indistinguishable from a
50
+ // renderer that could not open a window — and it got read as the latter.
51
+ export async function waitForWindow(match, { timeoutMs = 5000, isFixtureExited, progress } = {}) {
52
+ let deadline = Date.now() + timeoutMs;
53
+ let token = progress?.();
54
+ while (Date.now() < deadline) {
55
+ const window = listWindows().find(match);
56
+ if (window) return window;
57
+ if (isFixtureExited?.()) {
58
+ throw new Error("the fixture exited before opening a window");
59
+ }
60
+ await sleep(50);
61
+ const next = progress?.();
62
+ if (next !== token) {
63
+ token = next;
64
+ deadline = Date.now() + timeoutMs;
65
+ }
66
+ }
67
+ throw new Error(`GPUI window was not found (no progress for ${timeoutMs}ms)`);
68
+ }
69
+
70
+ export function linuxWindowRegistryDir() {
71
+ return join(process.env.XDG_RUNTIME_DIR || tmpdir(), "rngpui-windows");
72
+ }
73
+
74
+ function listLinuxWindows() {
75
+ const dir = linuxWindowRegistryDir();
76
+ if (!existsSync(dir)) return [];
77
+ return readdirSync(dir)
78
+ .filter((entry) => entry.endsWith(".json"))
79
+ .map((entry) => readLiveWindowRecord(join(dir, entry)))
80
+ .filter(Boolean)
81
+ .map((window) => ({
82
+ ...window,
83
+ bounds: { x: window.x, y: window.y, width: window.width, height: window.height },
84
+ }));
85
+ }
86
+
87
+ function readLiveWindowRecord(path) {
88
+ let record;
89
+ try {
90
+ record = JSON.parse(readFileSync(path, "utf8"));
91
+ } catch {
92
+ return null;
93
+ }
94
+ if (isLiveService(record.pid)) return record;
95
+ if (!existsSync(`/proc/${record.pid}`)) {
96
+ rmSync(path, { force: true });
97
+ rmSync(path.replace(/\.json$/, ".capture"), { force: true });
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function isLiveService(pid) {
103
+ try {
104
+ return readFileSync(`/proc/${pid}/comm`, "utf8").trim() === "rngpui-service";
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ export function listWindows() {
111
+ if (process.platform === "linux") return listLinuxWindows();
112
+ const swift = `
113
+ import Foundation
114
+ import CoreGraphics
115
+ let list = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as? [[String: Any]] ?? []
116
+ for window in list {
117
+ let owner = window[kCGWindowOwnerName as String] as? String ?? ""
118
+ let title = window[kCGWindowName as String] as? String ?? ""
119
+ let id = (window[kCGWindowNumber as String] as? NSNumber)?.intValue ?? 0
120
+ let pid = (window[kCGWindowOwnerPID as String] as? NSNumber)?.intValue ?? 0
121
+ let layer = (window[kCGWindowLayer as String] as? NSNumber)?.intValue ?? 0
122
+ let bounds = window[kCGWindowBounds as String] as? [String: Any] ?? [:]
123
+ let x = (bounds["X"] as? NSNumber)?.doubleValue ?? 0
124
+ let y = (bounds["Y"] as? NSNumber)?.doubleValue ?? 0
125
+ let width = (bounds["Width"] as? NSNumber)?.doubleValue ?? 0
126
+ let height = (bounds["Height"] as? NSNumber)?.doubleValue ?? 0
127
+ let alpha = (window[kCGWindowAlpha as String] as? NSNumber)?.doubleValue ?? -1
128
+ print("\\(id)\\t\\(pid)\\t\\(owner)\\t\\(title)\\t\\(layer)\\t\\(x)\\t\\(y)\\t\\(width)\\t\\(height)\\t\\(alpha)")
129
+ }
130
+ `;
131
+ const raw = execFileSync("swift", ["-e", swift], { encoding: "utf8", stdio: "pipe" });
132
+ return raw
133
+ .trim()
134
+ .split("\n")
135
+ .filter(Boolean)
136
+ .map((line) => {
137
+ const [id, pid, owner, title, layer, x, y, width, height, alpha] = line.split("\t");
138
+ return {
139
+ id: Number(id),
140
+ window_id: Number(id),
141
+ pid: Number(pid),
142
+ owner,
143
+ app_name: owner,
144
+ title,
145
+ layer: Number(layer),
146
+ x: Number(x),
147
+ y: Number(y),
148
+ width: Number(width),
149
+ height: Number(height),
150
+ alpha: Number(alpha),
151
+ bounds: {
152
+ x: Number(x),
153
+ y: Number(y),
154
+ width: Number(width),
155
+ height: Number(height),
156
+ },
157
+ };
158
+ })
159
+ .filter(
160
+ (window) =>
161
+ Number.isFinite(window.id) &&
162
+ window.id > 0 &&
163
+ Number.isFinite(window.pid) &&
164
+ window.pid > 0 &&
165
+ Number.isFinite(window.width) &&
166
+ window.width > 0 &&
167
+ Number.isFinite(window.height) &&
168
+ window.height > 0,
169
+ );
170
+ }
171
+
172
+ export function captureWindow(window, path) {
173
+ if (process.platform === "linux") {
174
+ captureLinuxWindow(window, path);
175
+ return;
176
+ }
177
+ execFileSync(
178
+ cuaDriver,
179
+ [
180
+ "call",
181
+ "get_window_state",
182
+ JSON.stringify({
183
+ pid: window.pid,
184
+ window_id: window.id ?? window.window_id,
185
+ capture_mode: "vision",
186
+ screenshot_out_file: path,
187
+ }),
188
+ ],
189
+ { stdio: "pipe" },
190
+ );
191
+ if (!existsSync(path)) throw new Error(`screenshot was not written at ${path}`);
192
+ }
193
+
194
+ function captureLinuxWindow(window, path) {
195
+ if (!window.capture) {
196
+ throw new Error(
197
+ `rngpui-service pid ${window.pid} cannot capture frames; start it with RNGPUI_FRAME_CAPTURE=1 (test and CLI launches enable it)`,
198
+ );
199
+ }
200
+ const target = resolve(path);
201
+ rmSync(target, { force: true });
202
+ const request = window.capture_request ?? join(linuxWindowRegistryDir(), `${window.pid}.capture`);
203
+ writeFileSync(`${request}.staging`, target);
204
+ renameSync(`${request}.staging`, request);
205
+ const deadline = Date.now() + 10_000;
206
+ while (!existsSync(target)) {
207
+ if (Date.now() > deadline) throw new Error(`rngpui-service pid ${window.pid} did not write a frame to ${target}`);
208
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
209
+ }
210
+ }
211
+
212
+ export function frontmostApp() {
213
+ return frontmostProcess().name;
214
+ }
215
+
216
+ export function frontmostProcess() {
217
+ if (process.platform !== "darwin") return { pid: 0, name: "" };
218
+ const raw = execFileSync(
219
+ "swift",
220
+ [
221
+ "-e",
222
+ `
223
+ import AppKit
224
+ let app = NSWorkspace.shared.frontmostApplication
225
+ print("\\(app?.processIdentifier ?? 0)\\t\\(app?.localizedName ?? "")")
226
+ `,
227
+ ],
228
+ {
229
+ encoding: "utf8",
230
+ stdio: "pipe",
231
+ },
232
+ ).trim();
233
+ const [pid, ...nameParts] = raw.split("\t");
234
+ return { pid: Number(pid), name: nameParts.join("\t") };
235
+ }
236
+
237
+ export function screenFrames() {
238
+ if (process.platform !== "darwin") return [];
239
+ const swift = `
240
+ import AppKit
241
+ for screen in NSScreen.screens {
242
+ let frame = screen.frame
243
+ print("\\(frame.origin.x)\\t\\(frame.origin.y)\\t\\(frame.size.width)\\t\\(frame.size.height)")
244
+ }
245
+ `;
246
+ return execFileSync("swift", ["-e", swift], { encoding: "utf8", stdio: "pipe" })
247
+ .trim()
248
+ .split("\n")
249
+ .filter(Boolean)
250
+ .map((line) => {
251
+ const [x, y, width, height] = line.split("\t").map(Number);
252
+ return { x, y, width, height };
253
+ });
254
+ }
255
+
256
+ export function assertWindowOffscreen(window, label = "GPUI test window") {
257
+ const frames = screenFrames();
258
+ const visibleFrame = frames.find((frame) => rectsIntersect(window.bounds ?? window, frame));
259
+ if (!visibleFrame) return;
260
+ throw new Error(
261
+ `${label} is on-screen at ${window.x},${window.y} ${window.width}x${window.height}; screen=${visibleFrame.x},${visibleFrame.y} ${visibleFrame.width}x${visibleFrame.height}`,
262
+ );
263
+ }
264
+
265
+ function rectsIntersect(a, b) {
266
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
267
+ }
268
+
269
+ export function execFileText(command, args, options = {}) {
270
+ return new Promise((resolve, reject) => {
271
+ const child = execFile(
272
+ command,
273
+ args,
274
+ {
275
+ encoding: "utf8",
276
+ cwd: options.cwd,
277
+ env: options.env,
278
+ },
279
+ (error, stdout, stderr) => {
280
+ if (error && options.reject !== false) {
281
+ reject(new Error(`${command} ${args.join(" ")} failed: ${stderr || error.message}`));
282
+ return;
283
+ }
284
+ resolve({ stdout, stderr, error });
285
+ },
286
+ );
287
+ if (options.input != null) child.stdin?.end(options.input);
288
+ });
289
+ }
290
+
291
+ export function sleep(ms) {
292
+ return new Promise((resolve) => setTimeout(resolve, ms));
293
+ }