@immediately-run/sdk 0.18.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/components/MountImage.cjs +44 -0
  2. package/dist/components/MountImage.cjs.map +1 -0
  3. package/dist/components/MountImage.d.cts +42 -0
  4. package/dist/components/MountImage.d.ts +42 -0
  5. package/dist/components/MountImage.js +20 -0
  6. package/dist/components/MountImage.js.map +1 -0
  7. package/dist/contextUtils.cjs +2 -1
  8. package/dist/contextUtils.cjs.map +1 -1
  9. package/dist/contextUtils.js +2 -1
  10. package/dist/contextUtils.js.map +1 -1
  11. package/dist/fs.cjs +35 -5
  12. package/dist/fs.cjs.map +1 -1
  13. package/dist/fs.d.cts +26 -1
  14. package/dist/fs.d.ts +26 -1
  15. package/dist/fs.js +34 -5
  16. package/dist/fs.js.map +1 -1
  17. package/dist/hooks.cjs +38 -2
  18. package/dist/hooks.cjs.map +1 -1
  19. package/dist/hooks.d.cts +33 -1
  20. package/dist/hooks.d.ts +33 -1
  21. package/dist/hooks.js +37 -2
  22. package/dist/hooks.js.map +1 -1
  23. package/dist/index.cjs +2 -0
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +4 -3
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +1 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/injectedBundler.cjs +9 -0
  30. package/dist/injectedBundler.cjs.map +1 -1
  31. package/dist/injectedBundler.d.cts +11 -1
  32. package/dist/injectedBundler.d.ts +11 -1
  33. package/dist/injectedBundler.js +8 -0
  34. package/dist/injectedBundler.js.map +1 -1
  35. package/dist/mounts.cjs +27 -1
  36. package/dist/mounts.cjs.map +1 -1
  37. package/dist/mounts.d.cts +42 -3
  38. package/dist/mounts.d.ts +42 -3
  39. package/dist/mounts.js +21 -1
  40. package/dist/mounts.js.map +1 -1
  41. package/dist/version.cjs +1 -1
  42. package/dist/version.cjs.map +1 -1
  43. package/dist/version.d.cts +1 -1
  44. package/dist/version.d.ts +1 -1
  45. package/dist/version.js +1 -1
  46. package/dist/version.js.map +1 -1
  47. package/package.json +1 -1
