@realitycollective/uix-devtools 0.1.0-preview.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/bin/uix-dev.mjs +9 -0
- package/dist/cli/lib.d.ts +36 -0
- package/dist/cli/lib.js +88 -0
- package/dist/cli/lib.js.map +1 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +170 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/gate.d.ts +93 -0
- package/dist/gate.js +118 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime-compile.d.ts +24 -0
- package/dist/runtime-compile.js +42 -0
- package/dist/runtime-compile.js.map +1 -0
- package/package.json +66 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Change log for the Reality Collective WebXR UI Extensions packages. All four packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
|
|
6
|
+
|
|
7
|
+
## [0.1.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `@realitycollective/webxr-uiextensions` - engine-free core: window manager, dock state and regions, drag maths, hold-to-drag, control models (stepper/toggle/expandable/log), the `SceneDescriptor` scene format, window chrome conventions and the platform-adapter contract.
|
|
12
|
+
- `@realitycollective/iwsdk-uiextensions` - Meta IWSDK adapter binding the core onto IWSDK's ECS, UIKitML and interaction systems, with shipped examples.
|
|
13
|
+
- `@realitycollective/xrblocks-uiextensions` - EXPERIMENTAL Google XR Blocks / plain three.js adapter: panel document, window host, follow and scale maths, desktop controls and locomotion, pointer forwarding.
|
|
14
|
+
- `@realitycollective/uix-devtools` - dev-only tooling: edit-session launch gate, runtime UIKitML compilation, and the `uix-dev` CLI (Cloudflare quick tunnel, QR onboarding, environment doctor).
|
|
15
|
+
- Demo clients: the IWSDK showcase, the devtools playground, and the multiplatform lab that picks its pipeline from the hardware.
|
|
16
|
+
|
|
17
|
+
[0.1.0]: https://github.com/realitycollective/WebXR-UIExtensions/commits/main
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reality Collective
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @realitycollective/uix-devtools
|
|
2
|
+
|
|
3
|
+
Developer tooling for [`@realitycollective/iwsdk-uiextensions`](../iwsdk-uiextensions/README.md): an edit-session **launch gate**, **runtime UIKitML compilation** for live UX editing, and the **`uix-dev` CLI** that gets a local build onto a headset in one command via a Cloudflare quick tunnel.
|
|
4
|
+
|
|
5
|
+
> **This package is dev tooling. Never ship it in a player-facing bundle.** Install it as a `devDependency`, guard every use behind a build flag, and load overlays through dynamic import. The design makes the safe path the easy path - see [Safety model](#safety-model).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install --save-dev @realitycollective/uix-devtools
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## The edit gate
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { installEditGate } from '@realitycollective/uix-devtools';
|
|
17
|
+
|
|
18
|
+
// Guarded so production builds ELIMINATE this whole block:
|
|
19
|
+
if (import.meta.env.DEV || import.meta.env.VITE_UIX_EDIT) {
|
|
20
|
+
const expected = import.meta.env.VITE_UIX_EDIT_TOKEN as string | undefined;
|
|
21
|
+
installEditGate({
|
|
22
|
+
...(expected ? { tokens: [expected] } : {}), // no token minted → local-dev open gate
|
|
23
|
+
load: (session) =>
|
|
24
|
+
import('./editor-overlay.js').then((m) => m.install(session)),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- The gate opens only when the page URL carries `?uix-edit=<token>` and the token is accepted. Accepted tokens are remembered in `sessionStorage` (override with `remember: false`) so in-headset reloads keep the session.
|
|
30
|
+
- `load` is invoked exactly once, and a throwing/rejecting overlay is contained - it can never take the host app down.
|
|
31
|
+
- `clearEditSession()` forgets a remembered token (an "exit edit mode" affordance).
|
|
32
|
+
|
|
33
|
+
All parts are individually exported (`readEditToken`, `isTokenAccepted`, `resolveEditSession`) and unit-tested - see `test/gate.test.ts`.
|
|
34
|
+
|
|
35
|
+
## Runtime UIKitML compilation
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { compilePanelSource } from '@realitycollective/uix-devtools';
|
|
39
|
+
import { createUIWindow } from '@realitycollective/iwsdk-uiextensions';
|
|
40
|
+
|
|
41
|
+
const compiled = compilePanelSource(source); // same JSON the Vite plugin emits
|
|
42
|
+
if (compiled.errors.length === 0) {
|
|
43
|
+
createUIWindow(world, { id: 'live', title: 'Live', config: compiled.configUrl });
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`compilePanelSource` runs the same `parse` the build-time Vite plugin uses and serves the JSON through a `blob:` URL, which IWSDK's `PanelUI` fetches like any file - so panels can be authored, edited and respawned entirely at runtime (desktop workbench or in-headset editor). Parse problems land in `errors` instead of throwing; linked stylesheets can be resolved via the `resolveFile` option.
|
|
48
|
+
|
|
49
|
+
## The `uix-dev` CLI
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uix-dev tunnel [--port N] [--cwd DIR] [--no-dev-server]
|
|
53
|
+
uix-dev doctor
|
|
54
|
+
uix-dev qr <url>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`tunnel` mints a per-run edit token, starts the demo's dev server with `VITE_UIX_EDIT`/`VITE_UIX_EDIT_TOKEN` exported, opens a Cloudflare quick tunnel (`cloudflared tunnel --url`) and prints QR codes for both the plain and the edit-mode URL. HTTPS + WSS means WebXR and Vite HMR both work from the headset. `doctor` checks node/cloudflared/adb with install hints.
|
|
58
|
+
|
|
59
|
+
The demo's `vite.config.ts` must allow the tunnel host and, when driven by the CLI (`UIX_DEV_TUNNEL=1`), point HMR at wss:443 - see `demos/devtools-playground/vite.config.ts` for the reference snippet.
|
|
60
|
+
|
|
61
|
+
## Safety model
|
|
62
|
+
|
|
63
|
+
| Layer | Mechanism | Player-facing result |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| packaging | separate dev-only package, never imported by the runtime library | not installed → not present |
|
|
66
|
+
| build | gate call guarded by `import.meta.env` flags | branch eliminated from production bundles |
|
|
67
|
+
| runtime | exact-token match required; overlay is a lazy chunk | wrong/no token → nothing loads, nothing downloads |
|
|
68
|
+
|
|
69
|
+
## Demo
|
|
70
|
+
|
|
71
|
+
[`demos/devtools-playground`](../../demos/devtools-playground/README.md) - the shared showcase scene plus this package's gate and a live in-headset UIKitML editor window.
|
|
72
|
+
|
|
73
|
+
## Live demos
|
|
74
|
+
|
|
75
|
+
- Showcase: **[webxr-uiextensions.pages.dev](https://webxr-uiextensions.pages.dev)**
|
|
76
|
+
- Multiplatform lab: **[webxr-uix-lab.pages.dev](https://webxr-uix-lab.pages.dev)**
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT © Reality Collective
|
package/bin/uix-dev.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the assigned quick-tunnel URL from a chunk of cloudflared output.
|
|
3
|
+
* cloudflared prints it (to stderr) inside a box banner once the tunnel is up.
|
|
4
|
+
*/
|
|
5
|
+
export declare function extractTunnelUrl(outputChunk: string): string | undefined;
|
|
6
|
+
/** Mint a URL-safe random session token. */
|
|
7
|
+
export declare function mintToken(bytes?: number): string;
|
|
8
|
+
/** Append the edit-session token to a base URL. */
|
|
9
|
+
export declare function buildEditUrl(baseUrl: string, token: string, param?: string): string;
|
|
10
|
+
export interface TunnelPlan {
|
|
11
|
+
/** Local dev-server port the tunnel forwards to. */
|
|
12
|
+
port: number;
|
|
13
|
+
/** Per-run edit-session token, exported as VITE_UIX_EDIT_TOKEN. */
|
|
14
|
+
token: string;
|
|
15
|
+
/** argv for the cloudflared process. */
|
|
16
|
+
cloudflaredArgs: string[];
|
|
17
|
+
/** Environment additions for the dev-server process. */
|
|
18
|
+
devServerEnv: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
/** Compute everything a `uix-dev tunnel` run needs up front. */
|
|
21
|
+
export declare function planTunnel(options?: {
|
|
22
|
+
port?: number;
|
|
23
|
+
token?: string;
|
|
24
|
+
}): TunnelPlan;
|
|
25
|
+
export interface DoctorCheck {
|
|
26
|
+
name: string;
|
|
27
|
+
ok: boolean;
|
|
28
|
+
detail: string;
|
|
29
|
+
hint?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Interpret a `node --version` string against the workspace requirement. */
|
|
32
|
+
export declare function checkNodeVersion(versionString: string): DoctorCheck;
|
|
33
|
+
/** Build the doctor result for an external binary probe. */
|
|
34
|
+
export declare function checkBinary(name: string, found: boolean, version: string | undefined, installHint: string): DoctorCheck;
|
|
35
|
+
/** Render doctor checks as terminal lines. */
|
|
36
|
+
export declare function formatDoctorReport(checks: readonly DoctorCheck[]): string;
|
package/dist/cli/lib.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic behind the `uix-dev` CLI - everything here is unit-testable in
|
|
3
|
+
* node with no processes spawned. The orchestration (spawning vite and
|
|
4
|
+
* cloudflared, wiring stdio) lives in `main.ts` and stays intentionally thin.
|
|
5
|
+
*/
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
/** Matches the URL cloudflared assigns to a quick tunnel. */
|
|
8
|
+
const TUNNEL_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
9
|
+
/**
|
|
10
|
+
* Extract the assigned quick-tunnel URL from a chunk of cloudflared output.
|
|
11
|
+
* cloudflared prints it (to stderr) inside a box banner once the tunnel is up.
|
|
12
|
+
*/
|
|
13
|
+
export function extractTunnelUrl(outputChunk) {
|
|
14
|
+
const match = TUNNEL_URL_PATTERN.exec(outputChunk);
|
|
15
|
+
return match ? match[0] : undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Mint a URL-safe random session token. */
|
|
18
|
+
export function mintToken(bytes = 16) {
|
|
19
|
+
return randomBytes(bytes).toString('base64url');
|
|
20
|
+
}
|
|
21
|
+
/** Append the edit-session token to a base URL. */
|
|
22
|
+
export function buildEditUrl(baseUrl, token, param = 'uix-edit') {
|
|
23
|
+
const url = new URL(baseUrl);
|
|
24
|
+
url.searchParams.set(param, token);
|
|
25
|
+
return url.toString();
|
|
26
|
+
}
|
|
27
|
+
/** Compute everything a `uix-dev tunnel` run needs up front. */
|
|
28
|
+
export function planTunnel(options = {}) {
|
|
29
|
+
const port = options.port ?? 8081;
|
|
30
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
31
|
+
throw new Error(`Invalid port: ${String(options.port)}`);
|
|
32
|
+
}
|
|
33
|
+
const token = options.token ?? mintToken();
|
|
34
|
+
return {
|
|
35
|
+
port,
|
|
36
|
+
token,
|
|
37
|
+
cloudflaredArgs: ['tunnel', '--url', `http://localhost:${port}`],
|
|
38
|
+
devServerEnv: {
|
|
39
|
+
// Compile the edit gate in and pin the accepted token to this run.
|
|
40
|
+
VITE_UIX_EDIT: '1',
|
|
41
|
+
VITE_UIX_EDIT_TOKEN: token,
|
|
42
|
+
// Tells vite configs to emit HMR client settings that work through an
|
|
43
|
+
// HTTPS tunnel (wss on port 443) instead of the local port.
|
|
44
|
+
UIX_DEV_TUNNEL: '1',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Interpret a `node --version` string against the workspace requirement. */
|
|
49
|
+
export function checkNodeVersion(versionString) {
|
|
50
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(versionString.trim());
|
|
51
|
+
if (!match) {
|
|
52
|
+
return {
|
|
53
|
+
name: 'node',
|
|
54
|
+
ok: false,
|
|
55
|
+
detail: `Unrecognised version "${versionString.trim()}"`,
|
|
56
|
+
hint: 'Install Node 20.19+, 22.12+ or 24+.',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const major = Number(match[1]);
|
|
60
|
+
const minor = Number(match[2]);
|
|
61
|
+
const ok = (major === 20 && minor >= 19) ||
|
|
62
|
+
(major === 22 && minor >= 12) ||
|
|
63
|
+
major >= 24;
|
|
64
|
+
return {
|
|
65
|
+
name: 'node',
|
|
66
|
+
ok,
|
|
67
|
+
detail: `Node ${match[0].replace(/^v?/, 'v')}`,
|
|
68
|
+
...(ok ? {} : { hint: 'This workspace needs Node 20.19+, 22.12+ or 24+.' }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Build the doctor result for an external binary probe. */
|
|
72
|
+
export function checkBinary(name, found, version, installHint) {
|
|
73
|
+
if (!found) {
|
|
74
|
+
return { name, ok: false, detail: 'not found on PATH', hint: installHint };
|
|
75
|
+
}
|
|
76
|
+
return { name, ok: true, detail: version?.trim().split('\n')[0] ?? 'found' };
|
|
77
|
+
}
|
|
78
|
+
/** Render doctor checks as terminal lines. */
|
|
79
|
+
export function formatDoctorReport(checks) {
|
|
80
|
+
return checks
|
|
81
|
+
.map((check) => {
|
|
82
|
+
const status = check.ok ? ' ok ' : 'FAIL';
|
|
83
|
+
const hint = check.hint ? `\n ↳ ${check.hint}` : '';
|
|
84
|
+
return `[${status}] ${check.name}: ${check.detail}${hint}`;
|
|
85
|
+
})
|
|
86
|
+
.join('\n');
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=lib.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.js","sourceRoot":"","sources":["../../src/cli/lib.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure logic behind the `uix-dev` CLI - everything here is unit-testable in\n * node with no processes spawned. The orchestration (spawning vite and\n * cloudflared, wiring stdio) lives in `main.ts` and stays intentionally thin.\n */\nimport { randomBytes } from 'node:crypto';\n\n/** Matches the URL cloudflared assigns to a quick tunnel. */\nconst TUNNEL_URL_PATTERN = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/;\n\n/**\n * Extract the assigned quick-tunnel URL from a chunk of cloudflared output.\n * cloudflared prints it (to stderr) inside a box banner once the tunnel is up.\n */\nexport function extractTunnelUrl(outputChunk: string): string | undefined {\n const match = TUNNEL_URL_PATTERN.exec(outputChunk);\n return match ? match[0] : undefined;\n}\n\n/** Mint a URL-safe random session token. */\nexport function mintToken(bytes = 16): string {\n return randomBytes(bytes).toString('base64url');\n}\n\n/** Append the edit-session token to a base URL. */\nexport function buildEditUrl(\n baseUrl: string,\n token: string,\n param = 'uix-edit',\n): string {\n const url = new URL(baseUrl);\n url.searchParams.set(param, token);\n return url.toString();\n}\n\nexport interface TunnelPlan {\n /** Local dev-server port the tunnel forwards to. */\n port: number;\n /** Per-run edit-session token, exported as VITE_UIX_EDIT_TOKEN. */\n token: string;\n /** argv for the cloudflared process. */\n cloudflaredArgs: string[];\n /** Environment additions for the dev-server process. */\n devServerEnv: Record<string, string>;\n}\n\n/** Compute everything a `uix-dev tunnel` run needs up front. */\nexport function planTunnel(options: { port?: number; token?: string } = {}): TunnelPlan {\n const port = options.port ?? 8081;\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(`Invalid port: ${String(options.port)}`);\n }\n const token = options.token ?? mintToken();\n return {\n port,\n token,\n cloudflaredArgs: ['tunnel', '--url', `http://localhost:${port}`],\n devServerEnv: {\n // Compile the edit gate in and pin the accepted token to this run.\n VITE_UIX_EDIT: '1',\n VITE_UIX_EDIT_TOKEN: token,\n // Tells vite configs to emit HMR client settings that work through an\n // HTTPS tunnel (wss on port 443) instead of the local port.\n UIX_DEV_TUNNEL: '1',\n },\n };\n}\n\nexport interface DoctorCheck {\n name: string;\n ok: boolean;\n detail: string;\n hint?: string;\n}\n\n/** Interpret a `node --version` string against the workspace requirement. */\nexport function checkNodeVersion(versionString: string): DoctorCheck {\n const match = /^v?(\\d+)\\.(\\d+)\\.(\\d+)/.exec(versionString.trim());\n if (!match) {\n return {\n name: 'node',\n ok: false,\n detail: `Unrecognised version \"${versionString.trim()}\"`,\n hint: 'Install Node 20.19+, 22.12+ or 24+.',\n };\n }\n const major = Number(match[1]);\n const minor = Number(match[2]);\n const ok =\n (major === 20 && minor >= 19) ||\n (major === 22 && minor >= 12) ||\n major >= 24;\n return {\n name: 'node',\n ok,\n detail: `Node ${match[0].replace(/^v?/, 'v')}`,\n ...(ok ? {} : { hint: 'This workspace needs Node 20.19+, 22.12+ or 24+.' }),\n };\n}\n\n/** Build the doctor result for an external binary probe. */\nexport function checkBinary(\n name: string,\n found: boolean,\n version: string | undefined,\n installHint: string,\n): DoctorCheck {\n if (!found) {\n return { name, ok: false, detail: 'not found on PATH', hint: installHint };\n }\n return { name, ok: true, detail: version?.trim().split('\\n')[0] ?? 'found' };\n}\n\n/** Render doctor checks as terminal lines. */\nexport function formatDoctorReport(checks: readonly DoctorCheck[]): string {\n return checks\n .map((check) => {\n const status = check.ok ? ' ok ' : 'FAIL';\n const hint = check.hint ? `\\n ↳ ${check.hint}` : '';\n return `[${status}] ${check.name}: ${check.detail}${hint}`;\n })\n .join('\\n');\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv: string[]): Promise<number>;
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uix-dev` - get an IWSDK UI Extensions client onto a headset in one command.
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* uix-dev tunnel [--port N] [--cwd DIR] [--no-dev-server]
|
|
6
|
+
* Mint an edit-session token, start the Vite dev server (npm run dev)
|
|
7
|
+
* with the token exported, open a Cloudflare quick tunnel to it and
|
|
8
|
+
* print both the plain and the edit-mode URL as text + QR codes.
|
|
9
|
+
* uix-dev doctor
|
|
10
|
+
* Check node / cloudflared / adb availability with install hints.
|
|
11
|
+
* uix-dev qr <url>
|
|
12
|
+
* Print a QR code for any URL (e.g. a staging deploy).
|
|
13
|
+
*
|
|
14
|
+
* Orchestration only - all decision logic lives in lib.ts where it is unit
|
|
15
|
+
* tested.
|
|
16
|
+
*/
|
|
17
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
18
|
+
import process from 'node:process';
|
|
19
|
+
import qrcode from 'qrcode-terminal';
|
|
20
|
+
import { buildEditUrl, checkBinary, checkNodeVersion, extractTunnelUrl, formatDoctorReport, planTunnel, } from './lib.js';
|
|
21
|
+
function printQr(url) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
qrcode.generate(url, { small: true }, (code) => {
|
|
24
|
+
console.log(code);
|
|
25
|
+
resolve();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function probeBinary(command, args) {
|
|
30
|
+
const result = spawnSync(command, args, { encoding: 'utf8' });
|
|
31
|
+
if (result.error || result.status !== 0) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return (result.stdout || result.stderr || '').trim() || 'found';
|
|
35
|
+
}
|
|
36
|
+
async function runDoctor() {
|
|
37
|
+
const checks = [
|
|
38
|
+
checkNodeVersion(process.version),
|
|
39
|
+
checkBinary('cloudflared', probeBinary('cloudflared', ['--version']) !== undefined, probeBinary('cloudflared', ['--version']), 'Install: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/ (brew install cloudflared / winget install Cloudflare.cloudflared)'),
|
|
40
|
+
checkBinary('adb', probeBinary('adb', ['--version']) !== undefined, probeBinary('adb', ['--version']), 'Optional (USB loop): install Android platform-tools for `adb reverse`.'),
|
|
41
|
+
];
|
|
42
|
+
console.log(formatDoctorReport(checks));
|
|
43
|
+
return checks.every((check) => check.ok || check.name === 'adb') ? 0 : 1;
|
|
44
|
+
}
|
|
45
|
+
function parseTunnelFlags(argv) {
|
|
46
|
+
const flags = { port: 8081, cwd: process.cwd(), devServer: true };
|
|
47
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
48
|
+
const arg = argv[i];
|
|
49
|
+
if (arg === '--port') {
|
|
50
|
+
flags.port = Number(argv[i + 1]);
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
else if (arg === '--cwd') {
|
|
54
|
+
flags.cwd = argv[i + 1] ?? flags.cwd;
|
|
55
|
+
i += 1;
|
|
56
|
+
}
|
|
57
|
+
else if (arg === '--no-dev-server') {
|
|
58
|
+
flags.devServer = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return flags;
|
|
62
|
+
}
|
|
63
|
+
async function runTunnel(argv) {
|
|
64
|
+
const flags = parseTunnelFlags(argv);
|
|
65
|
+
const plan = planTunnel({ port: flags.port });
|
|
66
|
+
const children = [];
|
|
67
|
+
const shutdown = () => {
|
|
68
|
+
for (const child of children) {
|
|
69
|
+
child.kill('SIGTERM');
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
process.on('SIGINT', () => {
|
|
73
|
+
shutdown();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
});
|
|
76
|
+
process.on('SIGTERM', () => {
|
|
77
|
+
shutdown();
|
|
78
|
+
process.exit(0);
|
|
79
|
+
});
|
|
80
|
+
if (flags.devServer) {
|
|
81
|
+
console.log(`[uix-dev] starting dev server (npm run dev) in ${flags.cwd} ...`);
|
|
82
|
+
const dev = spawn('npm', ['run', 'dev'], {
|
|
83
|
+
cwd: flags.cwd,
|
|
84
|
+
env: { ...process.env, ...plan.devServerEnv },
|
|
85
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
86
|
+
});
|
|
87
|
+
children.push(dev);
|
|
88
|
+
dev.on('exit', (code) => {
|
|
89
|
+
console.error(`[uix-dev] dev server exited (${String(code)}); stopping tunnel.`);
|
|
90
|
+
shutdown();
|
|
91
|
+
process.exit(code ?? 1);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(`[uix-dev] --no-dev-server: expecting a server on port ${String(plan.port)}. ` +
|
|
96
|
+
`Export VITE_UIX_EDIT_TOKEN=${plan.token} yourself for the gate to match.`);
|
|
97
|
+
}
|
|
98
|
+
console.log('[uix-dev] opening Cloudflare quick tunnel (no account needed) ...');
|
|
99
|
+
const tunnel = spawn('cloudflared', plan.cloudflaredArgs, {
|
|
100
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
101
|
+
});
|
|
102
|
+
children.push(tunnel);
|
|
103
|
+
let announced = false;
|
|
104
|
+
const onChunk = async (chunk) => {
|
|
105
|
+
const text = chunk.toString('utf8');
|
|
106
|
+
process.stderr.write(text);
|
|
107
|
+
if (announced) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const url = extractTunnelUrl(text);
|
|
111
|
+
if (!url) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
announced = true;
|
|
115
|
+
const editUrl = buildEditUrl(url, plan.token);
|
|
116
|
+
console.log('\n──────────────────────────────────────────────────────');
|
|
117
|
+
console.log(' Quick tunnel is up. HTTPS + WSS, HMR flows through.');
|
|
118
|
+
console.log(`\n Runtime : ${url}`);
|
|
119
|
+
await printQr(url);
|
|
120
|
+
console.log(`\n EDIT MODE: ${editUrl}`);
|
|
121
|
+
await printQr(editUrl);
|
|
122
|
+
console.log(' Scan the second code on the headset to open an edit session.');
|
|
123
|
+
console.log(' URL and token are minted per run - Ctrl+C invalidates both.');
|
|
124
|
+
console.log('──────────────────────────────────────────────────────\n');
|
|
125
|
+
};
|
|
126
|
+
tunnel.stderr?.on('data', onChunk);
|
|
127
|
+
tunnel.stdout?.on('data', onChunk);
|
|
128
|
+
tunnel.on('error', () => {
|
|
129
|
+
console.error('[uix-dev] could not start cloudflared - is it installed? Run `uix-dev doctor`.');
|
|
130
|
+
shutdown();
|
|
131
|
+
process.exit(1);
|
|
132
|
+
});
|
|
133
|
+
tunnel.on('exit', (code) => {
|
|
134
|
+
console.error(`[uix-dev] tunnel exited (${String(code)}).`);
|
|
135
|
+
shutdown();
|
|
136
|
+
process.exit(code ?? 1);
|
|
137
|
+
});
|
|
138
|
+
return new Promise(() => {
|
|
139
|
+
// Runs until Ctrl+C or a child exits - handlers above end the process.
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export async function main(argv) {
|
|
143
|
+
const [command, ...rest] = argv;
|
|
144
|
+
switch (command) {
|
|
145
|
+
case 'tunnel':
|
|
146
|
+
return runTunnel(rest);
|
|
147
|
+
case 'doctor':
|
|
148
|
+
return runDoctor();
|
|
149
|
+
case 'qr': {
|
|
150
|
+
const url = rest[0];
|
|
151
|
+
if (!url) {
|
|
152
|
+
console.error('Usage: uix-dev qr <url>');
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
await printQr(url);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
console.log([
|
|
160
|
+
'uix-dev - IWSDK UI Extensions dev tooling',
|
|
161
|
+
'',
|
|
162
|
+
'Usage:',
|
|
163
|
+
' uix-dev tunnel [--port N] [--cwd DIR] [--no-dev-server]',
|
|
164
|
+
' uix-dev doctor',
|
|
165
|
+
' uix-dev qr <url>',
|
|
166
|
+
].join('\n'));
|
|
167
|
+
return command === undefined || command === 'help' ? 0 : 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `uix-dev` - get an IWSDK UI Extensions client onto a headset in one command.\n *\n * Commands:\n * uix-dev tunnel [--port N] [--cwd DIR] [--no-dev-server]\n * Mint an edit-session token, start the Vite dev server (npm run dev)\n * with the token exported, open a Cloudflare quick tunnel to it and\n * print both the plain and the edit-mode URL as text + QR codes.\n * uix-dev doctor\n * Check node / cloudflared / adb availability with install hints.\n * uix-dev qr <url>\n * Print a QR code for any URL (e.g. a staging deploy).\n *\n * Orchestration only - all decision logic lives in lib.ts where it is unit\n * tested.\n */\nimport { spawn, spawnSync, type ChildProcess } from 'node:child_process';\nimport process from 'node:process';\nimport qrcode from 'qrcode-terminal';\nimport {\n buildEditUrl,\n checkBinary,\n checkNodeVersion,\n extractTunnelUrl,\n formatDoctorReport,\n planTunnel,\n} from './lib.js';\n\nfunction printQr(url: string): Promise<void> {\n return new Promise((resolve) => {\n qrcode.generate(url, { small: true }, (code: string) => {\n console.log(code);\n resolve();\n });\n });\n}\n\nfunction probeBinary(command: string, args: string[]): string | undefined {\n const result = spawnSync(command, args, { encoding: 'utf8' });\n if (result.error || result.status !== 0) {\n return undefined;\n }\n return (result.stdout || result.stderr || '').trim() || 'found';\n}\n\nasync function runDoctor(): Promise<number> {\n const checks = [\n checkNodeVersion(process.version),\n checkBinary(\n 'cloudflared',\n probeBinary('cloudflared', ['--version']) !== undefined,\n probeBinary('cloudflared', ['--version']),\n 'Install: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/ (brew install cloudflared / winget install Cloudflare.cloudflared)',\n ),\n checkBinary(\n 'adb',\n probeBinary('adb', ['--version']) !== undefined,\n probeBinary('adb', ['--version']),\n 'Optional (USB loop): install Android platform-tools for `adb reverse`.',\n ),\n ];\n console.log(formatDoctorReport(checks));\n return checks.every((check) => check.ok || check.name === 'adb') ? 0 : 1;\n}\n\ninterface TunnelFlags {\n port: number;\n cwd: string;\n devServer: boolean;\n}\n\nfunction parseTunnelFlags(argv: string[]): TunnelFlags {\n const flags: TunnelFlags = { port: 8081, cwd: process.cwd(), devServer: true };\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n if (arg === '--port') {\n flags.port = Number(argv[i + 1]);\n i += 1;\n } else if (arg === '--cwd') {\n flags.cwd = argv[i + 1] ?? flags.cwd;\n i += 1;\n } else if (arg === '--no-dev-server') {\n flags.devServer = false;\n }\n }\n return flags;\n}\n\nasync function runTunnel(argv: string[]): Promise<number> {\n const flags = parseTunnelFlags(argv);\n const plan = planTunnel({ port: flags.port });\n const children: ChildProcess[] = [];\n\n const shutdown = () => {\n for (const child of children) {\n child.kill('SIGTERM');\n }\n };\n process.on('SIGINT', () => {\n shutdown();\n process.exit(0);\n });\n process.on('SIGTERM', () => {\n shutdown();\n process.exit(0);\n });\n\n if (flags.devServer) {\n console.log(`[uix-dev] starting dev server (npm run dev) in ${flags.cwd} ...`);\n const dev = spawn('npm', ['run', 'dev'], {\n cwd: flags.cwd,\n env: { ...process.env, ...plan.devServerEnv },\n stdio: ['ignore', 'inherit', 'inherit'],\n });\n children.push(dev);\n dev.on('exit', (code) => {\n console.error(`[uix-dev] dev server exited (${String(code)}); stopping tunnel.`);\n shutdown();\n process.exit(code ?? 1);\n });\n } else {\n console.log(\n `[uix-dev] --no-dev-server: expecting a server on port ${String(plan.port)}. ` +\n `Export VITE_UIX_EDIT_TOKEN=${plan.token} yourself for the gate to match.`,\n );\n }\n\n console.log('[uix-dev] opening Cloudflare quick tunnel (no account needed) ...');\n const tunnel = spawn('cloudflared', plan.cloudflaredArgs, {\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n children.push(tunnel);\n\n let announced = false;\n const onChunk = async (chunk: Buffer) => {\n const text = chunk.toString('utf8');\n process.stderr.write(text);\n if (announced) {\n return;\n }\n const url = extractTunnelUrl(text);\n if (!url) {\n return;\n }\n announced = true;\n const editUrl = buildEditUrl(url, plan.token);\n console.log('\\n──────────────────────────────────────────────────────');\n console.log(' Quick tunnel is up. HTTPS + WSS, HMR flows through.');\n console.log(`\\n Runtime : ${url}`);\n await printQr(url);\n console.log(`\\n EDIT MODE: ${editUrl}`);\n await printQr(editUrl);\n console.log(' Scan the second code on the headset to open an edit session.');\n console.log(' URL and token are minted per run - Ctrl+C invalidates both.');\n console.log('──────────────────────────────────────────────────────\\n');\n };\n tunnel.stderr?.on('data', onChunk);\n tunnel.stdout?.on('data', onChunk);\n tunnel.on('error', () => {\n console.error(\n '[uix-dev] could not start cloudflared - is it installed? Run `uix-dev doctor`.',\n );\n shutdown();\n process.exit(1);\n });\n tunnel.on('exit', (code) => {\n console.error(`[uix-dev] tunnel exited (${String(code)}).`);\n shutdown();\n process.exit(code ?? 1);\n });\n\n return new Promise<number>(() => {\n // Runs until Ctrl+C or a child exits - handlers above end the process.\n });\n}\n\nexport async function main(argv: string[]): Promise<number> {\n const [command, ...rest] = argv;\n switch (command) {\n case 'tunnel':\n return runTunnel(rest);\n case 'doctor':\n return runDoctor();\n case 'qr': {\n const url = rest[0];\n if (!url) {\n console.error('Usage: uix-dev qr <url>');\n return 1;\n }\n await printQr(url);\n return 0;\n }\n default:\n console.log(\n [\n 'uix-dev - IWSDK UI Extensions dev tooling',\n '',\n 'Usage:',\n ' uix-dev tunnel [--port N] [--cwd DIR] [--no-dev-server]',\n ' uix-dev doctor',\n ' uix-dev qr <url>',\n ].join('\\n'),\n );\n return command === undefined || command === 'help' ? 0 : 1;\n }\n}\n"]}
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit-session launch gate.
|
|
3
|
+
*
|
|
4
|
+
* The gate is how a client opts into dev/edit mode WITHOUT the tooling ever
|
|
5
|
+
* being reachable by players:
|
|
6
|
+
*
|
|
7
|
+
* 1. Production builds compile the whole gate call out behind a build flag
|
|
8
|
+
* (`if (import.meta.env.VITE_UIX_EDIT) installEditGate(...)`) - the
|
|
9
|
+
* bundler removes the branch, so the code does not exist in player builds.
|
|
10
|
+
* 2. Where the gate IS compiled in (dev / staging), it only opens when the
|
|
11
|
+
* page URL carries an explicit session token (`?uix-edit=<token>`) that
|
|
12
|
+
* matches one the developer minted (`uix-dev tunnel` prints the URL).
|
|
13
|
+
* 3. The dev overlay itself is loaded through the `load` callback - always a
|
|
14
|
+
* `dynamic import()` - so it lives in a separate chunk that is never even
|
|
15
|
+
* downloaded for normal visitors.
|
|
16
|
+
*
|
|
17
|
+
* Everything here is DOM-optional and injectable so it is unit-testable in
|
|
18
|
+
* node: pass `href`/`storage` explicitly, or let it default to `window`.
|
|
19
|
+
*/
|
|
20
|
+
/** Default URL query parameter carrying the edit-session token. */
|
|
21
|
+
export declare const DEFAULT_EDIT_PARAM = "uix-edit";
|
|
22
|
+
/** sessionStorage key used to keep the session alive across reloads. */
|
|
23
|
+
export declare const EDIT_SESSION_STORAGE_KEY = "uix-devtools:edit-token";
|
|
24
|
+
/** An open edit session. */
|
|
25
|
+
export interface EditSession {
|
|
26
|
+
/** The token the page presented. */
|
|
27
|
+
token: string;
|
|
28
|
+
/** How the token arrived: fresh from the URL, or remembered from storage. */
|
|
29
|
+
source: 'url' | 'storage';
|
|
30
|
+
}
|
|
31
|
+
/** Minimal storage surface (sessionStorage-compatible), injectable for tests. */
|
|
32
|
+
export interface TokenStorage {
|
|
33
|
+
getItem(key: string): string | null;
|
|
34
|
+
setItem(key: string, value: string): void;
|
|
35
|
+
removeItem(key: string): void;
|
|
36
|
+
}
|
|
37
|
+
export interface EditGateOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Loader called exactly once when the gate opens. Do the dynamic import of
|
|
40
|
+
* your dev overlay here so it stays in its own chunk:
|
|
41
|
+
* `load: (session) => import('./dev-overlay.js').then(m => m.install(session))`
|
|
42
|
+
*/
|
|
43
|
+
load: (session: EditSession) => void | Promise<unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* Tokens that are allowed to open the gate. Typically
|
|
46
|
+
* `[import.meta.env.VITE_UIX_EDIT_TOKEN]` - minted per run by `uix-dev`.
|
|
47
|
+
* When omitted or empty, ANY non-empty token opens the gate; only do that
|
|
48
|
+
* in local dev builds that never leave your machine.
|
|
49
|
+
*/
|
|
50
|
+
tokens?: readonly string[];
|
|
51
|
+
/** URL query parameter name. Default: {@link DEFAULT_EDIT_PARAM}. */
|
|
52
|
+
param?: string;
|
|
53
|
+
/** Page URL to inspect. Default: `window.location.href`. */
|
|
54
|
+
href?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Remember an accepted token in sessionStorage so reloads (e.g. re-entering
|
|
57
|
+
* VR) keep the session without the query param. Default: true.
|
|
58
|
+
*/
|
|
59
|
+
remember?: boolean;
|
|
60
|
+
/** Storage override for tests. Default: `window.sessionStorage`. */
|
|
61
|
+
storage?: TokenStorage;
|
|
62
|
+
}
|
|
63
|
+
export interface EditGateResult {
|
|
64
|
+
/** True when an accepted token opened the gate and `load` was invoked. */
|
|
65
|
+
active: boolean;
|
|
66
|
+
session?: EditSession;
|
|
67
|
+
/** Present when a token arrived but was rejected. */
|
|
68
|
+
rejected?: string;
|
|
69
|
+
}
|
|
70
|
+
/** Extract the raw edit token from a URL, if any. Pure. */
|
|
71
|
+
export declare function readEditToken(href: string, param?: string): string | undefined;
|
|
72
|
+
/** Token acceptance rule. Pure. */
|
|
73
|
+
export declare function isTokenAccepted(token: string, allowed: readonly string[] | undefined): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the edit session for a page: URL token first, remembered token as
|
|
76
|
+
* fallback. Does NOT invoke any loader - pure resolution, used by
|
|
77
|
+
* {@link installEditGate} and directly testable.
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveEditSession(options: {
|
|
80
|
+
tokens?: readonly string[];
|
|
81
|
+
param?: string;
|
|
82
|
+
href?: string;
|
|
83
|
+
remember?: boolean;
|
|
84
|
+
storage?: TokenStorage;
|
|
85
|
+
}): EditGateResult;
|
|
86
|
+
/** Forget a remembered session (e.g. an "exit edit mode" button). */
|
|
87
|
+
export declare function clearEditSession(storage?: TokenStorage): void;
|
|
88
|
+
/**
|
|
89
|
+
* Install the gate: resolve the session and, when it opens, invoke `load`
|
|
90
|
+
* exactly once. Never throws into the host app - a broken overlay must not
|
|
91
|
+
* take the runtime down.
|
|
92
|
+
*/
|
|
93
|
+
export declare function installEditGate(options: EditGateOptions): EditGateResult;
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit-session launch gate.
|
|
3
|
+
*
|
|
4
|
+
* The gate is how a client opts into dev/edit mode WITHOUT the tooling ever
|
|
5
|
+
* being reachable by players:
|
|
6
|
+
*
|
|
7
|
+
* 1. Production builds compile the whole gate call out behind a build flag
|
|
8
|
+
* (`if (import.meta.env.VITE_UIX_EDIT) installEditGate(...)`) - the
|
|
9
|
+
* bundler removes the branch, so the code does not exist in player builds.
|
|
10
|
+
* 2. Where the gate IS compiled in (dev / staging), it only opens when the
|
|
11
|
+
* page URL carries an explicit session token (`?uix-edit=<token>`) that
|
|
12
|
+
* matches one the developer minted (`uix-dev tunnel` prints the URL).
|
|
13
|
+
* 3. The dev overlay itself is loaded through the `load` callback - always a
|
|
14
|
+
* `dynamic import()` - so it lives in a separate chunk that is never even
|
|
15
|
+
* downloaded for normal visitors.
|
|
16
|
+
*
|
|
17
|
+
* Everything here is DOM-optional and injectable so it is unit-testable in
|
|
18
|
+
* node: pass `href`/`storage` explicitly, or let it default to `window`.
|
|
19
|
+
*/
|
|
20
|
+
/** Default URL query parameter carrying the edit-session token. */
|
|
21
|
+
export const DEFAULT_EDIT_PARAM = 'uix-edit';
|
|
22
|
+
/** sessionStorage key used to keep the session alive across reloads. */
|
|
23
|
+
export const EDIT_SESSION_STORAGE_KEY = 'uix-devtools:edit-token';
|
|
24
|
+
/** Extract the raw edit token from a URL, if any. Pure. */
|
|
25
|
+
export function readEditToken(href, param = DEFAULT_EDIT_PARAM) {
|
|
26
|
+
let url;
|
|
27
|
+
try {
|
|
28
|
+
url = new URL(href);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const token = url.searchParams.get(param);
|
|
34
|
+
return token && token.length > 0 ? token : undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Token acceptance rule. Pure. */
|
|
37
|
+
export function isTokenAccepted(token, allowed) {
|
|
38
|
+
if (token.length === 0) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (!allowed || allowed.length === 0) {
|
|
42
|
+
return true; // open gate - local dev only, see EditGateOptions.tokens
|
|
43
|
+
}
|
|
44
|
+
return allowed.some((candidate) => candidate.length > 0 && candidate === token);
|
|
45
|
+
}
|
|
46
|
+
/* v8 ignore start -- environment shims, exercised only in a real browser */
|
|
47
|
+
function defaultHref() {
|
|
48
|
+
return typeof window !== 'undefined' ? window.location.href : undefined;
|
|
49
|
+
}
|
|
50
|
+
function defaultStorage() {
|
|
51
|
+
try {
|
|
52
|
+
return typeof window !== 'undefined' ? window.sessionStorage : undefined;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined; // storage can throw in privacy modes - gate still works
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/* v8 ignore stop */
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the edit session for a page: URL token first, remembered token as
|
|
61
|
+
* fallback. Does NOT invoke any loader - pure resolution, used by
|
|
62
|
+
* {@link installEditGate} and directly testable.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveEditSession(options) {
|
|
65
|
+
const param = options.param ?? DEFAULT_EDIT_PARAM;
|
|
66
|
+
const remember = options.remember ?? true;
|
|
67
|
+
const storage = options.storage ?? defaultStorage();
|
|
68
|
+
const href = options.href ?? defaultHref();
|
|
69
|
+
const urlToken = href !== undefined ? readEditToken(href, param) : undefined;
|
|
70
|
+
if (urlToken !== undefined) {
|
|
71
|
+
if (!isTokenAccepted(urlToken, options.tokens)) {
|
|
72
|
+
return { active: false, rejected: urlToken };
|
|
73
|
+
}
|
|
74
|
+
if (remember && storage) {
|
|
75
|
+
storage.setItem(EDIT_SESSION_STORAGE_KEY, urlToken);
|
|
76
|
+
}
|
|
77
|
+
return { active: true, session: { token: urlToken, source: 'url' } };
|
|
78
|
+
}
|
|
79
|
+
if (remember && storage) {
|
|
80
|
+
const remembered = storage.getItem(EDIT_SESSION_STORAGE_KEY);
|
|
81
|
+
if (remembered !== null && remembered.length > 0) {
|
|
82
|
+
if (!isTokenAccepted(remembered, options.tokens)) {
|
|
83
|
+
storage.removeItem(EDIT_SESSION_STORAGE_KEY);
|
|
84
|
+
return { active: false, rejected: remembered };
|
|
85
|
+
}
|
|
86
|
+
return { active: true, session: { token: remembered, source: 'storage' } };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { active: false };
|
|
90
|
+
}
|
|
91
|
+
/** Forget a remembered session (e.g. an "exit edit mode" button). */
|
|
92
|
+
export function clearEditSession(storage) {
|
|
93
|
+
const target = storage ?? defaultStorage();
|
|
94
|
+
target?.removeItem(EDIT_SESSION_STORAGE_KEY);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Install the gate: resolve the session and, when it opens, invoke `load`
|
|
98
|
+
* exactly once. Never throws into the host app - a broken overlay must not
|
|
99
|
+
* take the runtime down.
|
|
100
|
+
*/
|
|
101
|
+
export function installEditGate(options) {
|
|
102
|
+
const result = resolveEditSession(options);
|
|
103
|
+
if (result.active && result.session) {
|
|
104
|
+
try {
|
|
105
|
+
const outcome = options.load(result.session);
|
|
106
|
+
if (outcome && typeof outcome.catch === 'function') {
|
|
107
|
+
outcome.catch((error) => {
|
|
108
|
+
console.error('[uix-devtools] edit overlay failed to load:', error);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.error('[uix-devtools] edit overlay failed to load:', error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=gate.js.map
|
package/dist/gate.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Edit-session launch gate.\n *\n * The gate is how a client opts into dev/edit mode WITHOUT the tooling ever\n * being reachable by players:\n *\n * 1. Production builds compile the whole gate call out behind a build flag\n * (`if (import.meta.env.VITE_UIX_EDIT) installEditGate(...)`) - the\n * bundler removes the branch, so the code does not exist in player builds.\n * 2. Where the gate IS compiled in (dev / staging), it only opens when the\n * page URL carries an explicit session token (`?uix-edit=<token>`) that\n * matches one the developer minted (`uix-dev tunnel` prints the URL).\n * 3. The dev overlay itself is loaded through the `load` callback - always a\n * `dynamic import()` - so it lives in a separate chunk that is never even\n * downloaded for normal visitors.\n *\n * Everything here is DOM-optional and injectable so it is unit-testable in\n * node: pass `href`/`storage` explicitly, or let it default to `window`.\n */\n\n/** Default URL query parameter carrying the edit-session token. */\nexport const DEFAULT_EDIT_PARAM = 'uix-edit';\n\n/** sessionStorage key used to keep the session alive across reloads. */\nexport const EDIT_SESSION_STORAGE_KEY = 'uix-devtools:edit-token';\n\n/** An open edit session. */\nexport interface EditSession {\n /** The token the page presented. */\n token: string;\n /** How the token arrived: fresh from the URL, or remembered from storage. */\n source: 'url' | 'storage';\n}\n\n/** Minimal storage surface (sessionStorage-compatible), injectable for tests. */\nexport interface TokenStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface EditGateOptions {\n /**\n * Loader called exactly once when the gate opens. Do the dynamic import of\n * your dev overlay here so it stays in its own chunk:\n * `load: (session) => import('./dev-overlay.js').then(m => m.install(session))`\n */\n load: (session: EditSession) => void | Promise<unknown>;\n /**\n * Tokens that are allowed to open the gate. Typically\n * `[import.meta.env.VITE_UIX_EDIT_TOKEN]` - minted per run by `uix-dev`.\n * When omitted or empty, ANY non-empty token opens the gate; only do that\n * in local dev builds that never leave your machine.\n */\n tokens?: readonly string[];\n /** URL query parameter name. Default: {@link DEFAULT_EDIT_PARAM}. */\n param?: string;\n /** Page URL to inspect. Default: `window.location.href`. */\n href?: string;\n /**\n * Remember an accepted token in sessionStorage so reloads (e.g. re-entering\n * VR) keep the session without the query param. Default: true.\n */\n remember?: boolean;\n /** Storage override for tests. Default: `window.sessionStorage`. */\n storage?: TokenStorage;\n}\n\nexport interface EditGateResult {\n /** True when an accepted token opened the gate and `load` was invoked. */\n active: boolean;\n session?: EditSession;\n /** Present when a token arrived but was rejected. */\n rejected?: string;\n}\n\n/** Extract the raw edit token from a URL, if any. Pure. */\nexport function readEditToken(\n href: string,\n param: string = DEFAULT_EDIT_PARAM,\n): string | undefined {\n let url: URL;\n try {\n url = new URL(href);\n } catch {\n return undefined;\n }\n const token = url.searchParams.get(param);\n return token && token.length > 0 ? token : undefined;\n}\n\n/** Token acceptance rule. Pure. */\nexport function isTokenAccepted(\n token: string,\n allowed: readonly string[] | undefined,\n): boolean {\n if (token.length === 0) {\n return false;\n }\n if (!allowed || allowed.length === 0) {\n return true; // open gate - local dev only, see EditGateOptions.tokens\n }\n return allowed.some((candidate) => candidate.length > 0 && candidate === token);\n}\n\n/* v8 ignore start -- environment shims, exercised only in a real browser */\nfunction defaultHref(): string | undefined {\n return typeof window !== 'undefined' ? window.location.href : undefined;\n}\n\nfunction defaultStorage(): TokenStorage | undefined {\n try {\n return typeof window !== 'undefined' ? window.sessionStorage : undefined;\n } catch {\n return undefined; // storage can throw in privacy modes - gate still works\n }\n}\n/* v8 ignore stop */\n\n/**\n * Resolve the edit session for a page: URL token first, remembered token as\n * fallback. Does NOT invoke any loader - pure resolution, used by\n * {@link installEditGate} and directly testable.\n */\nexport function resolveEditSession(options: {\n tokens?: readonly string[];\n param?: string;\n href?: string;\n remember?: boolean;\n storage?: TokenStorage;\n}): EditGateResult {\n const param = options.param ?? DEFAULT_EDIT_PARAM;\n const remember = options.remember ?? true;\n const storage = options.storage ?? defaultStorage();\n const href = options.href ?? defaultHref();\n\n const urlToken = href !== undefined ? readEditToken(href, param) : undefined;\n if (urlToken !== undefined) {\n if (!isTokenAccepted(urlToken, options.tokens)) {\n return { active: false, rejected: urlToken };\n }\n if (remember && storage) {\n storage.setItem(EDIT_SESSION_STORAGE_KEY, urlToken);\n }\n return { active: true, session: { token: urlToken, source: 'url' } };\n }\n\n if (remember && storage) {\n const remembered = storage.getItem(EDIT_SESSION_STORAGE_KEY);\n if (remembered !== null && remembered.length > 0) {\n if (!isTokenAccepted(remembered, options.tokens)) {\n storage.removeItem(EDIT_SESSION_STORAGE_KEY);\n return { active: false, rejected: remembered };\n }\n return { active: true, session: { token: remembered, source: 'storage' } };\n }\n }\n\n return { active: false };\n}\n\n/** Forget a remembered session (e.g. an \"exit edit mode\" button). */\nexport function clearEditSession(storage?: TokenStorage): void {\n const target = storage ?? defaultStorage();\n target?.removeItem(EDIT_SESSION_STORAGE_KEY);\n}\n\n/**\n * Install the gate: resolve the session and, when it opens, invoke `load`\n * exactly once. Never throws into the host app - a broken overlay must not\n * take the runtime down.\n */\nexport function installEditGate(options: EditGateOptions): EditGateResult {\n const result = resolveEditSession(options);\n if (result.active && result.session) {\n try {\n const outcome = options.load(result.session);\n if (outcome && typeof (outcome as Promise<unknown>).catch === 'function') {\n (outcome as Promise<unknown>).catch((error) => {\n console.error('[uix-devtools] edit overlay failed to load:', error);\n });\n }\n } catch (error) {\n console.error('[uix-devtools] edit overlay failed to load:', error);\n }\n }\n return result;\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @realitycollective/uix-devtools - client-side entry.
|
|
3
|
+
*
|
|
4
|
+
* Only the pieces safe to reference from app code are exported here: the
|
|
5
|
+
* edit-session gate (designed to be compiled out of production builds) and
|
|
6
|
+
* the runtime UIKitML compiler that powers live panel editing.
|
|
7
|
+
*
|
|
8
|
+
* The `uix-dev` CLI ships from the same package (`bin/uix-dev.mjs`,
|
|
9
|
+
* `@realitycollective/uix-devtools/cli`) but is node-only - never import it
|
|
10
|
+
* from client code.
|
|
11
|
+
*/
|
|
12
|
+
export { DEFAULT_EDIT_PARAM, EDIT_SESSION_STORAGE_KEY, clearEditSession, installEditGate, isTokenAccepted, readEditToken, resolveEditSession, type EditGateOptions, type EditGateResult, type EditSession, type TokenStorage, } from './gate.js';
|
|
13
|
+
export { compilePanelSource, type CompiledPanel } from './runtime-compile.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @realitycollective/uix-devtools - client-side entry.
|
|
3
|
+
*
|
|
4
|
+
* Only the pieces safe to reference from app code are exported here: the
|
|
5
|
+
* edit-session gate (designed to be compiled out of production builds) and
|
|
6
|
+
* the runtime UIKitML compiler that powers live panel editing.
|
|
7
|
+
*
|
|
8
|
+
* The `uix-dev` CLI ships from the same package (`bin/uix-dev.mjs`,
|
|
9
|
+
* `@realitycollective/uix-devtools/cli`) but is node-only - never import it
|
|
10
|
+
* from client code.
|
|
11
|
+
*/
|
|
12
|
+
export { DEFAULT_EDIT_PARAM, EDIT_SESSION_STORAGE_KEY, clearEditSession, installEditGate, isTokenAccepted, readEditToken, resolveEditSession, } from './gate.js';
|
|
13
|
+
export { compilePanelSource } from './runtime-compile.js';
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,aAAa,EACb,kBAAkB,GAKnB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAsB,MAAM,sBAAsB,CAAC","sourcesContent":["/**\n * @realitycollective/uix-devtools - client-side entry.\n *\n * Only the pieces safe to reference from app code are exported here: the\n * edit-session gate (designed to be compiled out of production builds) and\n * the runtime UIKitML compiler that powers live panel editing.\n *\n * The `uix-dev` CLI ships from the same package (`bin/uix-dev.mjs`,\n * `@realitycollective/uix-devtools/cli`) but is node-only - never import it\n * from client code.\n */\nexport {\n DEFAULT_EDIT_PARAM,\n EDIT_SESSION_STORAGE_KEY,\n clearEditSession,\n installEditGate,\n isTokenAccepted,\n readEditToken,\n resolveEditSession,\n type EditGateOptions,\n type EditGateResult,\n type EditSession,\n type TokenStorage,\n} from './gate.js';\nexport { compilePanelSource, type CompiledPanel } from './runtime-compile.js';\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface CompiledPanel {
|
|
2
|
+
/** Compiled panel JSON, same shape the Vite plugin writes to public/ui. */
|
|
3
|
+
json: string;
|
|
4
|
+
/** Parse diagnostics. Empty when the source compiled cleanly. */
|
|
5
|
+
errors: string[];
|
|
6
|
+
/**
|
|
7
|
+
* `blob:` URL serving {@link json}; hand this to `PanelUI.config`.
|
|
8
|
+
* Empty string in non-browser environments (node tests).
|
|
9
|
+
*/
|
|
10
|
+
configUrl: string;
|
|
11
|
+
/** Release the blob URL once the panel has loaded (or on discard). */
|
|
12
|
+
revoke(): void;
|
|
13
|
+
}
|
|
14
|
+
export interface CompileOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Resolver for `<link ref="...">` stylesheet references in the source -
|
|
17
|
+
* return the CSS text for a path, throw for unknown paths (reported as a
|
|
18
|
+
* diagnostic). Omit to leave linked stylesheets unresolved (inline
|
|
19
|
+
* `<style>` always works).
|
|
20
|
+
*/
|
|
21
|
+
resolveFile?: (filePath: string) => string;
|
|
22
|
+
}
|
|
23
|
+
/** Compile UIKitML source text. Collects errors instead of throwing. */
|
|
24
|
+
export declare function compilePanelSource(source: string, options?: CompileOptions): CompiledPanel;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime UIKitML compilation.
|
|
3
|
+
*
|
|
4
|
+
* The `@iwsdk/vite-plugin-uikitml` build step is literally
|
|
5
|
+
* `JSON.stringify(parse(source))` - so the same `parse` from
|
|
6
|
+
* `@pmndrs/uikitml` can compile panel markup live in the browser. IWSDK's
|
|
7
|
+
* `PanelUI` loads its `config` with a plain `fetch()`, which accepts `blob:`
|
|
8
|
+
* URLs, so a runtime-compiled panel is:
|
|
9
|
+
*
|
|
10
|
+
* const compiled = compilePanelSource(source);
|
|
11
|
+
* createUIWindow(world, { config: compiled.configUrl, ... });
|
|
12
|
+
*
|
|
13
|
+
* This is the mechanism behind live UX editing (desktop workbench and
|
|
14
|
+
* in-headset editor alike): edit markup → recompile → respawn the window.
|
|
15
|
+
*/
|
|
16
|
+
import { parse } from '@pmndrs/uikitml';
|
|
17
|
+
/** Compile UIKitML source text. Collects errors instead of throwing. */
|
|
18
|
+
export function compilePanelSource(source, options = {}) {
|
|
19
|
+
const errors = [];
|
|
20
|
+
const result = parse(source, {
|
|
21
|
+
onError: (message) => {
|
|
22
|
+
errors.push(message);
|
|
23
|
+
},
|
|
24
|
+
...(options.resolveFile ? { resolveFile: options.resolveFile } : {}),
|
|
25
|
+
});
|
|
26
|
+
if (result.element === undefined) {
|
|
27
|
+
errors.push('UIKitML source produced no root element.');
|
|
28
|
+
}
|
|
29
|
+
const json = JSON.stringify(result, null, 2);
|
|
30
|
+
const blobSupported = typeof URL !== 'undefined' &&
|
|
31
|
+
typeof URL.createObjectURL === 'function' &&
|
|
32
|
+
typeof Blob !== 'undefined';
|
|
33
|
+
/* v8 ignore start -- only reachable in environments without Blob support */
|
|
34
|
+
if (!blobSupported) {
|
|
35
|
+
return { json, errors, configUrl: '', revoke: () => { } };
|
|
36
|
+
}
|
|
37
|
+
/* v8 ignore stop */
|
|
38
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
39
|
+
const url = URL.createObjectURL(blob);
|
|
40
|
+
return { json, errors, configUrl: url, revoke: () => URL.revokeObjectURL(url) };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=runtime-compile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-compile.js","sourceRoot":"","sources":["../src/runtime-compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AA0BxC,wEAAwE;AACxE,MAAM,UAAU,kBAAkB,CAChC,MAAc,EACd,UAA0B,EAAE;IAE5B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE;QAC3B,OAAO,EAAE,CAAC,OAAe,EAAE,EAAE;YAC3B,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACvB,CAAC;QACD,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACrE,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAE7C,MAAM,aAAa,GACjB,OAAO,GAAG,KAAK,WAAW;QAC1B,OAAO,GAAG,CAAC,eAAe,KAAK,UAAU;QACzC,OAAO,IAAI,KAAK,WAAW,CAAC;IAC9B,4EAA4E;IAC5E,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;IAC3D,CAAC;IACD,oBAAoB;IAEpB,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;AAClF,CAAC","sourcesContent":["/**\n * Runtime UIKitML compilation.\n *\n * The `@iwsdk/vite-plugin-uikitml` build step is literally\n * `JSON.stringify(parse(source))` - so the same `parse` from\n * `@pmndrs/uikitml` can compile panel markup live in the browser. IWSDK's\n * `PanelUI` loads its `config` with a plain `fetch()`, which accepts `blob:`\n * URLs, so a runtime-compiled panel is:\n *\n * const compiled = compilePanelSource(source);\n * createUIWindow(world, { config: compiled.configUrl, ... });\n *\n * This is the mechanism behind live UX editing (desktop workbench and\n * in-headset editor alike): edit markup → recompile → respawn the window.\n */\nimport { parse } from '@pmndrs/uikitml';\n\nexport interface CompiledPanel {\n /** Compiled panel JSON, same shape the Vite plugin writes to public/ui. */\n json: string;\n /** Parse diagnostics. Empty when the source compiled cleanly. */\n errors: string[];\n /**\n * `blob:` URL serving {@link json}; hand this to `PanelUI.config`.\n * Empty string in non-browser environments (node tests).\n */\n configUrl: string;\n /** Release the blob URL once the panel has loaded (or on discard). */\n revoke(): void;\n}\n\nexport interface CompileOptions {\n /**\n * Resolver for `<link ref=\"...\">` stylesheet references in the source -\n * return the CSS text for a path, throw for unknown paths (reported as a\n * diagnostic). Omit to leave linked stylesheets unresolved (inline\n * `<style>` always works).\n */\n resolveFile?: (filePath: string) => string;\n}\n\n/** Compile UIKitML source text. Collects errors instead of throwing. */\nexport function compilePanelSource(\n source: string,\n options: CompileOptions = {},\n): CompiledPanel {\n const errors: string[] = [];\n const result = parse(source, {\n onError: (message: string) => {\n errors.push(message);\n },\n ...(options.resolveFile ? { resolveFile: options.resolveFile } : {}),\n });\n\n if (result.element === undefined) {\n errors.push('UIKitML source produced no root element.');\n }\n\n const json = JSON.stringify(result, null, 2);\n\n const blobSupported =\n typeof URL !== 'undefined' &&\n typeof URL.createObjectURL === 'function' &&\n typeof Blob !== 'undefined';\n /* v8 ignore start -- only reachable in environments without Blob support */\n if (!blobSupported) {\n return { json, errors, configUrl: '', revoke: () => {} };\n }\n /* v8 ignore stop */\n\n const blob = new Blob([json], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n return { json, errors, configUrl: url, revoke: () => URL.revokeObjectURL(url) };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@realitycollective/uix-devtools",
|
|
3
|
+
"version": "0.1.0-preview.0",
|
|
4
|
+
"description": "Developer tooling for @realitycollective/iwsdk-uiextensions - edit-session launch gate, runtime UIKitML compilation, and the uix-dev CLI (Cloudflare quick tunnel + QR onboarding for headset testing). Never ship this in a player-facing bundle.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"realitycollective",
|
|
7
|
+
"iwsdk",
|
|
8
|
+
"webxr",
|
|
9
|
+
"devtools",
|
|
10
|
+
"uikitml",
|
|
11
|
+
"cloudflared",
|
|
12
|
+
"quest",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Reality Collective",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./cli": {
|
|
26
|
+
"types": "./dist/cli/lib.d.ts",
|
|
27
|
+
"default": "./dist/cli/lib.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"uix-dev": "./bin/uix-dev.mjs"
|
|
32
|
+
},
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"bin",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@pmndrs/uikitml": "^0.1.12",
|
|
45
|
+
"qrcode-terminal": "^0.12.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"typescript": "^5.5.0"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/realitycollective/WebXR-UIExtensions.git",
|
|
54
|
+
"directory": "packages/uix-devtools"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/realitycollective/WebXR-UIExtensions#readme",
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/realitycollective/WebXR-UIExtensions/issues"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=20.19.0 <21.0.0-0 || >=22.12.0 <23.0.0-0 || >=24.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|