{"version":3,"sources":["../src/hooks.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from '@immediately-run/platform-constants';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { openFs, type FsError } from './fs';\nimport { useMetadataStore } from './metadataSource';\nimport type { SandboxMount } from './mounts';\nimport {\n  FilesMetadata,\n  Metadata,\n  MetadataQueryEntry,\n  MetadataQueryFunction,\n  MetadataQueryResult,\n} from './sandboxTypes';\n\nconst entriesEqual = <T>(\n  a: MetadataQueryEntry<T, Record<string, unknown>>[],\n  b: MetadataQueryEntry<T, Record<string, unknown>>[],\n): 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    // Extra fields a record-returning query computed (R3-276) are part of the\n    // result, so a change in them is a change in the result — compared by value,\n    // one level, because they are derived scalars, not the store's shared objects.\n    const ax = a[i] as Record<string, unknown>;\n    const bx = b[i] as Record<string, unknown>;\n    const keys = new Set([...Object.keys(ax), ...Object.keys(bx)]);\n    for (const k of keys) {\n      if (k === 'path' || k === 'meta') continue;\n      if (ax[k] !== bx[k]) 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 * A query may return RECORDS instead of bare paths (R3-276) — `{ path, ...extra }`\n * — and the extra fields ride along on each entry, so something derived while\n * selecting does not have to be recomputed downstream:\n * ```ts\n * const posts = useMetadataQuery<PostMeta, { year: string }>((files) =>\n *   Object.entries(files).map(([path, m]) => ({ path, year: m.date.slice(0, 4) })),\n * );\n * ```\n * The store it queries is the nearest {@link MetadataSource}, else the host's.\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, E extends object = {}>(\n  queryFunction: MetadataQueryFunction<T>,\n): MetadataQueryResult<T, E> => {\n  const files = useMetadataStore<T>();\n  const previous = useRef<MetadataQueryEntry<T, Record<string, unknown>>[]>([]);\n  return useMemo<MetadataQueryResult<T, E>>(() => {\n    let entries: MetadataQueryEntry<T, Record<string, unknown>>[];\n    try {\n      // `path` and `meta` are applied AFTER the record's own fields: a query cannot\n      // shadow the two the hook is responsible for, whatever it happened to name.\n      entries = queryFunction(files).map((selected) =>\n        typeof selected === 'string'\n          ? { path: selected, meta: files[selected] }\n          : { ...selected, path: selected.path, meta: files[selected.path] },\n      );\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 as MetadataQueryEntry<T, E>[];\n    }\n    previous.current = entries;\n    return entries as MetadataQueryEntry<T, E>[];\n  }, [files, queryFunction]);\n};\n\n/**\n * Read one file's metadata (MDX frontmatter) by path. Returns `undefined` when the\n * path has no metadata. Pass a type parameter for typed field access.\n *\n * **The store is keyed by ABSOLUTE module path** — `/app/content/post.mdx`, the same\n * identifier `fs`, `module.dynamicImport` and `<Include>` use — not by the\n * repo-relative path this doc claimed until R3-276. Keeping metadata in the file\n * space is what lets an app read a file's metadata and render that same file by the\n * same path (`sandbox/src/bundler/metadataKey.test.ts` pins it).\n *\n * A repo-relative path (`/content/post.mdx`) is accepted as a fallback: if the path\n * is not a key, it is retried under the app root via the shared\n * `underAppRoot` helper (R3-275). That is additive — it only turns a previous\n * `undefined` into a value — and it exists because the old doc told people to pass\n * exactly that form. A path a {@link MetadataSource} provided in some other key\n * space is looked up as given, unchanged.\n */\nexport const useFileMetadata = <T = Metadata>(path: string): T | undefined => {\n  const files = useMetadataStore<T>();\n  return useMemo(() => {\n    const direct = files[path];\n    if (direct !== undefined) return direct;\n    // The fallback is for a path in the REPO-RELATIVE space. An already-app-rooted\n    // path that missed is simply a miss: retrying it would consult `/app/app/…`,\n    // which is not a key space anything writes — it would only ever hit by accident.\n    if (path === APP_ROOT || path.startsWith(`${APP_ROOT}/`)) return undefined;\n    return files[underAppRoot(path)];\n  }, [path, files]);\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> => useMetadataStore<T>();\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 *\n * Off-host: under `vite dev` with the `@immediately-run/dev-fs` plugin (>= 0.5.0)\n * this works against your local disk — the plugin publishes its fs bridge where\n * the SDK discovers the sandbox fs (see `sandboxFs` in `fs.ts`). Under plain\n * `vite dev` (no plugin) there is no filesystem at all, and the hook settles to\n * `{ url: null, loading: false, error }` with `error.code === 'unavailable'` —\n * render the error/absent state, don't treat it as forever-loading.\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":"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","names":[]}