package/dist/fs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean =>\n typeof fs?.promises?.readFile === 'function' || typeof fs?.readFile === 'function';\n\n/**\n * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the\n * resolution order previously duplicated in every app's `mountFs.ts`:\n *\n * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.\n * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`\n * whose surface has `readFile` (the bundler ZenFS-layer bound context).\n * 3. else `null` (local `vite dev` / before boot).\n *\n * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute\n * `/mnt/{hash}` paths (the file explorer / editor).\n */\nexport function sandboxFs(): SandboxFsPort | null {\n try {\n const shared = (globalThis as any).__sandpackSharedFs;\n if (hasFs(shared)) return shared as SandboxFsPort;\n } catch {\n /* not in the sandbox */\n }\n try {\n // @ts-ignore - `module` is injected by the sandbox runtime (see sandboxUtils transport).\n const layers = module?.evaluation?.module?.bundler?.fs?.layers;\n if (Array.isArray(layers)) {\n for (const layer of layers) {\n const fs = layer?.boundContext?.fs;\n if (hasFs(fs)) return fs as SandboxFsPort;\n }\n }\n } catch {\n /* not in the sandbox */\n }\n return null;\n}\n\n/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before\n * boot — gate file affordances on it so an app degrades instead of throwing. */\nexport function fsAvailable(): boolean {\n return sandboxFs() != null;\n}\n\n/** A directory entry from {@link MountFs.readdir}. */\nexport interface DirEntry {\n name: string;\n kind: 'file' | 'dir';\n}\n\n/** A stat result from {@link MountFs.stat}. */\nexport interface FileStat {\n kind: 'file' | 'dir';\n size: number;\n mtimeMs?: number;\n}\n\n/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`\n * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */\nexport interface FsError extends Error {\n code:\n | 'not-found' // ENOENT\n | 'read-only' // EROFS — a `ro` mount / downgraded role; NEVER surface as UX (gate with canWrite)\n | 'not-permitted' // EACCES\n | 'exists' // EEXIST\n | 'not-empty' // ENOTEMPTY\n | 'invalid-path' // a `..` segment / absolute escape was passed as a relPath\n | 'unavailable' // no sandbox fs (local dev / pre-boot)\n | 'unknown';\n}\n\nconst ERRNO: Record<string, FsError['code']> = {\n ENOENT: 'not-found',\n EROFS: 'read-only',\n EACCES: 'not-permitted',\n EPERM: 'not-permitted',\n EEXIST: 'exists',\n ENOTEMPTY: 'not-empty',\n};\n\nconst fsError = (code: FsError['code'], message: string): FsError => {\n const err = new Error(message) as FsError;\n err.code = code;\n return err;\n};\n\nconst mapError = (e: unknown): FsError => {\n const errno = (e as { code?: string } | null)?.code;\n const code: FsError['code'] = (errno ? ERRNO[errno] : undefined) ?? 'unknown';\n const err = new Error((e as Error)?.message ?? 'fs operation failed') as FsError;\n err.code = code;\n return err;\n};\n\nconst decoder = new TextDecoder();\nconst encoder = new TextEncoder();\n\n// Join a mount-RELATIVE path under the mount root, rejecting `..` escapes and absolute\n// paths (CLAUDE.md security rule 3 — don't probe for escapes). The host chroot is the\n// real enforcer; this keeps an honest app from accidentally naming outside its grant.\nconst resolveUnder = (root: string, relPath: string): string => {\n if (relPath.startsWith('/')) {\n throw fsError('invalid-path', `expected a mount-relative path, got absolute \"${relPath}\"`);\n }\n const parts: string[] = [];\n for (const seg of relPath.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n throw fsError('invalid-path', `\"${relPath}\" escapes the mount root`);\n }\n parts.push(seg);\n }\n const base = root.endsWith('/') ? root.slice(0, -1) : root;\n return parts.length ? `${base}/${parts.join('/')}` : base;\n};\n\n// The longest matching `rules` subtree governs a path (mounts.ts MountRule); fall back to\n// the whole-mount `mode`. A CLIENT-SIDE hint mirroring the host rule — EROFS stays\n// authoritative (the host re-checks live policy on every write).\nconst writableAt = (mount: SandboxMount, relPath: string): boolean => {\n const path = '/' + relPath.split('/').filter((s) => s && s !== '.').join('/');\n const rules: MountRule[] | undefined = mount.rules;\n if (rules && rules.length) {\n let best: MountRule | undefined;\n for (const r of rules) {\n const sub = r.subtree.endsWith('/') ? r.subtree : r.subtree + '/';\n if (path === r.subtree || path.startsWith(sub) || r.subtree === '/') {\n if (!best || r.subtree.length > best.subtree.length) best = r;\n }\n }\n if (best) return best.mode === 'rw';\n }\n return (mount.mode ?? 'rw') === 'rw';\n};\n\n/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;\n * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).\n * Obtain one with {@link openFs}. */\nexport interface MountFs {\n /** The mount this view is anchored to (read `mode`/`rules` for writability). */\n readonly mount: SandboxMount;\n /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */\n readFile(relPath: string, encoding: 'utf8'): Promise<string>;\n readFile(relPath: string): Promise<Uint8Array>;\n /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */\n writeFile(relPath: string, data: string | Uint8Array): Promise<void>;\n /** List a directory (the mount root when `relPath` is omitted). */\n readdir(relPath?: string): Promise<DirEntry[]>;\n /** Stat a path. Throws `not-found` if absent. */\n stat(relPath: string): Promise<FileStat>;\n /** Does `relPath` exist? Never throws on absence. */\n exists(relPath: string): Promise<boolean>;\n /** Create a directory (pass `{ recursive: true }` to make parents). */\n mkdir(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Remove a file, or a directory with `{ recursive: true }`. */\n rm(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Rename/move within the mount. */\n rename(fromRel: string, toRel: string): Promise<void>;\n /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),\n * so an app can hide an \"edit\" affordance instead of catching `read-only`\n * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade\n * flips it. EROFS from the host stays authoritative. */\n canWrite(relPath?: string): boolean;\n}\n\nconst promisesOf = (port: SandboxFsPort): NodeFsPromises =>\n (port.promises ?? (port as unknown as NodeFsPromises));\n\n/**\n * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:\n * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you\n * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path\n * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the\n * real enforcer).\n *\n * ```ts\n * import { mountSpace } from '@immediately-run/sdk';\n * import { openFs } from '@immediately-run/sdk/fs';\n * const fs = openFs(await mountSpace({ spaceId }));\n * const text = await fs.readFile('notes/idea.mdx', 'utf8');\n * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);\n * ```\n *\n * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`\n * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}\n * with a mapped `.code` (`not-found`, `read-only`, …).\n */\nexport function openFs(mount: SandboxMount): MountFs {\n const root = mount.path;\n\n const port = (): NodeFsPromises => {\n const p = sandboxFs();\n if (!p) throw fsError('unavailable', 'immediately.run: sandbox filesystem unavailable');\n return promisesOf(p);\n };\n\n const api: MountFs = {\n mount,\n async readFile(relPath: string, encoding?: 'utf8'): Promise<any> {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const data = await p.readFile(abs);\n const bytes =\n typeof data === 'string' ? encoder.encode(data) : (data as Uint8Array);\n return encoding === 'utf8' ? decoder.decode(bytes) : bytes;\n } catch (e) {\n throw mapError(e);\n }\n },\n async writeFile(relPath, data) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.writeFile(abs, typeof data === 'string' ? encoder.encode(data) : data);\n } catch (e) {\n throw mapError(e);\n }\n },\n async readdir(relPath = '') {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const entries = await p.readdir(abs, { withFileTypes: true });\n return entries.map((d: any) =>\n typeof d === 'string'\n ? ({ name: d, kind: 'file' } as DirEntry)\n : ({ name: d.name, kind: d.isDirectory?.() ? 'dir' : 'file' } as DirEntry),\n );\n } catch (e) {\n throw mapError(e);\n }\n },\n async stat(relPath) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const s: any = await p.stat(abs);\n return {\n kind: s.isDirectory?.() ? 'dir' : 'file',\n size: typeof s.size === 'number' ? s.size : 0,\n mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,\n };\n } catch (e) {\n throw mapError(e);\n }\n },\n async exists(relPath) {\n try {\n await api.stat(relPath);\n return true;\n } catch (e) {\n if ((e as FsError).code === 'not-found') return false;\n if ((e as FsError).code === 'unavailable' || (e as FsError).code === 'invalid-path') {\n throw e;\n }\n return false;\n }\n },\n async mkdir(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.mkdir(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rm(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.rm(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rename(fromRel, toRel) {\n const p = port();\n const from = resolveUnder(root, fromRel);\n const to = resolveUnder(root, toRel);\n try {\n await p.rename(from, to);\n } catch (e) {\n throw mapError(e);\n }\n },\n canWrite(relPath = '') {\n return writableAt(mount, relPath);\n },\n };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean =>\n typeof fs?.promises?.readFile === 'function' || typeof fs?.readFile === 'function';\n\n/**\n * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the\n * resolution order previously duplicated in every app's `mountFs.ts`:\n *\n * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.\n * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`\n * whose surface has `readFile` (the bundler ZenFS-layer bound context).\n * 3. else `null` (local `vite dev` / before boot).\n *\n * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute\n * `/mnt/{hash}` paths (the file explorer / editor).\n */\nexport function sandboxFs(): SandboxFsPort | null {\n try {\n const shared = (globalThis as any).__sandpackSharedFs;\n if (hasFs(shared)) return shared as SandboxFsPort;\n } catch {\n /* not in the sandbox */\n }\n try {\n // @ts-ignore - `module` is injected by the sandbox runtime (see sandboxUtils transport).\n const layers = module?.evaluation?.module?.bundler?.fs?.layers;\n if (Array.isArray(layers)) {\n for (const layer of layers) {\n const fs = layer?.boundContext?.fs;\n if (hasFs(fs)) return fs as SandboxFsPort;\n }\n }\n } catch {\n /* not in the sandbox */\n }\n return null;\n}\n\n/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before\n * boot — gate file affordances on it so an app degrades instead of throwing. */\nexport function fsAvailable(): boolean {\n return sandboxFs() != null;\n}\n\n/** A directory entry from {@link MountFs.readdir}. */\nexport interface DirEntry {\n name: string;\n kind: 'file' | 'dir';\n}\n\n/** A stat result from {@link MountFs.stat}. */\nexport interface FileStat {\n kind: 'file' | 'dir';\n size: number;\n mtimeMs?: number;\n}\n\n/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`\n * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */\nexport interface FsError extends Error {\n code:\n | 'not-found' // ENOENT\n | 'read-only' // EROFS — a `ro` mount / downgraded role; NEVER surface as UX (gate with canWrite)\n | 'not-permitted' // EACCES\n | 'exists' // EEXIST\n | 'not-empty' // ENOTEMPTY\n | 'invalid-path' // a `..` segment / absolute escape was passed as a relPath\n | 'unavailable' // no sandbox fs (local dev / pre-boot)\n | 'unknown';\n}\n\nconst ERRNO: Record<string, FsError['code']> = {\n ENOENT: 'not-found',\n EROFS: 'read-only',\n EACCES: 'not-permitted',\n EPERM: 'not-permitted',\n EEXIST: 'exists',\n ENOTEMPTY: 'not-empty',\n};\n\nconst fsError = (code: FsError['code'], message: string): FsError => {\n const err = new Error(message) as FsError;\n err.code = code;\n return err;\n};\n\nconst mapError = (e: unknown): FsError => {\n const errno = (e as { code?: string } | null)?.code;\n const code: FsError['code'] = (errno ? ERRNO[errno] : undefined) ?? 'unknown';\n const err = new Error((e as Error)?.message ?? 'fs operation failed') as FsError;\n err.code = code;\n return err;\n};\n\n// Lazily constructed so merely *importing* this module doesn't touch the\n// TextEncoder/TextDecoder globals — some non-DOM test/build environments only\n// provide them on demand, and no image/URL path needs them at all.\nlet _decoder: TextDecoder | undefined;\nlet _encoder: TextEncoder | undefined;\nconst decoder = (): TextDecoder => (_decoder ??= new TextDecoder());\nconst encoder = (): TextEncoder => (_encoder ??= new TextEncoder());\n\n// Extension → MIME type for the kinds an app displays inline. Images first (the\n// common case — `<img src>` off a mount), plus a couple of adjacent binary kinds.\n// Deliberately small: `mimeTypeFor` returns undefined for anything not here and\n// callers fall back to `application/octet-stream`.\nconst MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n avif: 'image/avif',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n};\n\n/**\n * Best-effort MIME type from a filename's extension — mainly image kinds\n * (png/jpg/jpeg/gif/webp/avif/svg/bmp/ico). Returns `undefined` when the\n * extension isn't recognized (the caller falls back to `application/octet-stream`).\n * Used by {@link MountFs.readBlob} / {@link MountFs.readObjectUrl}; exported so an\n * app can label a Blob it builds itself.\n */\nexport function mimeTypeFor(path: string): string | undefined {\n const dot = path.lastIndexOf('.');\n if (dot < 0) return undefined;\n return MIME_BY_EXT[path.slice(dot + 1).toLowerCase()];\n}\n\n// Join a mount-RELATIVE path under the mount root, rejecting `..` escapes and absolute\n// paths (CLAUDE.md security rule 3 — don't probe for escapes). The host chroot is the\n// real enforcer; this keeps an honest app from accidentally naming outside its grant.\nconst resolveUnder = (root: string, relPath: string): string => {\n if (relPath.startsWith('/')) {\n throw fsError('invalid-path', `expected a mount-relative path, got absolute \"${relPath}\"`);\n }\n const parts: string[] = [];\n for (const seg of relPath.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n throw fsError('invalid-path', `\"${relPath}\" escapes the mount root`);\n }\n parts.push(seg);\n }\n const base = root.endsWith('/') ? root.slice(0, -1) : root;\n return parts.length ? `${base}/${parts.join('/')}` : base;\n};\n\n// The longest matching `rules` subtree governs a path (mounts.ts MountRule); fall back to\n// the whole-mount `mode`. A CLIENT-SIDE hint mirroring the host rule — EROFS stays\n// authoritative (the host re-checks live policy on every write).\nconst writableAt = (mount: SandboxMount, relPath: string): boolean => {\n const path = '/' + relPath.split('/').filter((s) => s && s !== '.').join('/');\n const rules: MountRule[] | undefined = mount.rules;\n if (rules && rules.length) {\n let best: MountRule | undefined;\n for (const r of rules) {\n const sub = r.subtree.endsWith('/') ? r.subtree : r.subtree + '/';\n if (path === r.subtree || path.startsWith(sub) || r.subtree === '/') {\n if (!best || r.subtree.length > best.subtree.length) best = r;\n }\n }\n if (best) return best.mode === 'rw';\n }\n return (mount.mode ?? 'rw') === 'rw';\n};\n\n/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;\n * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).\n * Obtain one with {@link openFs}. */\nexport interface MountFs {\n /** The mount this view is anchored to (read `mode`/`rules` for writability). */\n readonly mount: SandboxMount;\n /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */\n readFile(relPath: string, encoding: 'utf8'): Promise<string>;\n readFile(relPath: string): Promise<Uint8Array>;\n /** Read a file's bytes as a `Blob`, tagged with a MIME `type` inferred from the\n * extension ({@link mimeTypeFor}) or `opts.type` when given (falls back to\n * `application/octet-stream`). The building block for downloads and object URLs. */\n readBlob(relPath: string, opts?: { type?: string }): Promise<Blob>;\n /** Read a file into an **object URL** suitable for `<img src>` / `<a href>` — the\n * fix for \"an opaque-origin iframe can't fetch a mount path\". Returns the `url`\n * and a `revoke()` you MUST call when done (typically on unmount) or the URL\n * leaks. Prefer the `useObjectUrl` hook / `MountImage` component, which revoke\n * for you; reach for this directly only outside React. */\n readObjectUrl(\n relPath: string,\n opts?: { type?: string },\n ): Promise<{ url: string; revoke: () => void }>;\n /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */\n writeFile(relPath: string, data: string | Uint8Array): Promise<void>;\n /** List a directory (the mount root when `relPath` is omitted). */\n readdir(relPath?: string): Promise<DirEntry[]>;\n /** Stat a path. Throws `not-found` if absent. */\n stat(relPath: string): Promise<FileStat>;\n /** Does `relPath` exist? Never throws on absence. */\n exists(relPath: string): Promise<boolean>;\n /** Create a directory (pass `{ recursive: true }` to make parents). */\n mkdir(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Remove a file, or a directory with `{ recursive: true }`. */\n rm(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Rename/move within the mount. */\n rename(fromRel: string, toRel: string): Promise<void>;\n /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),\n * so an app can hide an \"edit\" affordance instead of catching `read-only`\n * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade\n * flips it. EROFS from the host stays authoritative. */\n canWrite(relPath?: string): boolean;\n}\n\nconst promisesOf = (port: SandboxFsPort): NodeFsPromises =>\n (port.promises ?? (port as unknown as NodeFsPromises));\n\n/**\n * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:\n * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you\n * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path\n * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the\n * real enforcer).\n *\n * ```ts\n * import { mountSpace } from '@immediately-run/sdk';\n * import { openFs } from '@immediately-run/sdk/fs';\n * const fs = openFs(await mountSpace({ spaceId }));\n * const text = await fs.readFile('notes/idea.mdx', 'utf8');\n * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);\n * ```\n *\n * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`\n * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}\n * with a mapped `.code` (`not-found`, `read-only`, …).\n */\nexport function openFs(mount: SandboxMount): MountFs {\n const root = mount.path;\n\n const port = (): NodeFsPromises => {\n const p = sandboxFs();\n if (!p) throw fsError('unavailable', 'immediately.run: sandbox filesystem unavailable');\n return promisesOf(p);\n };\n\n const api: MountFs = {\n mount,\n async readFile(relPath: string, encoding?: 'utf8'): Promise<any> {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const data = await p.readFile(abs);\n const bytes =\n typeof data === 'string' ? encoder().encode(data) : (data as Uint8Array);\n return encoding === 'utf8' ? decoder().decode(bytes) : bytes;\n } catch (e) {\n throw mapError(e);\n }\n },\n async readBlob(relPath, opts) {\n const bytes = await api.readFile(relPath);\n const type = opts?.type ?? mimeTypeFor(relPath) ?? 'application/octet-stream';\n return new Blob([bytes as BlobPart], { type });\n },\n async readObjectUrl(relPath, opts) {\n const blob = await api.readBlob(relPath, opts);\n const url = URL.createObjectURL(blob);\n return { url, revoke: () => URL.revokeObjectURL(url) };\n },\n async writeFile(relPath, data) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.writeFile(abs, typeof data === 'string' ? encoder().encode(data) : data);\n } catch (e) {\n throw mapError(e);\n }\n },\n async readdir(relPath = '') {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const entries = await p.readdir(abs, { withFileTypes: true });\n return entries.map((d: any) =>\n typeof d === 'string'\n ? ({ name: d, kind: 'file' } as DirEntry)\n : ({ name: d.name, kind: d.isDirectory?.() ? 'dir' : 'file' } as DirEntry),\n );\n } catch (e) {\n throw mapError(e);\n }\n },\n async stat(relPath) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const s: any = await p.stat(abs);\n return {\n kind: s.isDirectory?.() ? 'dir' : 'file',\n size: typeof s.size === 'number' ? s.size : 0,\n mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,\n };\n } catch (e) {\n throw mapError(e);\n }\n },\n async exists(relPath) {\n try {\n await api.stat(relPath);\n return true;\n } catch (e) {\n if ((e as FsError).code === 'not-found') return false;\n if ((e as FsError).code === 'unavailable' || (e as FsError).code === 'invalid-path') {\n throw e;\n }\n return false;\n }\n },\n async mkdir(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.mkdir(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rm(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.rm(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rename(fromRel, toRel) {\n const p = port();\n const from = resolveUnder(root, fromRel);\n const to = resolveUnder(root, toRel);\n try {\n await p.rename(from, to);\n } catch (e) {\n throw mapError(e);\n }\n },\n canWrite(relPath = '') {\n return writableAt(mount, relPath);\n },\n };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
package/dist/hooks.cjs CHANGED
@@ -20,11 +20,13 @@ var hooks_exports = {};
20
20
  __export(hooks_exports, {
21
21
  useAllMetadata: () => useAllMetadata,
22
22
  useFileMetadata: () => useFileMetadata,
23
- useMetadataQuery: () => useMetadataQuery
23
+ useMetadataQuery: () => useMetadataQuery,
24
+ useObjectUrl: () => useObjectUrl
24
25
  });
25
26
  module.exports = __toCommonJS(hooks_exports);
26
27
  var import_react = require("react");
27
28
  var import_TinkerableContext = require("./TinkerableContext");
29
+ var import_fs = require("./fs");
28
30
  const entriesEqual = (a, b) => {
29
31
  if (a.length !== b.length) {
30
32
  return false;
@@ -65,10 +67,44 @@ const useAllMetadata = () => {
65
67
  const { filesMetadata } = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
66
68
  return filesMetadata ?? {};
67
69
  };
70
+ const useObjectUrl = (mount, relPath, opts) => {
71
+ const [state, setState] = (0, import_react.useState)({
72
+ url: null,
73
+ loading: Boolean(mount && relPath),
74
+ error: null
75
+ });
76
+ const type = opts?.type;
77
+ const mountPath = mount?.path;
78
+ (0, import_react.useEffect)(() => {
79
+ if (!mount || !relPath) {
80
+ setState({ url: null, loading: false, error: null });
81
+ return;
82
+ }
83
+ let alive = true;
84
+ let revoke = null;
85
+ setState({ url: null, loading: true, error: null });
86
+ (0, import_fs.openFs)(mount).readObjectUrl(relPath, type ? { type } : void 0).then((res) => {
87
+ if (!alive) {
88
+ res.revoke();
89
+ return;
90
+ }
91
+ revoke = res.revoke;
92
+ setState({ url: res.url, loading: false, error: null });
93
+ }).catch((e) => {
94
+ if (alive) setState({ url: null, loading: false, error: e });
95
+ });
96
+ return () => {
97
+ alive = false;
98
+ if (revoke) revoke();
99
+ };
100
+ }, [mountPath, relPath, type]);
101
+ return state;
102
+ };
68
103
  // Annotate the CommonJS export names for ESM import in node:
69
104
  0 && (module.exports = {
70
105
  useAllMetadata,
71
106
  useFileMetadata,
72
- useMetadataQuery
107
+ useMetadataQuery,
108
+ useObjectUrl
73
109
  });
74
110
  //# sourceMappingURL=hooks.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks.ts"],"sourcesContent":["import { use, useMemo, useRef } from 'react';\nimport { TinkerableContext } from './TinkerableContext';\nimport {\n FilesMetadata,\n Metadata,\n MetadataQueryEntry,\n MetadataQueryFunction,\n MetadataQueryResult,\n} from './sandboxTypes';\n\nconst entriesEqual = <T>(a: MetadataQueryEntry<T>[], b: MetadataQueryEntry<T>[]): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n // Unchanged frontmatter keeps its object identity across `metadata-update`\n // merges, so an identity check on `meta` is a sound \"did this match change?\".\n if (a[i].path !== b[i].path || a[i].meta !== b[i].meta) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Query the file metadata store (MDX frontmatter) with a plain JS function.\n *\n * The query receives every file's frontmatter keyed by path and returns the paths\n * that match; the hook resolves each path back to its frontmatter and returns an\n * array of `{ path, meta }` entries — so a single call gives you everything you\n * filtered on, with no second lookup. A throwing query is reported as `{ error }`\n * rather than crashing the render.\n *\n * The query runs synchronously during render (no empty first frame), and the\n * returned array keeps its identity while the matches are unchanged, so it is safe\n * to use directly in downstream `useMemo`/`useEffect` dependency arrays.\n *\n * Pass a type parameter to get typed frontmatter throughout:\n * ```ts\n * interface PostMeta { title: string; date: string; draft?: boolean }\n * const posts = useMetadataQuery<PostMeta>((files) =>\n * Object.entries(files)\n * .filter(([, m]) => !m.draft)\n * .sort(([, a], [, b]) => b.date.localeCompare(a.date))\n * .map(([path]) => path),\n * );\n * ```\n */\nexport const useMetadataQuery = <T = Metadata>(\n queryFunction: MetadataQueryFunction<T>,\n): MetadataQueryResult<T> => {\n const { filesMetadata } = use(TinkerableContext);\n const previous = useRef<MetadataQueryEntry<T>[]>([]);\n return useMemo<MetadataQueryResult<T>>(() => {\n const files = (filesMetadata ?? {}) as FilesMetadata<T>;\n let entries: MetadataQueryEntry<T>[];\n try {\n entries = queryFunction(files).map((path) => ({ path, meta: files[path] }));\n } catch (error) {\n return { error };\n }\n // Preserve the prior array reference when nothing matched differently.\n if (entriesEqual(entries, previous.current)) {\n return previous.current;\n }\n previous.current = entries;\n return entries;\n }, [filesMetadata, queryFunction]);\n};\n\n/**\n * Read one file's metadata (MDX frontmatter) by repo-relative path. Returns\n * `undefined` when the path has no metadata. Pass a type parameter for typed\n * field access.\n */\nexport const useFileMetadata = <T = Metadata>(path: string): T | undefined => {\n const { filesMetadata } = use(TinkerableContext);\n return useMemo(\n () => (filesMetadata ?? {})[path] as T | undefined,\n [path, filesMetadata],\n );\n};\n\n/**\n * The raw, reactive metadata store: a map from file path to frontmatter. The\n * escape hatch for apps that want to render their own index rather than express it\n * as a path-returning query. Pass a type parameter for typed frontmatter values.\n */\nexport const useAllMetadata = <T = Metadata>(): FilesMetadata<T> => {\n const { filesMetadata } = use(TinkerableContext);\n return (filesMetadata ?? {}) as FilesMetadata<T>;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAqC;AACrC,+BAAkC;AASlC,MAAM,eAAe,CAAI,GAA4B,MAAwC;AAC3F,MAAI,EAAE,WAAW,EAAE,QAAQ;AACzB,WAAO;AAAA,EACT;AACA,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAGjC,QAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AA0BO,MAAM,mBAAmB,CAC9B,kBAC2B;AAC3B,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,QAAM,eAAW,qBAAgC,CAAC,CAAC;AACnD,aAAO,sBAAgC,MAAM;AAC3C,UAAM,QAAS,iBAAiB,CAAC;AACjC,QAAI;AACJ,QAAI;AACF,gBAAU,cAAc,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,MAAM,MAAM,IAAI,EAAE,EAAE;AAAA,IAC5E,SAAS,OAAO;AACd,aAAO,EAAE,MAAM;AAAA,IACjB;AAEA,QAAI,aAAa,SAAS,SAAS,OAAO,GAAG;AAC3C,aAAO,SAAS;AAAA,IAClB;AACA,aAAS,UAAU;AACnB,WAAO;AAAA,EACT,GAAG,CAAC,eAAe,aAAa,CAAC;AACnC;AAOO,MAAM,kBAAkB,CAAe,SAAgC;AAC5E,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,aAAO;AAAA,IACL,OAAO,iBAAiB,CAAC,GAAG,IAAI;AAAA,IAChC,CAAC,MAAM,aAAa;AAAA,EACtB;AACF;AAOO,MAAM,iBAAiB,MAAsC;AAClE,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,SAAQ,iBAAiB,CAAC;AAC5B;","names":[]}
1
+ {"version":3,"sources":["../src/hooks.ts"],"sourcesContent":["import { use, useEffect, useMemo, useRef, useState } from 'react';\nimport { TinkerableContext } from './TinkerableContext';\nimport { openFs, type FsError } from './fs';\nimport type { SandboxMount } from './mounts';\nimport {\n FilesMetadata,\n Metadata,\n MetadataQueryEntry,\n MetadataQueryFunction,\n MetadataQueryResult,\n} from './sandboxTypes';\n\nconst entriesEqual = <T>(a: MetadataQueryEntry<T>[], b: MetadataQueryEntry<T>[]): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n // Unchanged frontmatter keeps its object identity across `metadata-update`\n // merges, so an identity check on `meta` is a sound \"did this match change?\".\n if (a[i].path !== b[i].path || a[i].meta !== b[i].meta) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Query the file metadata store (MDX frontmatter) with a plain JS function.\n *\n * The query receives every file's frontmatter keyed by path and returns the paths\n * that match; the hook resolves each path back to its frontmatter and returns an\n * array of `{ path, meta }` entries — so a single call gives you everything you\n * filtered on, with no second lookup. A throwing query is reported as `{ error }`\n * rather than crashing the render.\n *\n * The query runs synchronously during render (no empty first frame), and the\n * returned array keeps its identity while the matches are unchanged, so it is safe\n * to use directly in downstream `useMemo`/`useEffect` dependency arrays.\n *\n * Pass a type parameter to get typed frontmatter throughout:\n * ```ts\n * interface PostMeta { title: string; date: string; draft?: boolean }\n * const posts = useMetadataQuery<PostMeta>((files) =>\n * Object.entries(files)\n * .filter(([, m]) => !m.draft)\n * .sort(([, a], [, b]) => b.date.localeCompare(a.date))\n * .map(([path]) => path),\n * );\n * ```\n */\nexport const useMetadataQuery = <T = Metadata>(\n queryFunction: MetadataQueryFunction<T>,\n): MetadataQueryResult<T> => {\n const { filesMetadata } = use(TinkerableContext);\n const previous = useRef<MetadataQueryEntry<T>[]>([]);\n return useMemo<MetadataQueryResult<T>>(() => {\n const files = (filesMetadata ?? {}) as FilesMetadata<T>;\n let entries: MetadataQueryEntry<T>[];\n try {\n entries = queryFunction(files).map((path) => ({ path, meta: files[path] }));\n } catch (error) {\n return { error };\n }\n // Preserve the prior array reference when nothing matched differently.\n if (entriesEqual(entries, previous.current)) {\n return previous.current;\n }\n previous.current = entries;\n return entries;\n }, [filesMetadata, queryFunction]);\n};\n\n/**\n * Read one file's metadata (MDX frontmatter) by repo-relative path. Returns\n * `undefined` when the path has no metadata. Pass a type parameter for typed\n * field access.\n */\nexport const useFileMetadata = <T = Metadata>(path: string): T | undefined => {\n const { filesMetadata } = use(TinkerableContext);\n return useMemo(\n () => (filesMetadata ?? {})[path] as T | undefined,\n [path, filesMetadata],\n );\n};\n\n/**\n * The raw, reactive metadata store: a map from file path to frontmatter. The\n * escape hatch for apps that want to render their own index rather than express it\n * as a path-returning query. Pass a type parameter for typed frontmatter values.\n */\nexport const useAllMetadata = <T = Metadata>(): FilesMetadata<T> => {\n const { filesMetadata } = use(TinkerableContext);\n return (filesMetadata ?? {}) as FilesMetadata<T>;\n};\n\n/** The reactive state returned by {@link useObjectUrl}. */\nexport interface ObjectUrlState {\n /** The object URL once the bytes have loaded; `null` while loading or on error. */\n url: string | null;\n /** True while the file is being read. */\n loading: boolean;\n /** The {@link FsError} if the read failed (`not-found`, `unavailable`, …), else `null`. */\n error: FsError | null;\n}\n\n/**\n * Read a file from a mount into an **object URL** for `<img src>`, revoking it\n * automatically on unmount or when `mount`/`relPath` changes. This is the React\n * answer to \"an opaque-origin iframe can't fetch a mount path\": it reads the bytes\n * off the sandbox ZenFS ({@link openFs}) and hands you a URL to drop into an\n * `<img>`, and it owns the create/revoke lifecycle so you never leak a URL.\n *\n * Pass `null`/`undefined` for `mount` or `relPath` to mean \"nothing to load yet\"\n * (idle state, no read). For a ready-made element use `MountImage`.\n *\n * ```tsx\n * const { url, loading, error } = useObjectUrl(mount, 'photos/cat.png');\n * if (loading) return <Spinner />;\n * if (error || !url) return <span>missing</span>;\n * return <img src={url} alt=\"cat\" />;\n * ```\n */\nexport const useObjectUrl = (\n mount: SandboxMount | null | undefined,\n relPath: string | null | undefined,\n opts?: { type?: string },\n): ObjectUrlState => {\n const [state, setState] = useState<ObjectUrlState>({\n url: null,\n loading: Boolean(mount && relPath),\n error: null,\n });\n const type = opts?.type;\n // Key the effect on the mount's stable `path` (its object identity churns as the\n // mount set re-announces) plus the relPath and the optional type override.\n const mountPath = mount?.path;\n useEffect(() => {\n if (!mount || !relPath) {\n setState({ url: null, loading: false, error: null });\n return;\n }\n let alive = true;\n let revoke: (() => void) | null = null;\n setState({ url: null, loading: true, error: null });\n openFs(mount)\n .readObjectUrl(relPath, type ? { type } : undefined)\n .then((res) => {\n // Lost the race (unmounted / prop changed): revoke immediately, don't set.\n if (!alive) {\n res.revoke();\n return;\n }\n revoke = res.revoke;\n setState({ url: res.url, loading: false, error: null });\n })\n .catch((e) => {\n if (alive) setState({ url: null, loading: false, error: e as FsError });\n });\n return () => {\n alive = false;\n if (revoke) revoke();\n };\n // `mount` is intentionally tracked via its stable `mountPath`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [mountPath, relPath, type]);\n return state;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA0D;AAC1D,+BAAkC;AAClC,gBAAqC;AAUrC,MAAM,eAAe,CAAI,GAA4B,MAAwC;AAC3F,MAAI,EAAE,WAAW,EAAE,QAAQ;AACzB,WAAO;AAAA,EACT;AACA,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAGjC,QAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AA0BO,MAAM,mBAAmB,CAC9B,kBAC2B;AAC3B,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,QAAM,eAAW,qBAAgC,CAAC,CAAC;AACnD,aAAO,sBAAgC,MAAM;AAC3C,UAAM,QAAS,iBAAiB,CAAC;AACjC,QAAI;AACJ,QAAI;AACF,gBAAU,cAAc,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,MAAM,MAAM,IAAI,EAAE,EAAE;AAAA,IAC5E,SAAS,OAAO;AACd,aAAO,EAAE,MAAM;AAAA,IACjB;AAEA,QAAI,aAAa,SAAS,SAAS,OAAO,GAAG;AAC3C,aAAO,SAAS;AAAA,IAClB;AACA,aAAS,UAAU;AACnB,WAAO;AAAA,EACT,GAAG,CAAC,eAAe,aAAa,CAAC;AACnC;AAOO,MAAM,kBAAkB,CAAe,SAAgC;AAC5E,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,aAAO;AAAA,IACL,OAAO,iBAAiB,CAAC,GAAG,IAAI;AAAA,IAChC,CAAC,MAAM,aAAa;AAAA,EACtB;AACF;AAOO,MAAM,iBAAiB,MAAsC;AAClE,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,SAAQ,iBAAiB,CAAC;AAC5B;AA6BO,MAAM,eAAe,CAC1B,OACA,SACA,SACmB;AACnB,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAyB;AAAA,IACjD,KAAK;AAAA,IACL,SAAS,QAAQ,SAAS,OAAO;AAAA,IACjC,OAAO;AAAA,EACT,CAAC;AACD,QAAM,OAAO,MAAM;AAGnB,QAAM,YAAY,OAAO;AACzB,8BAAU,MAAM;AACd,QAAI,CAAC,SAAS,CAAC,SAAS;AACtB,eAAS,EAAE,KAAK,MAAM,SAAS,OAAO,OAAO,KAAK,CAAC;AACnD;AAAA,IACF;AACA,QAAI,QAAQ;AACZ,QAAI,SAA8B;AAClC,aAAS,EAAE,KAAK,MAAM,SAAS,MAAM,OAAO,KAAK,CAAC;AAClD,0BAAO,KAAK,EACT,cAAc,SAAS,OAAO,EAAE,KAAK,IAAI,MAAS,EAClD,KAAK,CAAC,QAAQ;AAEb,UAAI,CAAC,OAAO;AACV,YAAI,OAAO;AACX;AAAA,MACF;AACA,eAAS,IAAI;AACb,eAAS,EAAE,KAAK,IAAI,KAAK,SAAS,OAAO,OAAO,KAAK,CAAC;AAAA,IACxD,CAAC,EACA,MAAM,CAAC,MAAM;AACZ,UAAI,MAAO,UAAS,EAAE,KAAK,MAAM,SAAS,OAAO,OAAO,EAAa,CAAC;AAAA,IACxE,CAAC;AACH,WAAO,MAAM;AACX,cAAQ;AACR,UAAI,OAAQ,QAAO;AAAA,IACrB;AAAA,EAGF,GAAG,CAAC,WAAW,SAAS,IAAI,CAAC;AAC7B,SAAO;AACT;","names":[]}
package/dist/hooks.d.cts CHANGED
@@ -1,4 +1,7 @@
1
+ import { FsError } from './fs.cjs';
2
+ import { SandboxMount } from './mounts.cjs';
1
3
  import { Metadata, FilesMetadata, MetadataQueryFunction, MetadataQueryResult } from './sandboxTypes.cjs';
4
+ import './tasks.cjs';
2
5
 
3
6
  /**
4
7
  * Query the file metadata store (MDX frontmatter) with a plain JS function.
@@ -37,5 +40,34 @@ declare const useFileMetadata: <T = Metadata>(path: string) => T | undefined;
37
40
  * as a path-returning query. Pass a type parameter for typed frontmatter values.
38
41
  */
39
42
  declare const useAllMetadata: <T = Metadata>() => FilesMetadata<T>;
43
+ /** The reactive state returned by {@link useObjectUrl}. */
44
+ interface ObjectUrlState {
45
+ /** The object URL once the bytes have loaded; `null` while loading or on error. */
46
+ url: string | null;
47
+ /** True while the file is being read. */
48
+ loading: boolean;
49
+ /** The {@link FsError} if the read failed (`not-found`, `unavailable`, …), else `null`. */
50
+ error: FsError | null;
51
+ }
52
+ /**
53
+ * Read a file from a mount into an **object URL** for `<img src>`, revoking it
54
+ * automatically on unmount or when `mount`/`relPath` changes. This is the React
55
+ * answer to "an opaque-origin iframe can't fetch a mount path": it reads the bytes
56
+ * off the sandbox ZenFS ({@link openFs}) and hands you a URL to drop into an
57
+ * `<img>`, and it owns the create/revoke lifecycle so you never leak a URL.
58
+ *
59
+ * Pass `null`/`undefined` for `mount` or `relPath` to mean "nothing to load yet"
60
+ * (idle state, no read). For a ready-made element use `MountImage`.
61
+ *
62
+ * ```tsx
63
+ * const { url, loading, error } = useObjectUrl(mount, 'photos/cat.png');
64
+ * if (loading) return <Spinner />;
65
+ * if (error || !url) return <span>missing</span>;
66
+ * return <img src={url} alt="cat" />;
67
+ * ```
68
+ */
69
+ declare const useObjectUrl: (mount: SandboxMount | null | undefined, relPath: string | null | undefined, opts?: {
70
+ type?: string;
71
+ }) => ObjectUrlState;
40
72
 
41
- export { useAllMetadata, useFileMetadata, useMetadataQuery };
73
+ export { type ObjectUrlState, useAllMetadata, useFileMetadata, useMetadataQuery, useObjectUrl };
package/dist/hooks.d.ts CHANGED
@@ -1,4 +1,7 @@
1
+ import { FsError } from './fs.js';
2
+ import { SandboxMount } from './mounts.js';
1
3
  import { Metadata, FilesMetadata, MetadataQueryFunction, MetadataQueryResult } from './sandboxTypes.js';
4
+ import './tasks.js';
2
5
 
3
6
  /**
4
7
  * Query the file metadata store (MDX frontmatter) with a plain JS function.
@@ -37,5 +40,34 @@ declare const useFileMetadata: <T = Metadata>(path: string) => T | undefined;
37
40
  * as a path-returning query. Pass a type parameter for typed frontmatter values.
38
41
  */
39
42
  declare const useAllMetadata: <T = Metadata>() => FilesMetadata<T>;
43
+ /** The reactive state returned by {@link useObjectUrl}. */
44
+ interface ObjectUrlState {
45
+ /** The object URL once the bytes have loaded; `null` while loading or on error. */
46
+ url: string | null;
47
+ /** True while the file is being read. */
48
+ loading: boolean;
49
+ /** The {@link FsError} if the read failed (`not-found`, `unavailable`, …), else `null`. */
50
+ error: FsError | null;
51
+ }
52
+ /**
53
+ * Read a file from a mount into an **object URL** for `<img src>`, revoking it
54
+ * automatically on unmount or when `mount`/`relPath` changes. This is the React
55
+ * answer to "an opaque-origin iframe can't fetch a mount path": it reads the bytes
56
+ * off the sandbox ZenFS ({@link openFs}) and hands you a URL to drop into an
57
+ * `<img>`, and it owns the create/revoke lifecycle so you never leak a URL.
58
+ *
59
+ * Pass `null`/`undefined` for `mount` or `relPath` to mean "nothing to load yet"
60
+ * (idle state, no read). For a ready-made element use `MountImage`.
61
+ *
62
+ * ```tsx
63
+ * const { url, loading, error } = useObjectUrl(mount, 'photos/cat.png');
64
+ * if (loading) return <Spinner />;
65
+ * if (error || !url) return <span>missing</span>;
66
+ * return <img src={url} alt="cat" />;
67
+ * ```
68
+ */
69
+ declare const useObjectUrl: (mount: SandboxMount | null | undefined, relPath: string | null | undefined, opts?: {
70
+ type?: string;
71
+ }) => ObjectUrlState;
40
72
 
41
- export { useAllMetadata, useFileMetadata, useMetadataQuery };
73
+ export { type ObjectUrlState, useAllMetadata, useFileMetadata, useMetadataQuery, useObjectUrl };
package/dist/hooks.js CHANGED
@@ -1,5 +1,6 @@
1
- import { use, useMemo, useRef } from "react";
1
+ import { use, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { TinkerableContext } from "./TinkerableContext";
3
+ import { openFs } from "./fs";
3
4
  const entriesEqual = (a, b) => {
4
5
  if (a.length !== b.length) {
5
6
  return false;
@@ -40,9 +41,43 @@ const useAllMetadata = () => {
40
41
  const { filesMetadata } = use(TinkerableContext);
41
42
  return filesMetadata ?? {};
42
43
  };
44
+ const useObjectUrl = (mount, relPath, opts) => {
45
+ const [state, setState] = useState({
46
+ url: null,
47
+ loading: Boolean(mount && relPath),
48
+ error: null
49
+ });
50
+ const type = opts?.type;
51
+ const mountPath = mount?.path;
52
+ useEffect(() => {
53
+ if (!mount || !relPath) {
54
+ setState({ url: null, loading: false, error: null });
55
+ return;
56
+ }
57
+ let alive = true;
58
+ let revoke = null;
59
+ setState({ url: null, loading: true, error: null });
60
+ openFs(mount).readObjectUrl(relPath, type ? { type } : void 0).then((res) => {
61
+ if (!alive) {
62
+ res.revoke();
63
+ return;
64
+ }
65
+ revoke = res.revoke;
66
+ setState({ url: res.url, loading: false, error: null });
67
+ }).catch((e) => {
68
+ if (alive) setState({ url: null, loading: false, error: e });
69
+ });
70
+ return () => {
71
+ alive = false;
72
+ if (revoke) revoke();
73
+ };
74
+ }, [mountPath, relPath, type]);
75
+ return state;
76
+ };
43
77
  export {
44
78
  useAllMetadata,
45
79
  useFileMetadata,
46
- useMetadataQuery
80
+ useMetadataQuery,
81
+ useObjectUrl
47
82
  };
48
83
  //# sourceMappingURL=hooks.js.map
package/dist/hooks.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks.ts"],"sourcesContent":["import { use, useMemo, useRef } from 'react';\nimport { TinkerableContext } from './TinkerableContext';\nimport {\n FilesMetadata,\n Metadata,\n MetadataQueryEntry,\n MetadataQueryFunction,\n MetadataQueryResult,\n} from './sandboxTypes';\n\nconst entriesEqual = <T>(a: MetadataQueryEntry<T>[], b: MetadataQueryEntry<T>[]): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n // Unchanged frontmatter keeps its object identity across `metadata-update`\n // merges, so an identity check on `meta` is a sound \"did this match change?\".\n if (a[i].path !== b[i].path || a[i].meta !== b[i].meta) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Query the file metadata store (MDX frontmatter) with a plain JS function.\n *\n * The query receives every file's frontmatter keyed by path and returns the paths\n * that match; the hook resolves each path back to its frontmatter and returns an\n * array of `{ path, meta }` entries — so a single call gives you everything you\n * filtered on, with no second lookup. A throwing query is reported as `{ error }`\n * rather than crashing the render.\n *\n * The query runs synchronously during render (no empty first frame), and the\n * returned array keeps its identity while the matches are unchanged, so it is safe\n * to use directly in downstream `useMemo`/`useEffect` dependency arrays.\n *\n * Pass a type parameter to get typed frontmatter throughout:\n * ```ts\n * interface PostMeta { title: string; date: string; draft?: boolean }\n * const posts = useMetadataQuery<PostMeta>((files) =>\n * Object.entries(files)\n * .filter(([, m]) => !m.draft)\n * .sort(([, a], [, b]) => b.date.localeCompare(a.date))\n * .map(([path]) => path),\n * );\n * ```\n */\nexport const useMetadataQuery = <T = Metadata>(\n queryFunction: MetadataQueryFunction<T>,\n): MetadataQueryResult<T> => {\n const { filesMetadata } = use(TinkerableContext);\n const previous = useRef<MetadataQueryEntry<T>[]>([]);\n return useMemo<MetadataQueryResult<T>>(() => {\n const files = (filesMetadata ?? {}) as FilesMetadata<T>;\n let entries: MetadataQueryEntry<T>[];\n try {\n entries = queryFunction(files).map((path) => ({ path, meta: files[path] }));\n } catch (error) {\n return { error };\n }\n // Preserve the prior array reference when nothing matched differently.\n if (entriesEqual(entries, previous.current)) {\n return previous.current;\n }\n previous.current = entries;\n return entries;\n }, [filesMetadata, queryFunction]);\n};\n\n/**\n * Read one file's metadata (MDX frontmatter) by repo-relative path. Returns\n * `undefined` when the path has no metadata. Pass a type parameter for typed\n * field access.\n */\nexport const useFileMetadata = <T = Metadata>(path: string): T | undefined => {\n const { filesMetadata } = use(TinkerableContext);\n return useMemo(\n () => (filesMetadata ?? {})[path] as T | undefined,\n [path, filesMetadata],\n );\n};\n\n/**\n * The raw, reactive metadata store: a map from file path to frontmatter. The\n * escape hatch for apps that want to render their own index rather than express it\n * as a path-returning query. Pass a type parameter for typed frontmatter values.\n */\nexport const useAllMetadata = <T = Metadata>(): FilesMetadata<T> => {\n const { filesMetadata } = use(TinkerableContext);\n return (filesMetadata ?? {}) as FilesMetadata<T>;\n};\n"],"mappings":"AAAA,SAAS,KAAK,SAAS,cAAc;AACrC,SAAS,yBAAyB;AASlC,MAAM,eAAe,CAAI,GAA4B,MAAwC;AAC3F,MAAI,EAAE,WAAW,EAAE,QAAQ;AACzB,WAAO;AAAA,EACT;AACA,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAGjC,QAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AA0BO,MAAM,mBAAmB,CAC9B,kBAC2B;AAC3B,QAAM,EAAE,cAAc,IAAI,IAAI,iBAAiB;AAC/C,QAAM,WAAW,OAAgC,CAAC,CAAC;AACnD,SAAO,QAAgC,MAAM;AAC3C,UAAM,QAAS,iBAAiB,CAAC;AACjC,QAAI;AACJ,QAAI;AACF,gBAAU,cAAc,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,MAAM,MAAM,IAAI,EAAE,EAAE;AAAA,IAC5E,SAAS,OAAO;AACd,aAAO,EAAE,MAAM;AAAA,IACjB;AAEA,QAAI,aAAa,SAAS,SAAS,OAAO,GAAG;AAC3C,aAAO,SAAS;AAAA,IAClB;AACA,aAAS,UAAU;AACnB,WAAO;AAAA,EACT,GAAG,CAAC,eAAe,aAAa,CAAC;AACnC;AAOO,MAAM,kBAAkB,CAAe,SAAgC;AAC5E,QAAM,EAAE,cAAc,IAAI,IAAI,iBAAiB;AAC/C,SAAO;AAAA,IACL,OAAO,iBAAiB,CAAC,GAAG,IAAI;AAAA,IAChC,CAAC,MAAM,aAAa;AAAA,EACtB;AACF;AAOO,MAAM,iBAAiB,MAAsC;AAClE,QAAM,EAAE,cAAc,IAAI,IAAI,iBAAiB;AAC/C,SAAQ,iBAAiB,CAAC;AAC5B;","names":[]}
1
+ {"version":3,"sources":["../src/hooks.ts"],"sourcesContent":["import { use, useEffect, useMemo, useRef, useState } from 'react';\nimport { TinkerableContext } from './TinkerableContext';\nimport { openFs, type FsError } from './fs';\nimport type { SandboxMount } from './mounts';\nimport {\n FilesMetadata,\n Metadata,\n MetadataQueryEntry,\n MetadataQueryFunction,\n MetadataQueryResult,\n} from './sandboxTypes';\n\nconst entriesEqual = <T>(a: MetadataQueryEntry<T>[], b: MetadataQueryEntry<T>[]): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n // Unchanged frontmatter keeps its object identity across `metadata-update`\n // merges, so an identity check on `meta` is a sound \"did this match change?\".\n if (a[i].path !== b[i].path || a[i].meta !== b[i].meta) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Query the file metadata store (MDX frontmatter) with a plain JS function.\n *\n * The query receives every file's frontmatter keyed by path and returns the paths\n * that match; the hook resolves each path back to its frontmatter and returns an\n * array of `{ path, meta }` entries — so a single call gives you everything you\n * filtered on, with no second lookup. A throwing query is reported as `{ error }`\n * rather than crashing the render.\n *\n * The query runs synchronously during render (no empty first frame), and the\n * returned array keeps its identity while the matches are unchanged, so it is safe\n * to use directly in downstream `useMemo`/`useEffect` dependency arrays.\n *\n * Pass a type parameter to get typed frontmatter throughout:\n * ```ts\n * interface PostMeta { title: string; date: string; draft?: boolean }\n * const posts = useMetadataQuery<PostMeta>((files) =>\n * Object.entries(files)\n * .filter(([, m]) => !m.draft)\n * .sort(([, a], [, b]) => b.date.localeCompare(a.date))\n * .map(([path]) => path),\n * );\n * ```\n */\nexport const useMetadataQuery = <T = Metadata>(\n queryFunction: MetadataQueryFunction<T>,\n): MetadataQueryResult<T> => {\n const { filesMetadata } = use(TinkerableContext);\n const previous = useRef<MetadataQueryEntry<T>[]>([]);\n return useMemo<MetadataQueryResult<T>>(() => {\n const files = (filesMetadata ?? {}) as FilesMetadata<T>;\n let entries: MetadataQueryEntry<T>[];\n try {\n entries = queryFunction(files).map((path) => ({ path, meta: files[path] }));\n } catch (error) {\n return { error };\n }\n // Preserve the prior array reference when nothing matched differently.\n if (entriesEqual(entries, previous.current)) {\n return previous.current;\n }\n previous.current = entries;\n return entries;\n }, [filesMetadata, queryFunction]);\n};\n\n/**\n * Read one file's metadata (MDX frontmatter) by repo-relative path. Returns\n * `undefined` when the path has no metadata. Pass a type parameter for typed\n * field access.\n */\nexport const useFileMetadata = <T = Metadata>(path: string): T | undefined => {\n const { filesMetadata } = use(TinkerableContext);\n return useMemo(\n () => (filesMetadata ?? {})[path] as T | undefined,\n [path, filesMetadata],\n );\n};\n\n/**\n * The raw, reactive metadata store: a map from file path to frontmatter. The\n * escape hatch for apps that want to render their own index rather than express it\n * as a path-returning query. Pass a type parameter for typed frontmatter values.\n */\nexport const useAllMetadata = <T = Metadata>(): FilesMetadata<T> => {\n const { filesMetadata } = use(TinkerableContext);\n return (filesMetadata ?? {}) as FilesMetadata<T>;\n};\n\n/** The reactive state returned by {@link useObjectUrl}. */\nexport interface ObjectUrlState {\n /** The object URL once the bytes have loaded; `null` while loading or on error. */\n url: string | null;\n /** True while the file is being read. */\n loading: boolean;\n /** The {@link FsError} if the read failed (`not-found`, `unavailable`, …), else `null`. */\n error: FsError | null;\n}\n\n/**\n * Read a file from a mount into an **object URL** for `<img src>`, revoking it\n * automatically on unmount or when `mount`/`relPath` changes. This is the React\n * answer to \"an opaque-origin iframe can't fetch a mount path\": it reads the bytes\n * off the sandbox ZenFS ({@link openFs}) and hands you a URL to drop into an\n * `<img>`, and it owns the create/revoke lifecycle so you never leak a URL.\n *\n * Pass `null`/`undefined` for `mount` or `relPath` to mean \"nothing to load yet\"\n * (idle state, no read). For a ready-made element use `MountImage`.\n *\n * ```tsx\n * const { url, loading, error } = useObjectUrl(mount, 'photos/cat.png');\n * if (loading) return <Spinner />;\n * if (error || !url) return <span>missing</span>;\n * return <img src={url} alt=\"cat\" />;\n * ```\n */\nexport const useObjectUrl = (\n mount: SandboxMount | null | undefined,\n relPath: string | null | undefined,\n opts?: { type?: string },\n): ObjectUrlState => {\n const [state, setState] = useState<ObjectUrlState>({\n url: null,\n loading: Boolean(mount && relPath),\n error: null,\n });\n const type = opts?.type;\n // Key the effect on the mount's stable `path` (its object identity churns as the\n // mount set re-announces) plus the relPath and the optional type override.\n const mountPath = mount?.path;\n useEffect(() => {\n if (!mount || !relPath) {\n setState({ url: null, loading: false, error: null });\n return;\n }\n let alive = true;\n let revoke: (() => void) | null = null;\n setState({ url: null, loading: true, error: null });\n openFs(mount)\n .readObjectUrl(relPath, type ? { type } : undefined)\n .then((res) => {\n // Lost the race (unmounted / prop changed): revoke immediately, don't set.\n if (!alive) {\n res.revoke();\n return;\n }\n revoke = res.revoke;\n setState({ url: res.url, loading: false, error: null });\n })\n .catch((e) => {\n if (alive) setState({ url: null, loading: false, error: e as FsError });\n });\n return () => {\n alive = false;\n if (revoke) revoke();\n };\n // `mount` is intentionally tracked via its stable `mountPath`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [mountPath, relPath, type]);\n return state;\n};\n"],"mappings":"AAAA,SAAS,KAAK,WAAW,SAAS,QAAQ,gBAAgB;AAC1D,SAAS,yBAAyB;AAClC,SAAS,cAA4B;AAUrC,MAAM,eAAe,CAAI,GAA4B,MAAwC;AAC3F,MAAI,EAAE,WAAW,EAAE,QAAQ;AACzB,WAAO;AAAA,EACT;AACA,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAGjC,QAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AA0BO,MAAM,mBAAmB,CAC9B,kBAC2B;AAC3B,QAAM,EAAE,cAAc,IAAI,IAAI,iBAAiB;AAC/C,QAAM,WAAW,OAAgC,CAAC,CAAC;AACnD,SAAO,QAAgC,MAAM;AAC3C,UAAM,QAAS,iBAAiB,CAAC;AACjC,QAAI;AACJ,QAAI;AACF,gBAAU,cAAc,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,MAAM,MAAM,IAAI,EAAE,EAAE;AAAA,IAC5E,SAAS,OAAO;AACd,aAAO,EAAE,MAAM;AAAA,IACjB;AAEA,QAAI,aAAa,SAAS,SAAS,OAAO,GAAG;AAC3C,aAAO,SAAS;AAAA,IAClB;AACA,aAAS,UAAU;AACnB,WAAO;AAAA,EACT,GAAG,CAAC,eAAe,aAAa,CAAC;AACnC;AAOO,MAAM,kBAAkB,CAAe,SAAgC;AAC5E,QAAM,EAAE,cAAc,IAAI,IAAI,iBAAiB;AAC/C,SAAO;AAAA,IACL,OAAO,iBAAiB,CAAC,GAAG,IAAI;AAAA,IAChC,CAAC,MAAM,aAAa;AAAA,EACtB;AACF;AAOO,MAAM,iBAAiB,MAAsC;AAClE,QAAM,EAAE,cAAc,IAAI,IAAI,iBAAiB;AAC/C,SAAQ,iBAAiB,CAAC;AAC5B;AA6BO,MAAM,eAAe,CAC1B,OACA,SACA,SACmB;AACnB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAyB;AAAA,IACjD,KAAK;AAAA,IACL,SAAS,QAAQ,SAAS,OAAO;AAAA,IACjC,OAAO;AAAA,EACT,CAAC;AACD,QAAM,OAAO,MAAM;AAGnB,QAAM,YAAY,OAAO;AACzB,YAAU,MAAM;AACd,QAAI,CAAC,SAAS,CAAC,SAAS;AACtB,eAAS,EAAE,KAAK,MAAM,SAAS,OAAO,OAAO,KAAK,CAAC;AACnD;AAAA,IACF;AACA,QAAI,QAAQ;AACZ,QAAI,SAA8B;AAClC,aAAS,EAAE,KAAK,MAAM,SAAS,MAAM,OAAO,KAAK,CAAC;AAClD,WAAO,KAAK,EACT,cAAc,SAAS,OAAO,EAAE,KAAK,IAAI,MAAS,EAClD,KAAK,CAAC,QAAQ;AAEb,UAAI,CAAC,OAAO;AACV,YAAI,OAAO;AACX;AAAA,MACF;AACA,eAAS,IAAI;AACb,eAAS,EAAE,KAAK,IAAI,KAAK,SAAS,OAAO,OAAO,KAAK,CAAC;AAAA,IACxD,CAAC,EACA,MAAM,CAAC,MAAM;AACZ,UAAI,MAAO,UAAS,EAAE,KAAK,MAAM,SAAS,OAAO,OAAO,EAAa,CAAC;AAAA,IACxE,CAAC;AACH,WAAO,MAAM;AACX,cAAQ;AACR,UAAI,OAAQ,QAAO;AAAA,IACrB;AAAA,EAGF,GAAG,CAAC,WAAW,SAAS,IAAI,CAAC;AAC7B,SAAO;AACT;","names":[]}
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ __reExport(index_exports, require("./routing"), module.exports);
20
20
  __reExport(index_exports, require("./boot"), module.exports);
21
21
  __reExport(index_exports, require("./components/Include"), module.exports);
22
22
  __reExport(index_exports, require("./components/MDXComponents"), module.exports);
23
+ __reExport(index_exports, require("./components/MountImage"), module.exports);
23
24
  __reExport(index_exports, require("./components/Routes"), module.exports);
24
25
  __reExport(index_exports, require("./hooks"), module.exports);
25
26
  __reExport(index_exports, require("./auth"), module.exports);
@@ -54,6 +55,7 @@ __reExport(index_exports, require("./sandboxTypes"), module.exports);
54
55
  ...require("./boot"),
55
56
  ...require("./components/Include"),
56
57
  ...require("./components/MDXComponents"),
58
+ ...require("./components/MountImage"),
57
59
  ...require("./components/Routes"),
58
60
  ...require("./hooks"),
59
61
  ...require("./auth"),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAIA,0BAAc,uCAJd;AAKA,0BAAc,gCALd;AAMA,0BAAc,oBANd;AAOA,0BAAc,mBAPd;AAQA,0BAAc,oBARd;AASA,0BAAc,4BATd;AAUA,0BAAc,qBAVd;AAWA,0BAAc,yBAXd;AAYA,0BAAc,qBAZd;AAaA,0BAAc,qBAbd;AAcA,0BAAc,yBAdd;AAeA,0BAAc,sBAfd;AAgBA,0BAAc,kBAhBd;AAiBA,0BAAc,kBAjBd;AAkBA,0BAAc,uBAlBd;AAmBA,0BAAc,sBAnBd;AAoBA,0BAAc,kBApBd;AAqBA,0BAAc,0BArBd;AAsBA,0BAAc,yBAtBd;AAuBA,0BAAc,iBAvBd;AAwBA,0BAAc,oBAxBd;AAyBA,0BAAc,oBAzBd;AA0BA,0BAAc,sBA1Bd;AA2BA,0BAAc,wBA3Bd;AA4BA,0BAAc,oBA5Bd;AA6BA,0BAAc,sBA7Bd;AA8BA,0BAAc,6BA9Bd;AA+BA,0BAAc,2BA/Bd;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAIA,0BAAc,uCAJd;AAKA,0BAAc,oCALd;AAMA,0BAAc,gCANd;AAOA,0BAAc,oBAPd;AAQA,0BAAc,mBARd;AASA,0BAAc,oBATd;AAUA,0BAAc,4BAVd;AAWA,0BAAc,qBAXd;AAYA,0BAAc,yBAZd;AAaA,0BAAc,qBAbd;AAcA,0BAAc,qBAdd;AAeA,0BAAc,yBAfd;AAgBA,0BAAc,sBAhBd;AAiBA,0BAAc,kBAjBd;AAkBA,0BAAc,kBAlBd;AAmBA,0BAAc,uBAnBd;AAoBA,0BAAc,sBApBd;AAqBA,0BAAc,kBArBd;AAsBA,0BAAc,0BAtBd;AAuBA,0BAAc,yBAvBd;AAwBA,0BAAc,iBAxBd;AAyBA,0BAAc,oBAzBd;AA0BA,0BAAc,oBA1Bd;AA2BA,0BAAc,sBA3Bd;AA4BA,0BAAc,wBA5Bd;AA6BA,0BAAc,oBA7Bd;AA8BA,0BAAc,sBA9Bd;AA+BA,0BAAc,6BA/Bd;AAgCA,0BAAc,2BAhCd;","names":[]}
package/dist/index.d.cts CHANGED
@@ -3,15 +3,16 @@ export { AppliedRoutingRule, Router, applyRoutingRule, navigate, renderRoute, us
3
3
  export { BootProps, CATCH_ALL_ROUTING_SPEC, DEFAULT_ROUTING_SPEC, TinkerableApp, boot } from './boot.cjs';
4
4
  export { Include, RenderExportedComponent, RenderExportedComponentContext, RenderFileContextType } from './components/Include.cjs';
5
5
  export { DEFAULT_MDX_COMPONENTS, InternalLink, Link } from './components/MDXComponents.cjs';
6
+ export { MountImage, MountImageProps } from './components/MountImage.cjs';
6
7
  export { Route, RouteProps, Routes } from './components/Routes.cjs';
7
- export { useAllMetadata, useFileMetadata, useMetadataQuery } from './hooks.cjs';
8
+ export { ObjectUrlState, useAllMetadata, useFileMetadata, useMetadataQuery, useObjectUrl } from './hooks.cjs';
8
9
  export { AuthState, AuthStatus, SandboxUser, getAuthState, onAuthChange, useAuth } from './auth.cjs';
9
10
  export { HostTheme, getHostTheme, onHostThemeChange, setHostTheme, useHostTheme } from './theme.cjs';
10
11
  export { EditorContext, getEditorContext, onEditorContextChange, useEditorContext } from './editorContext.cjs';
11
12
  export { EditTarget, EditorOpenError, EditorSessionError, EditorWriteError, RequestEditError, closeFile, createFile, createFolder, deleteEntry, openInEditor, renameEntry, requestEdit, setActiveFile, uploadFile } from './editor.cjs';
12
13
  export { FormFactor, FormFactorClass, Orientation, getFormFactor, onFormFactorChange, useFormFactor } from './formFactor.cjs';
13
14
  export { getRegion, useRegion } from './region.cjs';
14
- export { GrantRecord, Member, MountQuery, MountRemoveReason, MountRule, RemovedMount, ResolvedUser, Role, SandboxMount, SpaceError, SpaceInfo, createSpace, findMount, getAppMountPath, getMounts, getSpaceMembers, importSettingsFromParent, listAllSpaces, listGrants, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount } from './mounts.cjs';
15
+ export { GrantRecord, Invite, Member, MountQuery, MountRemoveReason, MountRule, RemovedMount, ResolvedUser, Role, SandboxMount, SpaceError, SpaceInfo, acceptInvite, createSpace, declineInvite, findMount, getAppMountPath, getMounts, getSpaceMembers, importSettingsFromParent, inviteToSpace, listAllSpaces, listGrants, listMyInvites, listPendingInvites, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, revokeInvite, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount } from './mounts.cjs';
15
16
  export { ContributeMode, ContributeOptions, ContributionEvent, ContributionResult, contribute } from './contribute.cjs';
16
17
  export { ApiMethod, getCatalog, invoke, invokeStream, onCatalogChange, useCatalog } from './catalog.cjs';
17
18
  export { RegionMessage, onRegionMessage, postToRegion, useRegionMessage } from './ipc.cjs';
@@ -21,7 +22,7 @@ export { SecretError, SecretGrant, SecretHints, SecretQuery, SecretType, SecretV
21
22
  export { ChatDelta, ChatFeatures, ChatMessage, ChatProviderInfo, ChatRequest, ChatResult, ChatRole, ChatStopReason, ContentPart, ToolDef, chat, describeChat, onChatProviderChange, useChatProvider } from './llm.cjs';
22
23
  export { BuildError, ConsoleEntry, ConsoleLevel, Diagnostics, DiagnosticsProvenance, getDiagnostics, onDiagnosticsChange, useDiagnostics } from './diagnostics.cjs';
23
24
  export { FsChange, getFsChange, onFsChange, useFsChange } from './onFsChange.cjs';
24
- export { DirEntry, FileStat, FsError, MountFs, SandboxFsPort, fsAvailable, openAppFs, openFs, sandboxFs } from './fs.cjs';
25
+ export { DirEntry, FileStat, FsError, MountFs, SandboxFsPort, fsAvailable, mimeTypeFor, openAppFs, openFs, sandboxFs } from './fs.cjs';
25
26
  export { DebugLevel, debug, isDebugEnabled, log, useDebugEnabled } from './debug.cjs';
26
27
  export { DirCap, FileCap, TaskInput, cancelTask, capDir, capFile, completeTask, getTaskInput, invokeTask, useTaskInput } from './tasks.cjs';
27
28
  export { SDK_PROTOCOL_VERSION, SdkHandshake, announceHandshake, sdkHandshake } from './runtime.cjs';
package/dist/index.d.ts CHANGED
@@ -3,15 +3,16 @@ export { AppliedRoutingRule, Router, applyRoutingRule, navigate, renderRoute, us
3
3
  export { BootProps, CATCH_ALL_ROUTING_SPEC, DEFAULT_ROUTING_SPEC, TinkerableApp, boot } from './boot.js';
4
4
  export { Include, RenderExportedComponent, RenderExportedComponentContext, RenderFileContextType } from './components/Include.js';
5
5
  export { DEFAULT_MDX_COMPONENTS, InternalLink, Link } from './components/MDXComponents.js';
6
+ export { MountImage, MountImageProps } from './components/MountImage.js';
6
7
  export { Route, RouteProps, Routes } from './components/Routes.js';
7
- export { useAllMetadata, useFileMetadata, useMetadataQuery } from './hooks.js';
8
+ export { ObjectUrlState, useAllMetadata, useFileMetadata, useMetadataQuery, useObjectUrl } from './hooks.js';
8
9
  export { AuthState, AuthStatus, SandboxUser, getAuthState, onAuthChange, useAuth } from './auth.js';
9
10
  export { HostTheme, getHostTheme, onHostThemeChange, setHostTheme, useHostTheme } from './theme.js';
10
11
  export { EditorContext, getEditorContext, onEditorContextChange, useEditorContext } from './editorContext.js';
11
12
  export { EditTarget, EditorOpenError, EditorSessionError, EditorWriteError, RequestEditError, closeFile, createFile, createFolder, deleteEntry, openInEditor, renameEntry, requestEdit, setActiveFile, uploadFile } from './editor.js';
12
13
  export { FormFactor, FormFactorClass, Orientation, getFormFactor, onFormFactorChange, useFormFactor } from './formFactor.js';
13
14
  export { getRegion, useRegion } from './region.js';
14
- export { GrantRecord, Member, MountQuery, MountRemoveReason, MountRule, RemovedMount, ResolvedUser, Role, SandboxMount, SpaceError, SpaceInfo, createSpace, findMount, getAppMountPath, getMounts, getSpaceMembers, importSettingsFromParent, listAllSpaces, listGrants, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount } from './mounts.js';
15
+ export { GrantRecord, Invite, Member, MountQuery, MountRemoveReason, MountRule, RemovedMount, ResolvedUser, Role, SandboxMount, SpaceError, SpaceInfo, acceptInvite, createSpace, declineInvite, findMount, getAppMountPath, getMounts, getSpaceMembers, importSettingsFromParent, inviteToSpace, listAllSpaces, listGrants, listMyInvites, listPendingInvites, listSettingsApps, listSpaces, lookupUser, makeContentRef, mount, mountSpace, onMountsChange, openSettings, openSettingsOf, requestMount, requestSpace, resolveContentRef, resolveContentRefs, revokeGrant, revokeInvite, setSpaceRole, shareSpace, unmountSpace, unshareSpace, useMounts, waitForMount } from './mounts.js';
15
16
  export { ContributeMode, ContributeOptions, ContributionEvent, ContributionResult, contribute } from './contribute.js';
16
17
  export { ApiMethod, getCatalog, invoke, invokeStream, onCatalogChange, useCatalog } from './catalog.js';
17
18
  export { RegionMessage, onRegionMessage, postToRegion, useRegionMessage } from './ipc.js';
@@ -21,7 +22,7 @@ export { SecretError, SecretGrant, SecretHints, SecretQuery, SecretType, SecretV
21
22
  export { ChatDelta, ChatFeatures, ChatMessage, ChatProviderInfo, ChatRequest, ChatResult, ChatRole, ChatStopReason, ContentPart, ToolDef, chat, describeChat, onChatProviderChange, useChatProvider } from './llm.js';
22
23
  export { BuildError, ConsoleEntry, ConsoleLevel, Diagnostics, DiagnosticsProvenance, getDiagnostics, onDiagnosticsChange, useDiagnostics } from './diagnostics.js';
23
24
  export { FsChange, getFsChange, onFsChange, useFsChange } from './onFsChange.js';
24
- export { DirEntry, FileStat, FsError, MountFs, SandboxFsPort, fsAvailable, openAppFs, openFs, sandboxFs } from './fs.js';
25
+ export { DirEntry, FileStat, FsError, MountFs, SandboxFsPort, fsAvailable, mimeTypeFor, openAppFs, openFs, sandboxFs } from './fs.js';
25
26
  export { DebugLevel, debug, isDebugEnabled, log, useDebugEnabled } from './debug.js';
26
27
  export { DirCap, FileCap, TaskInput, cancelTask, capDir, capFile, completeTask, getTaskInput, invokeTask, useTaskInput } from './tasks.js';
27
28
  export { SDK_PROTOCOL_VERSION, SdkHandshake, announceHandshake, sdkHandshake } from './runtime.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./routing";
3
3
  export * from "./boot";
4
4
  export * from "./components/Include";
5
5
  export * from "./components/MDXComponents";
6
+ export * from "./components/MountImage";
6
7
  export * from "./components/Routes";
7
8
  export * from "./hooks";
8
9
  export * from "./auth";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./MDXProvider\";\nexport * from \"./routing\";\nexport * from \"./boot\";\nexport * from './components/Include';\nexport * from './components/MDXComponents';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks'\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './region';\nexport * from './mounts';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './secrets';\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './sandboxTypes';\n"],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var injectedBundler_exports = {};
20
20
  __export(injectedBundler_exports, {
21
21
  getInjectedMetadataEmitter: () => getInjectedMetadataEmitter,
22
+ getInjectedMetadataSnapshot: () => getInjectedMetadataSnapshot,
22
23
  resolveMetadataSource: () => resolveMetadataSource
23
24
  });
24
25
  module.exports = __toCommonJS(injectedBundler_exports);
@@ -39,11 +40,19 @@ const getInjectedMetadataEmitter = () => {
39
40
  }
40
41
  return null;
41
42
  };
43
+ const getInjectedMetadataSnapshot = () => {
44
+ const b = injectedBundler();
45
+ if (b && typeof b.getMetadataSnapshot === "function") {
46
+ return b.getMetadataSnapshot();
47
+ }
48
+ return null;
49
+ };
42
50
  const resolveMetadataSource = (injected) => injected ? { event: injected.onMetadataChange, enable: () => injected.enable() } : { event: void 0, enable: () => {
43
51
  } };
44
52
  // Annotate the CommonJS export names for ESM import in node:
45
53
  0 && (module.exports = {
46
54
  getInjectedMetadataEmitter,
55
+ getInjectedMetadataSnapshot,
47
56
  resolveMetadataSource
48
57
  });
49
58
  //# sourceMappingURL=injectedBundler.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/injectedBundler.ts"],"sourcesContent":["// Injected sandbox-bundler access (SDK_PACKAGING_SPEC §4/§8, Phase 5). The SDK was\n// historically wired straight to the injected bundler service objects\n// (`module.evaluation.module.bundler.<x>`). Phase 5 makes the SDK transport-agnostic\n// so those services can eventually be retired: every reader PREFERS the injection\n// (so the current, live path stays byte-for-byte unchanged) and FALLS BACK to the\n// §4 transport when the SDK is fetched from npm with no injection present.\n//\n// This module centralizes the `module.evaluation.module.bundler.*` reads (the same\n// philosophy as `sandboxUtils`' transport resolver) and exposes a PURE resolver for\n// the metadata-update subscription so the dual-mode decision is unit-tested without\n// a live bundler. The ambient reads themselves are thin (untestable without the\n// sandbox realm, exactly like `sandboxUtils.transport()`).\n\n/** vscode-style Event source: subscribe with a listener, get a disposable back. */\nexport type EventSource = (listener: (msg: any) => void) => { dispose(): void };\n\n/** The injected bundler's metadata emitter — fires `{type:'metadata-update', update}`\n * as files (re)compile. Absent when the SDK is npm-fetched (no injection). */\nexport interface InjectedMetadataEmitter {\n onMetadataChange: EventSource;\n /** Start the DelayedEmitter once a subscriber is attached (injected path only). */\n enable(): void;\n}\n\n/** The injected bundler object, or null when there is no injection (npm-fetched). */\nconst injectedBundler = (): any | null => {\n try {\n // @ts-ignore - `module.evaluation` is injected by the sandbox runtime\n return module?.evaluation?.module?.bundler ?? null;\n } catch {\n return null;\n }\n};\n\n/** The injected bundler's metadata emitter, or null when npm-fetched. */\nexport const getInjectedMetadataEmitter = (): InjectedMetadataEmitter | null => {\n const b = injectedBundler();\n if (b && typeof b.onMetadataChange === 'function' && b.onMetadataChangeEmitter) {\n return {\n onMetadataChange: b.onMetadataChange,\n enable: () => b.onMetadataChangeEmitter.enable(),\n };\n }\n return null;\n};\n\n/** What `boot` needs to subscribe to metadata updates: the `event` source to hand\n * `addListener` (the injected emitter, or `undefined` → listen over the transport)\n * and an `enable` to start the injected DelayedEmitter (a no-op off-injection). */\nexport interface MetadataSource {\n event?: EventSource;\n enable(): void;\n}\n\n/**\n * Resolve the metadata-update subscription source (PURE — the dual-mode decision).\n * With the injected emitter: use it and arm it, so the live path is byte-identical.\n * Without it (npm-fetched): return no `event`, so the caller's `addListener` falls\n * back to the §4 transport's `onMessage`, and `enable` is a no-op.\n */\nexport const resolveMetadataSource = (\n injected: InjectedMetadataEmitter | null,\n): MetadataSource =>\n injected\n ? { event: injected.onMetadataChange, enable: () => injected.enable() }\n : { event: undefined, enable: () => {} };\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBA,MAAM,kBAAkB,MAAkB;AACxC,MAAI;AAEF,WAAO,QAAQ,YAAY,QAAQ,WAAW;AAAA,EAChD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,MAAM,6BAA6B,MAAsC;AAC9E,QAAM,IAAI,gBAAgB;AAC1B,MAAI,KAAK,OAAO,EAAE,qBAAqB,cAAc,EAAE,yBAAyB;AAC9E,WAAO;AAAA,MACL,kBAAkB,EAAE;AAAA,MACpB,QAAQ,MAAM,EAAE,wBAAwB,OAAO;AAAA,IACjD;AAAA,EACF;AACA,SAAO;AACT;AAgBO,MAAM,wBAAwB,CACnC,aAEA,WACI,EAAE,OAAO,SAAS,kBAAkB,QAAQ,MAAM,SAAS,OAAO,EAAE,IACpE,EAAE,OAAO,QAAW,QAAQ,MAAM;AAAC,EAAE;","names":[]}
1
+ {"version":3,"sources":["../src/injectedBundler.ts"],"sourcesContent":["// Injected sandbox-bundler access (SDK_PACKAGING_SPEC §4/§8, Phase 5). The SDK was\n// historically wired straight to the injected bundler service objects\n// (`module.evaluation.module.bundler.<x>`). Phase 5 makes the SDK transport-agnostic\n// so those services can eventually be retired: every reader PREFERS the injection\n// (so the current, live path stays byte-for-byte unchanged) and FALLS BACK to the\n// §4 transport when the SDK is fetched from npm with no injection present.\n//\n// This module centralizes the `module.evaluation.module.bundler.*` reads (the same\n// philosophy as `sandboxUtils`' transport resolver) and exposes a PURE resolver for\n// the metadata-update subscription so the dual-mode decision is unit-tested without\n// a live bundler. The ambient reads themselves are thin (untestable without the\n// sandbox realm, exactly like `sandboxUtils.transport()`).\n\n/** vscode-style Event source: subscribe with a listener, get a disposable back. */\nexport type EventSource = (listener: (msg: any) => void) => { dispose(): void };\n\n/** The injected bundler's metadata emitter — fires `{type:'metadata-update', update}`\n * as files (re)compile. Absent when the SDK is npm-fetched (no injection). */\nexport interface InjectedMetadataEmitter {\n onMetadataChange: EventSource;\n /** Start the DelayedEmitter once a subscriber is attached (injected path only). */\n enable(): void;\n}\n\n/** The injected bundler object, or null when there is no injection (npm-fetched). */\nconst injectedBundler = (): any | null => {\n try {\n // @ts-ignore - `module.evaluation` is injected by the sandbox runtime\n return module?.evaluation?.module?.bundler ?? null;\n } catch {\n return null;\n }\n};\n\n/** The injected bundler's metadata emitter, or null when npm-fetched. */\nexport const getInjectedMetadataEmitter = (): InjectedMetadataEmitter | null => {\n const b = injectedBundler();\n if (b && typeof b.onMetadataChange === 'function' && b.onMetadataChangeEmitter) {\n return {\n onMetadataChange: b.onMetadataChange,\n enable: () => b.onMetadataChangeEmitter.enable(),\n };\n }\n return null;\n};\n\n/**\n * The injected bundler's synchronous metadata snapshot for the boot seed\n * (MDX_CONTENT_COLLECTIONS_SPEC §1.4). Returns the full `/app`-keyed collection the\n * bundler seeded (from the frontmatter sidecar) so the app's first render already\n * holds it — the SDK-side counterpart of the bundler seeding. Null when npm-fetched\n * (no in-realm bundler) → the SDK degrades to event-fill over the §4 transport, no\n * first-paint guarantee. The returned VALUE refs are the same objects the emitter\n * replays, so the `enable()` replay is a no-op (the §1.4 identity contract).\n */\nexport const getInjectedMetadataSnapshot = (): Record<string, Record<string, any>> | null => {\n const b = injectedBundler();\n if (b && typeof b.getMetadataSnapshot === 'function') {\n return b.getMetadataSnapshot();\n }\n return null;\n};\n\n/** What `boot` needs to subscribe to metadata updates: the `event` source to hand\n * `addListener` (the injected emitter, or `undefined` → listen over the transport)\n * and an `enable` to start the injected DelayedEmitter (a no-op off-injection). */\nexport interface MetadataSource {\n event?: EventSource;\n enable(): void;\n}\n\n/**\n * Resolve the metadata-update subscription source (PURE — the dual-mode decision).\n * With the injected emitter: use it and arm it, so the live path is byte-identical.\n * Without it (npm-fetched): return no `event`, so the caller's `addListener` falls\n * back to the §4 transport's `onMessage`, and `enable` is a no-op.\n */\nexport const resolveMetadataSource = (\n injected: InjectedMetadataEmitter | null,\n): MetadataSource =>\n injected\n ? { event: injected.onMetadataChange, enable: () => injected.enable() }\n : { event: undefined, enable: () => {} };\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBA,MAAM,kBAAkB,MAAkB;AACxC,MAAI;AAEF,WAAO,QAAQ,YAAY,QAAQ,WAAW;AAAA,EAChD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,MAAM,6BAA6B,MAAsC;AAC9E,QAAM,IAAI,gBAAgB;AAC1B,MAAI,KAAK,OAAO,EAAE,qBAAqB,cAAc,EAAE,yBAAyB;AAC9E,WAAO;AAAA,MACL,kBAAkB,EAAE;AAAA,MACpB,QAAQ,MAAM,EAAE,wBAAwB,OAAO;AAAA,IACjD;AAAA,EACF;AACA,SAAO;AACT;AAWO,MAAM,8BAA8B,MAAkD;AAC3F,QAAM,IAAI,gBAAgB;AAC1B,MAAI,KAAK,OAAO,EAAE,wBAAwB,YAAY;AACpD,WAAO,EAAE,oBAAoB;AAAA,EAC/B;AACA,SAAO;AACT;AAgBO,MAAM,wBAAwB,CACnC,aAEA,WACI,EAAE,OAAO,SAAS,kBAAkB,QAAQ,MAAM,SAAS,OAAO,EAAE,IACpE,EAAE,OAAO,QAAW,QAAQ,MAAM;AAAC,EAAE;","names":[]}
@@ -11,6 +11,16 @@ interface InjectedMetadataEmitter {
11
11
  }
12
12
  /** The injected bundler's metadata emitter, or null when npm-fetched. */
13
13
  declare const getInjectedMetadataEmitter: () => InjectedMetadataEmitter | null;
14
+ /**
15
+ * The injected bundler's synchronous metadata snapshot for the boot seed
16
+ * (MDX_CONTENT_COLLECTIONS_SPEC §1.4). Returns the full `/app`-keyed collection the
17
+ * bundler seeded (from the frontmatter sidecar) so the app's first render already
18
+ * holds it — the SDK-side counterpart of the bundler seeding. Null when npm-fetched
19
+ * (no in-realm bundler) → the SDK degrades to event-fill over the §4 transport, no
20
+ * first-paint guarantee. The returned VALUE refs are the same objects the emitter
21
+ * replays, so the `enable()` replay is a no-op (the §1.4 identity contract).
22
+ */
23
+ declare const getInjectedMetadataSnapshot: () => Record<string, Record<string, any>> | null;
14
24
  /** What `boot` needs to subscribe to metadata updates: the `event` source to hand
15
25
  * `addListener` (the injected emitter, or `undefined` → listen over the transport)
16
26
  * and an `enable` to start the injected DelayedEmitter (a no-op off-injection). */
@@ -26,4 +36,4 @@ interface MetadataSource {
26
36
  */
27
37
  declare const resolveMetadataSource: (injected: InjectedMetadataEmitter | null) => MetadataSource;
28
38
 
29
- export { type EventSource, type InjectedMetadataEmitter, type MetadataSource, getInjectedMetadataEmitter, resolveMetadataSource };
39
+ export { type EventSource, type InjectedMetadataEmitter, type MetadataSource, getInjectedMetadataEmitter, getInjectedMetadataSnapshot, resolveMetadataSource };
@@ -11,6 +11,16 @@ interface InjectedMetadataEmitter {
11
11
  }
12
12
  /** The injected bundler's metadata emitter, or null when npm-fetched. */
13
13
  declare const getInjectedMetadataEmitter: () => InjectedMetadataEmitter | null;
14
+ /**
15
+ * The injected bundler's synchronous metadata snapshot for the boot seed
16
+ * (MDX_CONTENT_COLLECTIONS_SPEC §1.4). Returns the full `/app`-keyed collection the
17
+ * bundler seeded (from the frontmatter sidecar) so the app's first render already
18
+ * holds it — the SDK-side counterpart of the bundler seeding. Null when npm-fetched
19
+ * (no in-realm bundler) → the SDK degrades to event-fill over the §4 transport, no
20
+ * first-paint guarantee. The returned VALUE refs are the same objects the emitter
21
+ * replays, so the `enable()` replay is a no-op (the §1.4 identity contract).
22
+ */
23
+ declare const getInjectedMetadataSnapshot: () => Record<string, Record<string, any>> | null;
14
24
  /** What `boot` needs to subscribe to metadata updates: the `event` source to hand
15
25
  * `addListener` (the injected emitter, or `undefined` → listen over the transport)
16
26
  * and an `enable` to start the injected DelayedEmitter (a no-op off-injection). */
@@ -26,4 +36,4 @@ interface MetadataSource {
26
36
  */
27
37
  declare const resolveMetadataSource: (injected: InjectedMetadataEmitter | null) => MetadataSource;
28
38
 
29
- export { type EventSource, type InjectedMetadataEmitter, type MetadataSource, getInjectedMetadataEmitter, resolveMetadataSource };
39
+ export { type EventSource, type InjectedMetadataEmitter, type MetadataSource, getInjectedMetadataEmitter, getInjectedMetadataSnapshot, resolveMetadataSource };
@@ -15,10 +15,18 @@ const getInjectedMetadataEmitter = () => {
15
15
  }
16
16
  return null;
17
17
  };
18
+ const getInjectedMetadataSnapshot = () => {
19
+ const b = injectedBundler();
20
+ if (b && typeof b.getMetadataSnapshot === "function") {
21
+ return b.getMetadataSnapshot();
22
+ }
23
+ return null;
24
+ };
18
25
  const resolveMetadataSource = (injected) => injected ? { event: injected.onMetadataChange, enable: () => injected.enable() } : { event: void 0, enable: () => {
19
26
  } };
20
27
  export {
21
28
  getInjectedMetadataEmitter,
29
+ getInjectedMetadataSnapshot,
22
30
  resolveMetadataSource
23
31
  };
24
32
  //# sourceMappingURL=injectedBundler.js.map