simdeck 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.
- package/LICENSE +13 -0
- package/README.md +240 -0
- package/bin/simdeck.mjs +50 -0
- package/build/simdeck-bin +0 -0
- package/client/dist/assets/index-BL9Mcd6u.js +9 -0
- package/client/dist/assets/index-Cu4TL413.css +1 -0
- package/client/dist/assets/simulatorStream.worker-CH72C_tF.js +22 -0
- package/client/dist/index.html +28 -0
- package/package.json +82 -0
- package/packages/simdeck-test/dist/index.d.ts +75 -0
- package/packages/simdeck-test/dist/index.js +376 -0
- package/scripts/experimental/swiftui-preview.mjs +1438 -0
- package/scripts/postinstall.mjs +42 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{color-scheme:light dark;--bg: #1e1e1e;--surface: #252526;--surface-hover: #2d2d2e;--border: #3c3c3c;--border-subtle: #333333;--text: #cccccc;--text-secondary: #888888;--text-muted: #666666;--accent: #0078d4;--accent-text: #ffffff;--success: #4ec9b0;--success-bg: rgba(78, 201, 176, .12);--error: #f14c4c;--error-bg: rgba(241, 76, 76, .1);--canvas-bg: #1a1a1a;--canvas-dot: rgba(255, 255, 255, .04);--bezel: #111111;--bezel-highlight: rgba(255, 255, 255, .06);--screen-bg: #000000}@media(prefers-color-scheme:light){:root{--bg: #f3f3f3;--surface: #ffffff;--surface-hover: #e8e8e8;--border: #d4d4d4;--border-subtle: #e0e0e0;--text: #1e1e1e;--text-secondary: #656565;--text-muted: #999999;--canvas-bg: #e0e0e0;--canvas-dot: rgba(0, 0, 0, .05);--bezel: #1a1a1a;--bezel-highlight: rgba(255, 255, 255, .08);--error: #cd3131;--error-bg: rgba(205, 49, 49, .08);--success: #16825d;--success-bg: rgba(22, 130, 93, .08)}}*,*:before,*:after{box-sizing:border-box;margin:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,system-ui,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}button,input{font:inherit}button{cursor:pointer}.app{display:grid;grid-template-rows:auto 1fr;height:100vh;height:100dvh;overflow:hidden}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;height:40px;padding:0 8px;background:var(--surface);border-bottom:1px solid var(--border);-webkit-user-select:none;user-select:none;flex-shrink:0}.toolbar-left,.toolbar-right{display:flex;align-items:center;gap:6px;min-width:0}.toolbar-left{flex:1;min-width:0}.toolbar-sim-info{display:flex;align-items:center;gap:8px;min-width:0;overflow:hidden}.toolbar-sim-copy{display:flex;flex-direction:column;gap:1px;min-width:0}.toolbar-sim-title-row{display:flex;align-items:center;gap:6px;min-width:0}.toolbar-sim-name{font-weight:600;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.toolbar-sim-detail{color:var(--text-secondary);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.toolbar-status-dot{width:8px;height:8px}.toolbar-actions{display:flex;align-items:center;gap:2px;min-width:0}.main{display:flex;overflow:hidden;min-height:0;min-width:0;width:100%;height:100%}.viewport-zoom-dock{position:absolute;right:16px;bottom:16px;z-index:8}@media(max-width:600px){.app{grid-template-rows:auto minmax(0,1fr)}.toolbar{height:auto;min-height:40px;flex-wrap:wrap;align-items:stretch;gap:6px;padding:6px}.toolbar-left,.toolbar-right{width:100%}.toolbar-left{display:grid;grid-template-columns:auto auto minmax(0,1fr)}.toolbar-right{overflow-x:auto;padding-bottom:2px;scrollbar-width:none}.toolbar-right::-webkit-scrollbar{display:none}.toolbar-actions{width:max-content;min-width:100%}.main{flex-direction:column}.debug-grid{grid-template-columns:1fr}.debug-row-wide{grid-column:auto}.viewport-zoom-dock{right:12px;bottom:12px;max-width:calc(100vw - 24px)}.menu-popover{width:min(260px,calc(100vw - 16px))}.hierarchy-panel{width:100%;min-width:0;max-width:none;max-height:min(42dvh,360px);border-right:0;border-bottom:1px solid var(--border)}.toolbar-sim-detail{display:none}}.tbtn{display:inline-flex;align-items:center;justify-content:center;gap:4px;height:26px;min-width:26px;padding:0 8px;border:none;border-radius:5px;background:transparent;color:var(--text);font-size:12px;white-space:nowrap;transition:background .1s}.tbtn:hover,.tbtn.active{background:var(--surface-hover)}.tbtn:disabled{cursor:default;opacity:.45}.tbtn:disabled:hover{background:transparent}.tbtn.accent{background:var(--accent);color:var(--accent-text);font-weight:600}.tbtn.accent:hover{filter:brightness(1.15)}.icon-btn{padding:0}.state-dot{width:6px;height:6px;border-radius:50%;background:var(--text-muted);flex-shrink:0}.state-dot.booted{background:var(--success)}.zoom-controls{display:flex;align-items:center;gap:2px}.zoom-controls-floating{gap:4px;flex-wrap:wrap;justify-content:flex-end;padding:6px;border:1px solid var(--border);border-radius:10px;background:color-mix(in srgb,var(--surface) 94%,transparent);box-shadow:0 12px 24px #0003;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.zoom-label{font-size:11px;font-variant-numeric:tabular-nums;color:var(--text-secondary);min-width:36px;text-align:center;-webkit-user-select:none;user-select:none}.debug-panel{padding:10px 12px 12px;border:1px solid var(--border);border-radius:8px;background:color-mix(in srgb,var(--surface) 96%,transparent);box-shadow:0 12px 32px #00000047;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.debug-panel-popover{position:absolute;top:calc(100% + 8px);right:0;width:min(360px,calc(100vw - 16px));z-index:30}.debug-panel-inline{width:100%;box-shadow:none;-webkit-backdrop-filter:none;backdrop-filter:none}.debug-panel-header{margin-bottom:10px;color:var(--text-secondary);font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.debug-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px}.debug-row{min-width:0}.debug-row-wide{grid-column:1 / -1}.debug-label{margin-bottom:2px;color:var(--text-muted);font-size:10px;font-weight:600;letter-spacing:.04em;text-transform:uppercase}.debug-value{color:var(--text);font-size:12px;font-variant-numeric:tabular-nums}.debug-value-wrap{line-height:1.4;white-space:normal;word-break:break-word}.menu-wrap{position:relative}.menu-popover{position:absolute;top:calc(100% + 6px);left:0;width:260px;max-height:min(72vh,560px);display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 12px 32px #00000047;overflow:hidden;z-index:20}.sidebar-search{width:100%;padding:8px 12px;border:none;border-bottom:1px solid var(--border-subtle);background:transparent;color:var(--text);outline:none;font-size:12px;flex-shrink:0}.sidebar-search:focus{background:var(--surface-hover)}.sidebar-search::placeholder{color:var(--text-muted)}.sim-list{flex:1;overflow-y:auto;padding:4px;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.sim-list::-webkit-scrollbar{width:6px}.sim-list::-webkit-scrollbar-track{background:transparent}.sim-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.menu-divider{height:1px;background:var(--border-subtle)}.menu-actions{display:flex;flex-direction:column;padding:4px}.menu-debug-panel{padding:6px 4px 4px}.menu-action{width:100%;padding:8px 10px;border:none;border-radius:6px;background:transparent;color:var(--text);text-align:left;font-size:12px;transition:background .1s}.menu-action:hover{background:var(--surface-hover)}.sim-item{display:flex;flex-direction:column;gap:2px;width:100%;padding:7px 10px;border:none;border-radius:6px;background:transparent;color:inherit;text-align:left;transition:background .1s}.sim-item:hover{background:var(--surface-hover)}.sim-item.selected{background:var(--accent);color:#fff}.sim-item.selected .sim-item-meta{color:#ffffffb3}.sim-item.selected .state-dot{background:#ffffff80}.sim-item.selected .state-dot.booted{background:#80f0c8}.sim-item-name{font-size:12px;font-weight:600;line-height:1.3}.sim-item-meta{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text-secondary);line-height:1.3}.list-empty{padding:20px 12px;text-align:center;color:var(--text-muted);font-size:12px}.hierarchy-panel{position:relative;flex:0 0 auto;min-width:240px;max-width:55vw;display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--surface);overflow:hidden}.hierarchy-tools{display:flex;align-items:center;gap:4px;min-height:34px;padding:4px 6px;border-bottom:1px solid var(--border-subtle)}.hierarchy-tools .tbtn.active{color:var(--accent-text);background:var(--accent)}.hierarchy-header{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:52px;padding:9px 10px 9px 12px;border-bottom:1px solid var(--border-subtle)}.hierarchy-title{color:var(--text);font-size:12px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.hierarchy-subtitle{max-width:190px;overflow:hidden;color:var(--text-secondary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.hierarchy-tree{flex:1;overflow:auto;padding:4px;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.hierarchy-source{display:flex;align-items:center;gap:6px;min-height:26px;padding:3px 6px 6px;color:var(--text-muted);font-size:10px}.hierarchy-source-switcher{display:inline-flex;align-items:center;gap:4px;flex:0 0 auto}.hierarchy-source-pill{display:inline-flex;align-items:center;max-width:150px;overflow:hidden;padding:2px 6px;border:1px solid var(--border-subtle);border-radius:999px;background:color-mix(in srgb,var(--surface-hover) 70%,transparent);color:var(--text-secondary);font-weight:700;letter-spacing:.03em;line-height:1.2;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap}.hierarchy-source-pill:not(:disabled):hover{background:var(--surface-hover);color:var(--text)}.hierarchy-source-pill.active{border-color:color-mix(in srgb,var(--accent) 58%,var(--border));background:color-mix(in srgb,var(--accent) 18%,transparent);color:var(--text)}.hierarchy-source-pill.source-in-app-inspector{border-color:color-mix(in srgb,var(--success) 50%,var(--border));background:var(--success-bg);color:color-mix(in srgb,var(--success) 82%,var(--text))}.hierarchy-source-pill.source-nativescript{border-color:color-mix(in srgb,#65d2ff 55%,var(--border));background:color-mix(in srgb,#65d2ff 16%,transparent);color:color-mix(in srgb,#65d2ff 82%,var(--text))}.hierarchy-source-pill.source-react-native{border-color:color-mix(in srgb,#61dafb 50%,var(--border));background:color-mix(in srgb,#61dafb 14%,transparent);color:color-mix(in srgb,#61dafb 78%,var(--text))}.hierarchy-source-pill.source-swiftui{border-color:color-mix(in srgb,#ff6b9d 50%,var(--border));background:color-mix(in srgb,#ff6b9d 14%,transparent);color:color-mix(in srgb,#ff6b9d 82%,var(--text))}.hierarchy-source-pill.source-native-ax{border-color:color-mix(in srgb,#d7ba7d 55%,var(--border));background:color-mix(in srgb,#d7ba7d 13%,transparent);color:color-mix(in srgb,#d7ba7d 82%,var(--text))}.hierarchy-source-pill.active{box-shadow:inset 0 0 0 1px currentColor}.hierarchy-source-note{min-width:0;overflow:hidden;color:var(--text-muted);text-overflow:ellipsis;white-space:nowrap}.hierarchy-node{display:flex;align-items:center;gap:2px;width:max-content;min-width:100%;min-height:26px;padding-block:1px;padding-right:8px;border-radius:5px;color:var(--text)}.hierarchy-node:hover{background:var(--surface-hover)}.hierarchy-node.selected{background:var(--accent);color:var(--accent-text)}.hierarchy-disclosure{width:16px;height:22px;flex:0 0 auto;padding:0;border:none;background:transparent;color:var(--text-secondary);font-size:11px;line-height:1}.hierarchy-disclosure.empty{visibility:hidden}.hierarchy-node-main{display:flex;align-items:center;gap:7px;flex:0 0 auto;width:max-content;height:24px;padding:0;border:none;background:transparent;color:inherit;text-align:left}.hierarchy-node-kind{color:var(--success);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;white-space:nowrap}.hierarchy-node-chain{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:16px;padding:0 5px;border:1px solid var(--border-subtle);border-radius:999px;color:var(--text-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;line-height:1;white-space:nowrap}.hierarchy-node-chain-path{max-width:260px;overflow:hidden;color:var(--text-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;text-overflow:ellipsis;white-space:nowrap}.hierarchy-node.selected .hierarchy-node-kind{color:color-mix(in srgb,var(--accent-text) 82%,var(--success))}.hierarchy-node-text{color:inherit;font-size:12px;white-space:nowrap}.hierarchy-node-source{color:var(--text-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;white-space:nowrap}.hierarchy-node.selected .hierarchy-node-source{color:color-mix(in srgb,var(--accent-text) 72%,transparent)}.hierarchy-empty{padding:18px 10px;color:var(--text-muted);font-size:12px;line-height:1.4;text-align:center}.hierarchy-empty.error{color:var(--error)}.hierarchy-details-wrap{position:relative;flex:0 0 auto;min-height:132px;max-height:60%;overflow:hidden;border-top:1px solid var(--border-subtle);background:color-mix(in srgb,var(--surface) 94%,var(--bg))}.hierarchy-details{height:100%;overflow:auto;padding:12px}.hierarchy-details-title{margin-bottom:8px;color:var(--text-secondary);font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.hierarchy-detail-row{display:grid;grid-template-columns:88px minmax(0,1fr);gap:8px;padding:3px 0;font-size:11px;line-height:1.35}.hierarchy-detail-label{color:var(--text-muted)}.hierarchy-detail-value{overflow-wrap:anywhere;color:var(--text)}.uikit-script{margin-top:14px;padding-top:12px;border-top:1px solid var(--border-subtle)}.uikit-script-header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:8px}.uikit-script-title{color:var(--text-secondary);font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.uikit-script-target,.uikit-script-result{color:var(--text-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;line-height:1.35;overflow-wrap:anywhere}.uikit-script-run{padding:6px 8px;border:1px solid var(--border-subtle);border-radius:6px;background:color-mix(in srgb,var(--surface) 88%,var(--bg));color:var(--text);cursor:pointer;font-size:11px;font-weight:650}.uikit-script-run:hover:not(:disabled){border-color:var(--accent)}.uikit-script-run:disabled{cursor:default;opacity:.55}.uikit-script-form{display:grid;gap:6px}.uikit-script-input{width:100%;min-height:58px;resize:vertical;padding:7px 8px;border:1px solid var(--border-subtle);border-radius:7px;outline:none;background:var(--bg);color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px}.uikit-script-input:focus{border-color:var(--accent)}.uikit-script-result{margin-top:7px}.uikit-script-error{margin-top:7px;color:var(--error);font-size:11px;line-height:1.35}.console-panel{flex:1;overflow:auto;overflow-anchor:none;padding:6px 0;background:color-mix(in srgb,var(--surface) 92%,var(--bg));color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;line-height:1.35;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.console-filterbar{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:5px;overflow:visible;padding:6px 8px;border-bottom:1px solid var(--border-subtle);background:color-mix(in srgb,var(--surface) 96%,var(--bg))}.console-text-filterbar{flex:0 0 auto;padding:6px 8px;border-bottom:1px solid var(--border-subtle);background:color-mix(in srgb,var(--surface) 94%,var(--bg))}.console-text-filter{width:100%;height:26px;padding:0 8px;border:1px solid var(--border-subtle);border-radius:6px;outline:none;background:var(--bg);color:var(--text);font-size:12px}.console-text-filter:focus{border-color:color-mix(in srgb,var(--accent) 60%,var(--border))}.console-text-filter::placeholder{color:var(--text-muted)}.console-filter-group{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.console-filter-label{margin-right:2px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.console-filter-chip{max-width:120px;overflow:hidden;padding:3px 7px;border:1px solid var(--border-subtle);border-radius:999px;background:transparent;color:var(--text-secondary);font-size:10px;line-height:1.2;text-overflow:ellipsis;white-space:nowrap}.console-filter-chip.active{border-color:color-mix(in srgb,var(--accent) 55%,var(--border));background:color-mix(in srgb,var(--accent) 18%,transparent);color:var(--text)}.console-filter-chip.level-error.active{border-color:color-mix(in srgb,var(--error) 70%,var(--border));background:color-mix(in srgb,var(--error) 20%,transparent);color:color-mix(in srgb,var(--error) 78%,var(--text))}.console-filter-chip.level-default.active{border-color:color-mix(in srgb,var(--text-secondary) 70%,var(--border));background:color-mix(in srgb,var(--text-secondary) 18%,transparent)}.console-filter-chip.level-info.active{border-color:color-mix(in srgb,var(--success) 70%,var(--border));background:var(--success-bg);color:color-mix(in srgb,var(--success) 78%,var(--text))}.console-filter-chip.level-debug.active{border-color:color-mix(in srgb,#d7ba7d 70%,var(--border));background:color-mix(in srgb,#d7ba7d 18%,transparent);color:color-mix(in srgb,#d7ba7d 78%,var(--text))}.console-service-menu-wrap{position:relative;flex:0 0 auto}.console-service-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:32;width:min(280px,70vw);max-height:min(360px,58vh);overflow:auto;padding:5px;border:1px solid var(--border);border-radius:8px;background:var(--surface);box-shadow:0 12px 32px #00000047;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.console-service-empty{padding:12px;color:var(--text-muted);font-size:11px;text-align:center}.console-service-option{display:flex;align-items:center;gap:8px;width:100%;min-height:28px;padding:5px 7px;border:none;border-radius:5px;background:transparent;color:var(--text);font-size:11px;text-align:left}.console-service-option:hover:not(:disabled){background:var(--surface-hover)}.console-service-option:disabled{cursor:default}.console-service-check{width:12px;height:12px;flex:0 0 auto;border:1px solid var(--border);border-radius:3px}.console-service-check.checked{border-color:var(--accent);background:var(--accent);box-shadow:inset 0 0 0 2px var(--surface)}.console-service-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.console-service-pin{color:var(--success);font-size:10px;text-transform:uppercase}.console-row{display:block;padding:3px 10px;border-bottom:1px solid color-mix(in srgb,var(--border-subtle) 55%,transparent)}.console-process{display:inline;margin-right:7px;color:var(--console-service-color);font-weight:700}.console-message{display:inline;overflow-wrap:anywhere;color:var(--text)}.hierarchy-resize-x{position:absolute;top:0;right:-3px;bottom:0;z-index:4;width:6px;cursor:col-resize}.hierarchy-resize-x:hover{background:color-mix(in srgb,var(--accent) 35%,transparent)}.hierarchy-resize-y{position:absolute;top:-3px;right:0;left:0;z-index:4;height:6px;cursor:row-resize}.hierarchy-resize-y:hover{background:color-mix(in srgb,var(--accent) 35%,transparent)}.is-resizing{cursor:grabbing;-webkit-user-select:none;user-select:none}.canvas{position:relative;overflow:hidden;display:flex;flex:1;align-items:center;justify-content:center;-webkit-user-select:none;user-select:none;min-height:0;min-width:0;width:100%;height:100%;overscroll-behavior:none;background-color:var(--canvas-bg);background-image:radial-gradient(var(--canvas-dot) 1px,transparent 1px);background-size:16px 16px}.canvas.pan-enabled{cursor:grab}.canvas.panning{cursor:grabbing}.device-anchor{transform-origin:center center;will-change:transform}.device-anchor-loading{opacity:0;pointer-events:none}.device-frame{position:relative}.device-presentation{position:relative;transform-origin:top left;will-change:transform}.device-anchor.animated{transition:transform .18s ease}.device-bezel{position:relative;background:var(--bezel);border-radius:48px;padding:14px;box-shadow:inset 0 0 0 1px var(--bezel-highlight)}.device-shell{position:relative}.device-chrome{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;pointer-events:none;-webkit-user-drag:none;z-index:2}.pan-enabled .device-bezel,.pan-enabled .device-shell{cursor:grab}.panning .device-bezel,.panning .device-shell{cursor:grabbing}.device-screen{width:320px;background:var(--screen-bg);border-radius:36px;overflow:hidden;cursor:crosshair;touch-action:none;position:relative;z-index:1}.device-screen.chrome-screen{position:absolute;width:auto;border-radius:0}.stream-canvas{position:absolute;inset:0;display:block;width:100%;height:100%;background:#000;pointer-events:none}.accessibility-picker-layer{position:absolute;inset:0;z-index:3;cursor:crosshair;touch-action:none}.accessibility-overlay{position:absolute;inset:0;z-index:2;pointer-events:none}.touch-interaction-overlay{position:absolute;inset:0;z-index:4;pointer-events:none}.touch-indicator{position:absolute;width:30px;height:30px;margin:-15px 0 0 -15px;border:2px solid rgba(245,245,245,.92);border-radius:999px;background:#ffffff2e;box-shadow:0 0 0 1px #00000070,0 0 14px #ffffff42;transform:translateZ(0) scale(.86);opacity:.9;transition:opacity .18s ease,transform .18s ease}.touch-indicator-began,.touch-indicator-moved{transform:translateZ(0) scale(1)}.touch-indicator-ended,.touch-indicator-cancelled{opacity:0;transform:translateZ(0) scale(1.55)}.accessibility-rect{position:absolute;min-width:8px;min-height:8px;border:1px solid var(--success);background:color-mix(in srgb,var(--success) 16%,transparent);box-shadow:0 0 0 1px #00000047}.accessibility-rect.hovered{border-style:dashed;background:color-mix(in srgb,var(--accent) 12%,transparent)}.accessibility-rect.selected{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 18%,transparent)}.accessibility-rect span{position:absolute;left:-1px;bottom:calc(100% + 3px);max-width:180px;overflow:hidden;padding:2px 5px;border-radius:4px;background:var(--accent);color:var(--accent-text);font-size:10px;line-height:1.2;text-overflow:ellipsis;white-space:nowrap}.screen-overlay{position:absolute;inset:0;display:grid;place-items:center;color:#555;font-size:13px;padding:24px;text-align:center}.canvas-empty{color:var(--text-muted);font-size:13px;text-align:center}.canvas-loading{position:absolute;inset:0;z-index:12;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--text-secondary);font-size:13px;text-align:center;pointer-events:none}.canvas-loading p{margin:0}.loading-spinner{width:28px;height:28px;border:2px solid color-mix(in srgb,var(--text-muted) 25%,transparent);border-top-color:var(--accent);border-radius:50%;animation:spin .78s linear infinite}.debug-overlay{position:absolute;top:12px;right:12px;z-index:14;width:min(360px,calc(100% - 24px));pointer-events:auto}.debug-overlay .debug-panel-inline{box-shadow:0 12px 32px #00000047;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}@media(max-width:600px){.viewport-zoom-dock{right:8px;bottom:8px;left:auto;width:max-content;max-width:calc(100vw - 16px)}.zoom-controls-floating{width:max-content;max-width:100%;justify-content:flex-end;overflow-x:auto;padding:5px;border-radius:8px}.zoom-controls-floating .tbtn{flex:0 0 auto}.debug-overlay{top:8px;right:8px;left:8px;width:auto;max-height:calc(100% - 72px);overflow:auto}.device-bezel{border-radius:34px;padding:10px}.device-screen{width:min(320px,calc(100vw - 44px))}.accessibility-rect span{max-width:120px}}@keyframes spin{to{transform:rotate(360deg)}}.error-msg{color:var(--error);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0;max-width:220px}.muted{color:var(--text-muted)}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
(function(){"use strict";function Q(e,r){if(!e.length)return r;const t=new Uint8Array(e.length+r.length);return t.set(e),t.set(r,e.length),t}function C(e,r){return(e[r]<<24|e[r+1]<<16|e[r+2]<<8|e[r+3])>>>0}function q(e,r){const t=new DataView(e.buffer,e.byteOffset,e.byteLength);return Number(t.getBigUint64(r,!1))}function me(e){const r=atob(e),t=new Uint8Array(r.length);for(let n=0;n<r.length;n+=1)t[n]=r.charCodeAt(n);return t}function Ee(e){const r=e.trim();if(r.length%2!==0)throw new Error("Invalid hex string.");const t=new Uint8Array(r.length/2);for(let n=0;n<r.length;n+=2)t[n/2]=Number.parseInt(r.slice(n,n+2),16);return t}function j(e){if(e)return typeof e=="string"?me(e):e}function Te(e){return e?typeof e=="string"?e:Array.from(e).join(","):""}function ge(e){const r=[];let t=0;for(;e.length-t>=36;){const n=t,o=e[n];if(o!==1)throw new Error(`Unsupported binary video packet version ${o}.`);const d=e[n+1]??0,l=q(e,n+4),u=q(e,n+12),R=C(e,n+20),c=C(e,n+24),i=C(e,n+28),h=C(e,n+32),T=36+i+h;if(e.length-n<T)break;t+=36;const U=(d&2)!==0&&i>0?e.subarray(t,t+i):void 0;t+=i;const X=e.subarray(t,t+h);t+=h,r.push({metadata:{description:U,frameSequence:l,height:c,isKeyFrame:(d&1)!==0,timestampUs:u,width:R},payload:X})}return{packets:r,remainder:e.subarray(t)}}function J(){return{averageRenderMs:0,codec:"",decodeQueueSize:0,decodedFrames:0,droppedFrames:0,frameSequence:0,height:0,latestFrameGapMs:0,latestRenderMs:0,maxRenderMs:0,receivedPackets:0,reconnects:0,renderedFrames:0,waitingForKeyFrame:!1,width:0}}const we=`#version 300 es
|
|
2
|
+
layout(location = 0) in vec2 a_position;
|
|
3
|
+
layout(location = 1) in vec2 a_texCoord;
|
|
4
|
+
|
|
5
|
+
out vec2 v_texCoord;
|
|
6
|
+
|
|
7
|
+
void main() {
|
|
8
|
+
v_texCoord = a_texCoord;
|
|
9
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
10
|
+
}
|
|
11
|
+
`,ye=`#version 300 es
|
|
12
|
+
precision mediump float;
|
|
13
|
+
|
|
14
|
+
in vec2 v_texCoord;
|
|
15
|
+
uniform sampler2D u_texture;
|
|
16
|
+
|
|
17
|
+
out vec4 outColor;
|
|
18
|
+
|
|
19
|
+
void main() {
|
|
20
|
+
outColor = texture(u_texture, v_texCoord);
|
|
21
|
+
}
|
|
22
|
+
`;function Z(e,r,t){const n=e.createShader(r);if(!n)throw new Error("Unable to allocate a WebGL shader.");if(e.shaderSource(n,t),e.compileShader(n),e.getShaderParameter(n,e.COMPILE_STATUS))return n;const o=e.getShaderInfoLog(n)??"Unknown shader compile failure.";throw e.deleteShader(n),new Error(o)}function Re(e){const r=Z(e,e.VERTEX_SHADER,we),t=Z(e,e.FRAGMENT_SHADER,ye),n=e.createProgram();if(!n)throw e.deleteShader(r),e.deleteShader(t),new Error("Unable to allocate a WebGL program.");if(e.attachShader(n,r),e.attachShader(n,t),e.linkProgram(n),e.deleteShader(r),e.deleteShader(t),e.getProgramParameter(n,e.LINK_STATUS))return n;const o=e.getProgramInfoLog(n)??"Unknown program link failure.";throw e.deleteProgram(n),new Error(o)}function Ae(e){return e.getContext("webgl2",{alpha:!1,antialias:!1,depth:!1,desynchronized:!0,powerPreference:"high-performance",premultipliedAlpha:!1,preserveDrawingBuffer:!1,stencil:!1})}class _e{canvas;gl;program;texture;vertexArray;textureHeight=0;textureWidth=0;constructor(r){this.canvas=r;const t=Ae(r);if(!t)throw new Error("This browser does not support WebGL2.");this.gl=t,this.program=Re(t);const n=t.createVertexArray();if(!n)throw new Error("Unable to allocate a WebGL vertex array.");this.vertexArray=n;const o=t.createBuffer();if(!o)throw new Error("Unable to allocate a WebGL vertex buffer.");const d=t.createTexture();if(!d)throw new Error("Unable to allocate a WebGL texture.");this.texture=d,t.bindVertexArray(n),t.bindBuffer(t.ARRAY_BUFFER,o),t.bufferData(t.ARRAY_BUFFER,new Float32Array([-1,-1,0,1,1,-1,1,1,-1,1,0,0,-1,1,0,0,1,-1,1,1,1,1,1,0]),t.STATIC_DRAW),t.enableVertexAttribArray(0),t.vertexAttribPointer(0,2,t.FLOAT,!1,16,0),t.enableVertexAttribArray(1),t.vertexAttribPointer(1,2,t.FLOAT,!1,16,8),t.useProgram(this.program);const l=t.getUniformLocation(this.program,"u_texture");l&&t.uniform1i(l,0),t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,this.texture),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.LINEAR),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.LINEAR),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE),t.pixelStorei(t.UNPACK_ALIGNMENT,1),t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,0),t.disable(t.BLEND),t.disable(t.DITHER),t.disable(t.DEPTH_TEST),t.disable(t.STENCIL_TEST),t.clearColor(0,0,0,1),this.syncViewport(Math.max(1,r.width),Math.max(1,r.height))}clear(){this.gl.clear(this.gl.COLOR_BUFFER_BIT)}drawFrame(r){this.syncViewport(r.displayWidth,r.displayHeight),this.gl.activeTexture(this.gl.TEXTURE0),this.gl.bindTexture(this.gl.TEXTURE_2D,this.texture),this.uploadFrame(r),this.gl.useProgram(this.program),this.gl.bindVertexArray(this.vertexArray),this.gl.drawArrays(this.gl.TRIANGLES,0,6)}uploadFrame(r){if(this.textureWidth!==r.displayWidth||this.textureHeight!==r.displayHeight){this.textureWidth=r.displayWidth,this.textureHeight=r.displayHeight,this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGBA,this.gl.RGBA,this.gl.UNSIGNED_BYTE,r);return}this.gl.texSubImage2D(this.gl.TEXTURE_2D,0,0,0,this.gl.RGBA,this.gl.UNSIGNED_BYTE,r)}syncViewport(r,t){const n=Math.max(1,Math.round(r)),o=Math.max(1,Math.round(t));this.canvas.width!==n&&(this.canvas.width=n),this.canvas.height!==o&&(this.canvas.height=o),this.gl.viewport(0,0,n,o)}}const g=self,ee=120,be=1e3,ve=8,Se=200,Ue=g.crypto?.randomUUID?.()??`worker-${Math.random().toString(36).slice(2)}`;let N=null,A=null,f=null,F=null,w=null,s=0,m=null,D="",b=!1,x=0,$="",H="",B=0,L=0,v=0,V=0,k=0,G=0,te="idle",O=0,a=J(),E=0,S=0,I=!1;function P(e){return e instanceof Error?e.name&&e.name!=="Error"?`${e.name}: ${e.message}`:e.message:String(e)}function Fe(e){return e.includes("WebTransport")||e.includes("Mach port invalid")||e.includes("device disconnected")||e.includes("opening handshake failed")||e.includes("stream closed")||e.includes("session closed")||e.includes("closed before sending")}function xe(e){return e.startsWith("hvc1.")||e.startsWith("hev1.")}function Ce(e){return e.startsWith("hvc1.")?`hev1.${e.slice(5)}`:e.startsWith("hev1.")?`hvc1.${e.slice(5)}`:null}function De(e){const r=[{config:e,label:`${e.codec} prefer-hardware`}],t=Ce(e.codec);return t&&r.push({config:{...e,codec:t},label:`${t} prefer-hardware`}),r.push({config:{codedHeight:e.codedHeight,codedWidth:e.codedWidth,codec:e.codec,description:e.description,optimizeForLatency:e.optimizeForLatency},label:`${e.codec} default-acceleration`}),t&&r.push({config:{codedHeight:e.codedHeight,codedWidth:e.codedWidth,codec:t,description:e.description,optimizeForLatency:e.optimizeForLatency},label:`${t} default-acceleration`}),r}function K(e){g.postMessage(e)}function p(e){te=e.state;const r=`${e.state}|${e.detail??""}|${e.error??""}`;r!==$&&($=r,K({type:"status",status:e}))}function Le(){return new URL("/api/client-stream-stats",g.location.href).toString()}function re(){L=0,v=0,V=0,k=0,G=0,O=0,I=!1}function _(){a.decodeQueueSize=f?.decodeQueueSize??0,a.waitingForKeyFrame=b}function ne(e=!1){const r=m;if(!r||I)return;const t=performance.now();if(!e&&v&&t-v<be)return;const n=v?t-v:0,o=a.receivedPackets-G,d=a.decodedFrames-V,l=a.droppedFrames-k,u=n>0?1e3/n:0;v=t,G=a.receivedPackets,V=a.decodedFrames,k=a.droppedFrames,_(),I=!0,fetch(Le(),{body:JSON.stringify({...a,clientId:Ue,connectionId:s,decodedFps:d*u,droppedFps:l*u,kind:"worker",packetFps:o*u,status:te,timestampMs:Date.now(),udid:r.udid,url:g.location.href,userAgent:g.navigator.userAgent}),cache:"no-store",headers:{"content-type":"application/json"},method:"POST"}).catch(()=>{}).finally(()=>{I=!1})}function oe(){E=0,x=performance.now(),_(),K({type:"stats",stats:{...a}}),ne()}function ae(){S&&(clearTimeout(S),S=0)}function y(e=!1){const r=performance.now();if(e||x===0||r-x>=ee){E&&(clearTimeout(E),E=0),oe();return}if(E)return;const t=Math.max(0,ee-(r-x));E=setTimeout(()=>{oe()},t)}function ie(e,r){const t=`${e}x${r}`;t!==H&&(H=t,K({type:"video-config",size:{width:e,height:r}}))}function se(){!N||A||(A=new _e(N))}function M(){A&&A.clear()}function W(){if(f)try{f.close()}catch{}f=null,D="",b=!1}function ce(){if(w){try{w.close({closeCode:0,reason:"disconnect"})}catch{}w=null}}function de(){$="",H="",x=0,B=0,ae(),E&&(clearTimeout(E),E=0)}function Ie(){ne(!0),s+=1,F?.abort(),F=null,m=null,ce(),W(),de(),re(),M(),p({state:"idle"})}function Y(e,r=s){const t=m;!t||S||r!==s||(p({detail:e,state:"connecting"}),S=setTimeout(()=>{S=0,!(!t||r!==s||t!==m)&&ue(t,!0)},150))}async function Pe(e,r){if(r!==s)return!1;if(typeof VideoDecoder!="function")return p({error:"This browser does not support WebCodecs.",state:"error"}),!1;const t=e.codec??a.codec,n=e.description,o=`${t}:${Te(n)}:${e.width}x${e.height}`;if(f&&D===o||f&&!n&&D.startsWith(`${t}:`)&&a.width===e.width&&a.height===e.height)return!0;if(!t||!n)return!1;W();const d=r;f=new VideoDecoder({output(c){if(d!==s){c.close();return}try{se()}catch(i){c.close(),p({error:i instanceof Error?i.message:"Unable to initialize the GPU renderer.",state:"error"});return}if(!A){c.close();return}try{const i=performance.now();A.drawFrame(c);const h=performance.now(),T=h-i;a.renderedFrames+=1,O+=T,a.latestRenderMs=T,a.maxRenderMs=Math.max(a.maxRenderMs,T),a.averageRenderMs=O/a.renderedFrames,L>0&&(a.latestFrameGapMs=h-L),L=h}catch(i){c.close(),p({error:i instanceof Error?i.message:"Unable to render the decoded frame.",state:"error"});return}c.close(),a.decodedFrames+=1,_(),y(),p({state:"streaming"})},error(c){d===s&&(p({error:c.message,state:"error"}),Y("Reconnecting live stream…",d))}});const l={codedHeight:e.height,codedWidth:e.width,codec:t,description:j(n),hardwareAcceleration:"prefer-hardware",optimizeForLatency:!0};let u=null;const R=[];for(const c of De(l))try{const i=await VideoDecoder.isConfigSupported(c.config);if(R.push(`${c.label}: ${i.supported?"yes":"no"}`),i.supported){u=i.config??c.config;break}}catch(i){R.push(`${c.label}: ${P(i)}`)}if(r!==s||!f)return!1;if(!u){const c=j(n),i=xe(t)?" HEVC decode is disabled or unavailable in this Chrome profile; check Chrome hardware acceleration and chrome://gpu video decode status.":"";return p({error:`Unsupported decoder configuration for ${t} at ${e.width}x${e.height} (${c?.byteLength??0} config bytes). Tried ${R.join("; ")}.${i}`,state:"error"}),W(),!1}return f.configure(u),D=o,a.codec=u.codec,!0}async function Me(e,r){if(r!==s)return;if(a.receivedPackets+=1,a.frameSequence=e.metadata.frameSequence,a.width=e.metadata.width,a.height=e.metadata.height,ie(e.metadata.width,e.metadata.height),e.metadata.isKeyFrame)b=!1;else if(b){a.droppedFrames+=1,_(),y();return}if(!await Pe(e.metadata,r)||!f){y();return}if(r===s){if(f.decodeQueueSize>ve&&!e.metadata.isKeyFrame){a.droppedFrames+=1,b=!0,_(),y(),m&&le(m.udid);return}try{f.decode(new EncodedVideoChunk({data:e.payload,timestamp:Number(e.metadata.timestampUs??0),type:e.metadata.isKeyFrame?"key":"delta"}))}catch{if(r!==s)return;a.droppedFrames+=1,b=!0,_(),y(),m&&le(m.udid);return}_(),y()}}function We(){return new URL("/api/health",g.location.href).toString()}function Ne(e){return new URL(`/api/simulators/${encodeURIComponent(e)}/refresh`,g.location.href).toString()}function $e(e,r){return e.replace("{udid}",encodeURIComponent(r))}async function He(e,r,t){if(!r||t||e!==s)return t;const n=await Promise.race([r.closed.then(()=>"WebTransport session closed.").catch(o=>P(o)),new Promise(o=>{setTimeout(()=>o(""),75)})]);return e===s?n:t}async function Be(e){const r=await fetch(We(),{cache:"no-store",signal:e});if(!r.ok)throw new Error(`Health check failed with status ${r.status}`);return await r.json()}async function Ve(e){const r=e.getReader();let t=new Uint8Array(0);for(;;){const{done:n,value:o}=await r.read();if(n)return new Uint8Array(t);o?.length&&(t=Q(t,o))}}async function ke(e){const r=await Ve(e);return JSON.parse(new TextDecoder().decode(r))}async function le(e){const r=performance.now();if(!(r-B<Se)){B=r;try{await fetch(Ne(e),{cache:"no-store",method:"POST"})}catch{}}}async function ue(e,r=!1){s+=1;const t=s;let n="";m=e,F?.abort(),F=new AbortController,ae(),ce(),W(),de(),M(),a=J(),re(),r&&(a.reconnects+=1),y(!0),p({detail:"Opening live stream…",state:"connecting"});try{if(typeof WebTransport!="function")throw new Error("This browser does not support WebTransport.");const o=await Be(F.signal),d=o.webTransport?.urlTemplate,l=o.webTransport?.certificateHash?.algorithm,u=o.webTransport?.certificateHash?.value,R=o.webTransport?.packetVersion;if(!d||!l||!u||R==null)throw new Error("Server did not provide WebTransport connection details.");if(l!=="sha-256")throw new Error(`Unsupported WebTransport certificate hash algorithm ${l}.`);w=new WebTransport($e(d,e.udid),{congestionControl:"low-latency",serverCertificateHashes:[{algorithm:l,value:new Uint8Array(Ee(u))}]}),w.closed.then(()=>{t===s&&(n||="WebTransport session closed.")}).catch(z=>{t===s&&(n=P(z))}),await w.ready;const c=w.incomingUnidirectionalStreams.getReader(),i=await c.read();if(i.done||!i.value)throw new Error("WebTransport closed before sending control stream.");const h=await ke(i.value);if(h.version!==R)throw new Error(`Unsupported WebTransport packet version ${h.version}.`);if(h.packet_format!=="binary-video-v1")throw new Error(`Unsupported WebTransport packet format ${h.packet_format}.`);a.codec=h.codec??"",ie(h.width,h.height),y(!0);const T=await c.read();if(T.done||!T.value)throw new Error("WebTransport closed before sending video stream.");let U=new Uint8Array(0);const X=T.value.getReader();for(;t===s;){const{done:z,value:he}=await X.read();if(z)break;if(!he?.length)continue;U=Q(U,he);const fe=ge(U);U=fe.remainder;for(const pe of fe.packets){if(t!==s)return;pe.metadata.codec??=h.codec,await Me(pe,t)}}t===s&&Y("Live stream ended. Reconnecting…",t)}catch(o){if(o.name==="AbortError")return;const d=P(o),l=await He(t,w,n),u=l&&l!==d?`${d} (${l})`:d;t===s&&(Fe(u)?p({detail:"Reconnecting live stream…",state:"connecting"}):p({error:u,state:"error"}),Y("Reconnecting live stream…",t))}}g.onmessage=e=>{const r=e.data;switch(r.type){case"attach-canvas":N=r.canvas,A=null;try{se(),M()}catch(t){p({error:t instanceof Error?t.message:"Unable to initialize the GPU renderer.",state:"error"})}break;case"connect":ue(r.target);break;case"disconnect":Ie();break;case"clear":M();break}}})();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0, user-scalable=no"
|
|
8
|
+
/>
|
|
9
|
+
<meta name="color-scheme" content="light dark" />
|
|
10
|
+
<title>SimDeck</title>
|
|
11
|
+
<script>
|
|
12
|
+
try {
|
|
13
|
+
localStorage.removeItem("xcw-debug-visible");
|
|
14
|
+
const key = "xcw-ui-state";
|
|
15
|
+
const state = JSON.parse(localStorage.getItem(key) || "{}");
|
|
16
|
+
if (state && Object.prototype.hasOwnProperty.call(state, "search")) {
|
|
17
|
+
delete state.search;
|
|
18
|
+
localStorage.setItem(key, JSON.stringify(state));
|
|
19
|
+
}
|
|
20
|
+
} catch {}
|
|
21
|
+
</script>
|
|
22
|
+
<script type="module" crossorigin src="/assets/index-BL9Mcd6u.js"></script>
|
|
23
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cu4TL413.css">
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<div id="root"></div>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "simdeck",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Native macOS simulator control plane with a browser client",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"bin": {
|
|
7
|
+
"simdeck": "bin/simdeck.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"README.md",
|
|
12
|
+
"bin/",
|
|
13
|
+
"scripts/experimental/",
|
|
14
|
+
"scripts/postinstall.mjs",
|
|
15
|
+
"build/simdeck-bin",
|
|
16
|
+
"client/dist/",
|
|
17
|
+
"packages/simdeck-test/dist/"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
"./test": {
|
|
21
|
+
"types": "./packages/simdeck-test/dist/index.d.ts",
|
|
22
|
+
"import": "./packages/simdeck-test/dist/index.js",
|
|
23
|
+
"default": "./packages/simdeck-test/dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"os": [
|
|
30
|
+
"darwin"
|
|
31
|
+
],
|
|
32
|
+
"cpu": [
|
|
33
|
+
"arm64"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build:cli": "scripts/build-cli.sh",
|
|
40
|
+
"build:client": "scripts/build-client.sh",
|
|
41
|
+
"build:app": "npm run build:cli && npm run build:client",
|
|
42
|
+
"build:inspectors": "npm run build:nativescript-inspector && npm run build:react-native-inspector",
|
|
43
|
+
"build:nativescript-inspector": "npm run --prefix packages/nativescript-inspector build",
|
|
44
|
+
"build:react-native-inspector": "npm run --prefix packages/react-native-inspector build",
|
|
45
|
+
"build:simdeck-test": "tsc -p packages/simdeck-test/tsconfig.json && prettier --write packages/simdeck-test/dist",
|
|
46
|
+
"build:packages": "npm run build:inspectors && npm run build:simdeck-test",
|
|
47
|
+
"build:docs": "npm run docs:build",
|
|
48
|
+
"build:vscode-extension": "npm run package:vscode-extension",
|
|
49
|
+
"build:all": "npm run build:app && npm run build:packages",
|
|
50
|
+
"build": "npm run build:app",
|
|
51
|
+
"package:vscode-extension": "node scripts/package-vscode-extension.mjs",
|
|
52
|
+
"package:vscode": "npm run package:vscode-extension",
|
|
53
|
+
"package:vsix": "npm run package:vscode-extension",
|
|
54
|
+
"package:npm": "npm pack",
|
|
55
|
+
"package:all": "npm run build:all && npm run package:vscode-extension && npm run package:npm",
|
|
56
|
+
"install:vscode-extension": "node scripts/install-vscode-extension.mjs",
|
|
57
|
+
"install:vscode": "npm run install:vscode-extension",
|
|
58
|
+
"format": "prettier --write . && cargo fmt --manifest-path server/Cargo.toml",
|
|
59
|
+
"format:check": "prettier --check . && cargo fmt --manifest-path server/Cargo.toml --check",
|
|
60
|
+
"lint": "npm run format:check && cargo clippy --manifest-path server/Cargo.toml --all-targets -- -D warnings && npm run --prefix client typecheck",
|
|
61
|
+
"test": "cargo test --manifest-path server/Cargo.toml && npm run --prefix client test",
|
|
62
|
+
"test:integration:cli": "node scripts/integration/cli.mjs",
|
|
63
|
+
"test:integration:cli:verbose": "SIMDECK_INTEGRATION_VERBOSE=1 SIMDECK_INTEGRATION_SHOW_SIMULATOR=1 node scripts/integration/cli.mjs",
|
|
64
|
+
"test:integration:js-api": "node scripts/integration/js-api.mjs",
|
|
65
|
+
"test:integration:js-api:verbose": "SIMDECK_INTEGRATION_SHOW_SIMULATOR=1 node scripts/integration/js-api.mjs",
|
|
66
|
+
"ci": "npm run lint && npm run build:all && npm run test && npm run package:vscode-extension",
|
|
67
|
+
"dev": "npm run build:cli && node scripts/dev.mjs",
|
|
68
|
+
"preview:swiftui": "node scripts/experimental/swiftui-preview.mjs",
|
|
69
|
+
"docs:dev": "vitepress dev docs",
|
|
70
|
+
"docs:build": "vitepress build docs",
|
|
71
|
+
"docs:preview": "vitepress preview docs",
|
|
72
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
73
|
+
"prepack": "npm run build:cli && npm run build:client && npm run build:simdeck-test"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@types/node": "^22.10.2",
|
|
77
|
+
"@vscode/vsce": "^3.6.2",
|
|
78
|
+
"prettier": "^3.6.2",
|
|
79
|
+
"typescript": "^5.9.2",
|
|
80
|
+
"vitepress": "^1.6.4"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type SimDeckLaunchOptions = {
|
|
2
|
+
cliPath?: string;
|
|
3
|
+
projectRoot?: string;
|
|
4
|
+
keepDaemon?: boolean;
|
|
5
|
+
isolated?: boolean;
|
|
6
|
+
port?: number;
|
|
7
|
+
videoCodec?: "hevc" | "h264" | "h264-software";
|
|
8
|
+
};
|
|
9
|
+
export type QueryOptions = {
|
|
10
|
+
source?: "auto" | "nativescript" | "uikit" | "native-ax";
|
|
11
|
+
maxDepth?: number;
|
|
12
|
+
includeHidden?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type ElementSelector = {
|
|
15
|
+
id?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
value?: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
};
|
|
20
|
+
export type TapOptions = QueryOptions & {
|
|
21
|
+
durationMs?: number;
|
|
22
|
+
waitTimeoutMs?: number;
|
|
23
|
+
pollMs?: number;
|
|
24
|
+
};
|
|
25
|
+
export type SimDeckSession = {
|
|
26
|
+
endpoint: string;
|
|
27
|
+
pid: number;
|
|
28
|
+
projectRoot: string;
|
|
29
|
+
list(): Promise<unknown>;
|
|
30
|
+
install(udid: string, appPath: string): Promise<void>;
|
|
31
|
+
uninstall(udid: string, bundleId: string): Promise<void>;
|
|
32
|
+
launch(udid: string, bundleId: string): Promise<void>;
|
|
33
|
+
openUrl(udid: string, url: string): Promise<void>;
|
|
34
|
+
tap(udid: string, x: number, y: number): Promise<void>;
|
|
35
|
+
tapElement(
|
|
36
|
+
udid: string,
|
|
37
|
+
selector: ElementSelector,
|
|
38
|
+
options?: TapOptions,
|
|
39
|
+
): Promise<void>;
|
|
40
|
+
touch(udid: string, x: number, y: number, phase: string): Promise<void>;
|
|
41
|
+
key(udid: string, keyCode: number, modifiers?: number): Promise<void>;
|
|
42
|
+
button(udid: string, button: string, durationMs?: number): Promise<void>;
|
|
43
|
+
pasteboardSet(udid: string, text: string): Promise<void>;
|
|
44
|
+
pasteboardGet(udid: string): Promise<string>;
|
|
45
|
+
chromeProfile(udid: string): Promise<unknown>;
|
|
46
|
+
tree(udid: string, options?: QueryOptions): Promise<unknown>;
|
|
47
|
+
query(
|
|
48
|
+
udid: string,
|
|
49
|
+
selector: ElementSelector,
|
|
50
|
+
options?: QueryOptions,
|
|
51
|
+
): Promise<unknown[]>;
|
|
52
|
+
assert(
|
|
53
|
+
udid: string,
|
|
54
|
+
selector: ElementSelector,
|
|
55
|
+
options?: QueryOptions,
|
|
56
|
+
): Promise<unknown>;
|
|
57
|
+
waitFor(
|
|
58
|
+
udid: string,
|
|
59
|
+
selector: ElementSelector,
|
|
60
|
+
options?: QueryOptions & {
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
pollMs?: number;
|
|
63
|
+
},
|
|
64
|
+
): Promise<unknown>;
|
|
65
|
+
batch(
|
|
66
|
+
udid: string,
|
|
67
|
+
steps: unknown[],
|
|
68
|
+
continueOnError?: boolean,
|
|
69
|
+
): Promise<unknown>;
|
|
70
|
+
screenshot(udid: string): Promise<Buffer>;
|
|
71
|
+
close(): void;
|
|
72
|
+
};
|
|
73
|
+
export declare function connect(
|
|
74
|
+
options?: SimDeckLaunchOptions,
|
|
75
|
+
): Promise<SimDeckSession>;
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
export async function connect(options = {}) {
|
|
9
|
+
const cliPath = options.cliPath ?? "simdeck";
|
|
10
|
+
const result = options.isolated
|
|
11
|
+
? await startIsolatedDaemon(cliPath, options)
|
|
12
|
+
: runJson(cliPath, ["daemon", "start"], {
|
|
13
|
+
cwd: options.projectRoot,
|
|
14
|
+
});
|
|
15
|
+
const endpoint = result.url;
|
|
16
|
+
const session = {
|
|
17
|
+
endpoint,
|
|
18
|
+
pid: result.pid,
|
|
19
|
+
projectRoot: result.projectRoot,
|
|
20
|
+
list: () => requestJson(endpoint, "GET", "/api/simulators"),
|
|
21
|
+
install: (udid, appPath) =>
|
|
22
|
+
requestOk(
|
|
23
|
+
endpoint,
|
|
24
|
+
`/api/simulators/${encodeURIComponent(udid)}/install`,
|
|
25
|
+
{
|
|
26
|
+
appPath,
|
|
27
|
+
},
|
|
28
|
+
),
|
|
29
|
+
uninstall: (udid, bundleId) =>
|
|
30
|
+
requestOk(
|
|
31
|
+
endpoint,
|
|
32
|
+
`/api/simulators/${encodeURIComponent(udid)}/uninstall`,
|
|
33
|
+
{
|
|
34
|
+
bundleId,
|
|
35
|
+
},
|
|
36
|
+
),
|
|
37
|
+
launch: (udid, bundleId) =>
|
|
38
|
+
requestOk(
|
|
39
|
+
endpoint,
|
|
40
|
+
`/api/simulators/${encodeURIComponent(udid)}/launch`,
|
|
41
|
+
{
|
|
42
|
+
bundleId,
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
openUrl: (udid, url) =>
|
|
46
|
+
requestOk(
|
|
47
|
+
endpoint,
|
|
48
|
+
`/api/simulators/${encodeURIComponent(udid)}/open-url`,
|
|
49
|
+
{
|
|
50
|
+
url,
|
|
51
|
+
},
|
|
52
|
+
),
|
|
53
|
+
tap: (udid, x, y) =>
|
|
54
|
+
requestOk(endpoint, `/api/simulators/${encodeURIComponent(udid)}/tap`, {
|
|
55
|
+
x,
|
|
56
|
+
y,
|
|
57
|
+
normalized: true,
|
|
58
|
+
}),
|
|
59
|
+
tapElement: (udid, selector, tapOptions) =>
|
|
60
|
+
requestOk(endpoint, `/api/simulators/${encodeURIComponent(udid)}/tap`, {
|
|
61
|
+
selector: selectorPayload(selector),
|
|
62
|
+
...tapOptions,
|
|
63
|
+
}),
|
|
64
|
+
touch: (udid, x, y, phase) =>
|
|
65
|
+
requestOk(endpoint, `/api/simulators/${encodeURIComponent(udid)}/touch`, {
|
|
66
|
+
x,
|
|
67
|
+
y,
|
|
68
|
+
phase,
|
|
69
|
+
}),
|
|
70
|
+
key: (udid, keyCode, modifiers = 0) =>
|
|
71
|
+
requestOk(endpoint, `/api/simulators/${encodeURIComponent(udid)}/key`, {
|
|
72
|
+
keyCode,
|
|
73
|
+
modifiers,
|
|
74
|
+
}),
|
|
75
|
+
button: (udid, button, durationMs = 0) =>
|
|
76
|
+
requestOk(
|
|
77
|
+
endpoint,
|
|
78
|
+
`/api/simulators/${encodeURIComponent(udid)}/button`,
|
|
79
|
+
{
|
|
80
|
+
button,
|
|
81
|
+
durationMs,
|
|
82
|
+
},
|
|
83
|
+
),
|
|
84
|
+
pasteboardSet: (udid, text) =>
|
|
85
|
+
requestOk(
|
|
86
|
+
endpoint,
|
|
87
|
+
`/api/simulators/${encodeURIComponent(udid)}/pasteboard`,
|
|
88
|
+
{
|
|
89
|
+
text,
|
|
90
|
+
},
|
|
91
|
+
),
|
|
92
|
+
pasteboardGet: async (udid) => {
|
|
93
|
+
const result = await requestJson(
|
|
94
|
+
endpoint,
|
|
95
|
+
"GET",
|
|
96
|
+
`/api/simulators/${encodeURIComponent(udid)}/pasteboard`,
|
|
97
|
+
);
|
|
98
|
+
return result.text ?? "";
|
|
99
|
+
},
|
|
100
|
+
chromeProfile: (udid) =>
|
|
101
|
+
requestJson(
|
|
102
|
+
endpoint,
|
|
103
|
+
"GET",
|
|
104
|
+
`/api/simulators/${encodeURIComponent(udid)}/chrome-profile`,
|
|
105
|
+
),
|
|
106
|
+
tree: (udid, treeOptions) =>
|
|
107
|
+
requestJson(
|
|
108
|
+
endpoint,
|
|
109
|
+
"GET",
|
|
110
|
+
`/api/simulators/${encodeURIComponent(udid)}/accessibility-tree?${treeQuery(treeOptions)}`,
|
|
111
|
+
),
|
|
112
|
+
query: async (udid, selector, treeOptions) => {
|
|
113
|
+
const result = await requestJson(
|
|
114
|
+
endpoint,
|
|
115
|
+
"POST",
|
|
116
|
+
`/api/simulators/${encodeURIComponent(udid)}/query`,
|
|
117
|
+
{
|
|
118
|
+
selector: selectorPayload(selector),
|
|
119
|
+
...treeOptions,
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
return result.matches;
|
|
123
|
+
},
|
|
124
|
+
assert: (udid, selector, assertOptions) =>
|
|
125
|
+
requestJson(
|
|
126
|
+
endpoint,
|
|
127
|
+
"POST",
|
|
128
|
+
`/api/simulators/${encodeURIComponent(udid)}/assert`,
|
|
129
|
+
{
|
|
130
|
+
selector: selectorPayload(selector),
|
|
131
|
+
...assertOptions,
|
|
132
|
+
},
|
|
133
|
+
),
|
|
134
|
+
waitFor: (udid, selector, waitOptions) =>
|
|
135
|
+
requestJson(
|
|
136
|
+
endpoint,
|
|
137
|
+
"POST",
|
|
138
|
+
`/api/simulators/${encodeURIComponent(udid)}/wait-for`,
|
|
139
|
+
{
|
|
140
|
+
selector: selectorPayload(selector),
|
|
141
|
+
...waitOptions,
|
|
142
|
+
},
|
|
143
|
+
),
|
|
144
|
+
batch: (udid, steps, continueOnError = false) =>
|
|
145
|
+
requestJson(
|
|
146
|
+
endpoint,
|
|
147
|
+
"POST",
|
|
148
|
+
`/api/simulators/${encodeURIComponent(udid)}/batch`,
|
|
149
|
+
{
|
|
150
|
+
steps,
|
|
151
|
+
continueOnError,
|
|
152
|
+
},
|
|
153
|
+
),
|
|
154
|
+
screenshot: (udid) =>
|
|
155
|
+
requestBuffer(
|
|
156
|
+
endpoint,
|
|
157
|
+
`/api/simulators/${encodeURIComponent(udid)}/screenshot.png`,
|
|
158
|
+
),
|
|
159
|
+
close: () => {
|
|
160
|
+
if (options.keepDaemon) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (result.child) {
|
|
164
|
+
result.child.kill();
|
|
165
|
+
if (result.isolatedRoot) {
|
|
166
|
+
fs.rmSync(result.isolatedRoot, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (result.started) {
|
|
171
|
+
spawnSync(cliPath, ["daemon", "stop"], { cwd: options.projectRoot });
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
return session;
|
|
176
|
+
}
|
|
177
|
+
async function startIsolatedDaemon(cliPath, options) {
|
|
178
|
+
const port = options.port ?? (await freePortPair());
|
|
179
|
+
const projectRoot = fs.mkdtempSync(
|
|
180
|
+
path.join(os.tmpdir(), "simdeck-test-project-"),
|
|
181
|
+
);
|
|
182
|
+
const metadataPath = path.join(
|
|
183
|
+
os.tmpdir(),
|
|
184
|
+
`simdeck-test-${process.pid}-${Date.now()}-${crypto.randomUUID()}.json`,
|
|
185
|
+
);
|
|
186
|
+
const accessToken = crypto.randomBytes(32).toString("hex");
|
|
187
|
+
const child = spawn(
|
|
188
|
+
cliPath,
|
|
189
|
+
[
|
|
190
|
+
"daemon",
|
|
191
|
+
"run",
|
|
192
|
+
"--project-root",
|
|
193
|
+
projectRoot,
|
|
194
|
+
"--metadata-path",
|
|
195
|
+
metadataPath,
|
|
196
|
+
"--port",
|
|
197
|
+
String(port),
|
|
198
|
+
"--bind",
|
|
199
|
+
"127.0.0.1",
|
|
200
|
+
"--access-token",
|
|
201
|
+
accessToken,
|
|
202
|
+
"--video-codec",
|
|
203
|
+
options.videoCodec ?? "h264-software",
|
|
204
|
+
],
|
|
205
|
+
{
|
|
206
|
+
cwd: options.projectRoot,
|
|
207
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
const output = captureChildOutput(child);
|
|
211
|
+
const url = `http://127.0.0.1:${port}`;
|
|
212
|
+
try {
|
|
213
|
+
await waitForHealth(url, child, output);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
child.kill();
|
|
216
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
projectRoot,
|
|
222
|
+
pid: child.pid ?? 0,
|
|
223
|
+
url,
|
|
224
|
+
started: true,
|
|
225
|
+
child,
|
|
226
|
+
isolatedRoot: projectRoot,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function waitForHealth(endpoint, child, output) {
|
|
230
|
+
const deadline = Date.now() + 60_000;
|
|
231
|
+
let lastError;
|
|
232
|
+
while (Date.now() < deadline) {
|
|
233
|
+
if (child.exitCode !== null) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`SimDeck isolated daemon exited with ${child.exitCode}.\n${output()}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
await requestJson(endpoint, "GET", "/api/health");
|
|
240
|
+
return;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
lastError = error;
|
|
243
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Timed out waiting for isolated SimDeck daemon: ${lastError instanceof Error ? lastError.message : String(lastError)}\n${output()}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
function captureChildOutput(child) {
|
|
251
|
+
const chunks = [];
|
|
252
|
+
const append = (source, chunk) => {
|
|
253
|
+
chunks.push(`[${source}] ${chunk.toString("utf8")}`);
|
|
254
|
+
while (chunks.join("").length > 16_384) {
|
|
255
|
+
chunks.shift();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
child.stdout?.on("data", (chunk) => append("stdout", chunk));
|
|
259
|
+
child.stderr?.on("data", (chunk) => append("stderr", chunk));
|
|
260
|
+
return () => chunks.join("").trim();
|
|
261
|
+
}
|
|
262
|
+
async function freePortPair() {
|
|
263
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
264
|
+
const port = await freePort();
|
|
265
|
+
if (port < 65535 && (await portAvailable(port + 1))) {
|
|
266
|
+
return port;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
throw new Error("Unable to allocate adjacent free TCP ports.");
|
|
270
|
+
}
|
|
271
|
+
function freePort() {
|
|
272
|
+
return new Promise((resolve, reject) => {
|
|
273
|
+
const server = net.createServer();
|
|
274
|
+
server.listen(0, "127.0.0.1", () => {
|
|
275
|
+
const address = server.address();
|
|
276
|
+
if (!address || typeof address === "string") {
|
|
277
|
+
server.close();
|
|
278
|
+
reject(new Error("Unable to allocate a free TCP port."));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const port = address.port;
|
|
282
|
+
server.close(() => resolve(port));
|
|
283
|
+
});
|
|
284
|
+
server.on("error", reject);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function portAvailable(port) {
|
|
288
|
+
return new Promise((resolve) => {
|
|
289
|
+
const server = net.createServer();
|
|
290
|
+
server.once("error", () => resolve(false));
|
|
291
|
+
server.listen(port, "127.0.0.1", () => {
|
|
292
|
+
server.close(() => resolve(true));
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function runJson(command, args, options = {}) {
|
|
297
|
+
const result = spawnSync(command, args, {
|
|
298
|
+
cwd: options.cwd,
|
|
299
|
+
encoding: "utf8",
|
|
300
|
+
maxBuffer: 1024 * 1024,
|
|
301
|
+
});
|
|
302
|
+
if (result.status !== 0) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
result.stderr.trim() || `${command} ${args.join(" ")} failed`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return JSON.parse(result.stdout);
|
|
308
|
+
}
|
|
309
|
+
function requestOk(endpoint, pathName, body) {
|
|
310
|
+
return requestJson(endpoint, "POST", pathName, body).then(() => undefined);
|
|
311
|
+
}
|
|
312
|
+
function requestJson(endpoint, method, pathName, body) {
|
|
313
|
+
return requestBuffer(endpoint, pathName, method, body).then((buffer) =>
|
|
314
|
+
JSON.parse(buffer.toString("utf8")),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
function requestBuffer(endpoint, pathName, method = "GET", body) {
|
|
318
|
+
const url = new URL(pathName, endpoint);
|
|
319
|
+
const payload =
|
|
320
|
+
body === undefined ? undefined : Buffer.from(JSON.stringify(body));
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
const request = http.request(
|
|
323
|
+
url,
|
|
324
|
+
{
|
|
325
|
+
method,
|
|
326
|
+
headers: payload
|
|
327
|
+
? {
|
|
328
|
+
"content-type": "application/json",
|
|
329
|
+
"content-length": String(payload.length),
|
|
330
|
+
origin: endpoint,
|
|
331
|
+
}
|
|
332
|
+
: { origin: endpoint },
|
|
333
|
+
},
|
|
334
|
+
(response) => {
|
|
335
|
+
const chunks = [];
|
|
336
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
337
|
+
response.on("end", () => {
|
|
338
|
+
const buffer = Buffer.concat(chunks);
|
|
339
|
+
if (
|
|
340
|
+
(response.statusCode ?? 500) < 200 ||
|
|
341
|
+
(response.statusCode ?? 500) >= 300
|
|
342
|
+
) {
|
|
343
|
+
reject(
|
|
344
|
+
new Error(
|
|
345
|
+
`${method} ${pathName} returned ${response.statusCode}: ${buffer.toString("utf8") || response.statusMessage || ""}`,
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
} else {
|
|
349
|
+
resolve(buffer);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
request.on("error", reject);
|
|
355
|
+
if (payload) {
|
|
356
|
+
request.write(payload);
|
|
357
|
+
}
|
|
358
|
+
request.end();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function treeQuery(options = {}) {
|
|
362
|
+
const params = new URLSearchParams();
|
|
363
|
+
if (options.source) params.set("source", options.source);
|
|
364
|
+
if (options.maxDepth !== undefined)
|
|
365
|
+
params.set("maxDepth", String(options.maxDepth));
|
|
366
|
+
if (options.includeHidden) params.set("includeHidden", "true");
|
|
367
|
+
return params.toString();
|
|
368
|
+
}
|
|
369
|
+
function selectorPayload(selector) {
|
|
370
|
+
return {
|
|
371
|
+
id: selector.id,
|
|
372
|
+
label: selector.label,
|
|
373
|
+
value: selector.value,
|
|
374
|
+
elementType: selector.type,
|
|
375
|
+
};
|
|
376
|
+
}
|