@elytracms/studio 0.0.100 → 0.0.102

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 (170) hide show
  1. package/convex/analytics-cost.test.ts +1 -1
  2. package/convex/asset-meta-patch.test.ts +7 -27
  3. package/convex/assets.ts +6 -41
  4. package/convex/cms.ts +2 -29
  5. package/convex/delivery.ts +2 -2
  6. package/convex/record-functions.ts +15 -82
  7. package/convex/redirects.ts +11 -116
  8. package/convex/schema.ts +4 -23
  9. package/convex/shared-capture.convex.test.ts +165 -0
  10. package/convex/triggers.ts +7 -14
  11. package/dist/components/ai/ai-chat-panel-lazy.js +7 -2
  12. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  13. package/dist/components/ai/ai-chat-panel.js +8 -5
  14. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  15. package/dist/components/ai/proposal-card.js +2 -1
  16. package/dist/components/ai/proposal-card.js.map +1 -1
  17. package/dist/components/ai/proposal-unfilled.d.ts +14 -0
  18. package/dist/components/ai/proposal-unfilled.js +33 -0
  19. package/dist/components/ai/proposal-unfilled.js.map +1 -0
  20. package/dist/components/ai/translate-document-action.js +8 -6
  21. package/dist/components/ai/translate-document-action.js.map +1 -1
  22. package/dist/components/ai/translate-skipped.js +1 -0
  23. package/dist/components/ai/translate-skipped.js.map +1 -1
  24. package/dist/components/app-views/studio-config.d.ts +12 -12
  25. package/dist/components/app-views/studio-config.js.map +1 -1
  26. package/dist/components/assets/asset-detail.js +1 -2
  27. package/dist/components/assets/asset-detail.js.map +1 -1
  28. package/dist/components/assets/asset-metadata.d.ts +2 -9
  29. package/dist/components/assets/asset-metadata.js +3 -10
  30. package/dist/components/assets/asset-metadata.js.map +1 -1
  31. package/dist/components/assets/asset-search.js +1 -1
  32. package/dist/components/assets/asset-search.js.map +1 -1
  33. package/dist/components/assets/asset-selection.js +2 -2
  34. package/dist/components/assets/asset-selection.js.map +1 -1
  35. package/dist/components/assets/assets-collection-view-source.d.ts +2 -3
  36. package/dist/components/assets/assets-collection-view-source.js +2 -3
  37. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  38. package/dist/components/builder/__fixtures__/reuse-race.d.ts +30 -0
  39. package/dist/components/builder/__fixtures__/reuse-race.js +77 -0
  40. package/dist/components/builder/__fixtures__/reuse-race.js.map +1 -0
  41. package/dist/components/builder/builder-store.d.ts +37 -1
  42. package/dist/components/builder/builder-store.js +21 -6
  43. package/dist/components/builder/builder-store.js.map +1 -1
  44. package/dist/components/builder/canvas-chrome-css.d.ts +1 -1
  45. package/dist/components/builder/canvas-chrome-css.js +1 -1
  46. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  47. package/dist/components/builder/canvas-chrome.d.ts +1 -1
  48. package/dist/components/builder/canvas-chrome.js +1 -1
  49. package/dist/components/builder/canvas-chrome.js.map +1 -1
  50. package/dist/components/builder/canvas-rebase.d.ts +4 -4
  51. package/dist/components/builder/canvas-rebase.js +3 -2
  52. package/dist/components/builder/canvas-rebase.js.map +1 -1
  53. package/dist/components/builder/canvas-render-context.d.ts +1 -1
  54. package/dist/components/builder/canvas-render-context.js +1 -1
  55. package/dist/components/builder/canvas-render-context.js.map +1 -1
  56. package/dist/components/builder/composition-field-editor-lazy.js +2 -1
  57. package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
  58. package/dist/components/builder/composition-field-editor.js +3 -3
  59. package/dist/components/builder/composition-field-editor.js.map +1 -1
  60. package/dist/components/builder/reuse-context.d.ts +9 -2
  61. package/dist/components/builder/reuse-context.js +46 -6
  62. package/dist/components/builder/reuse-context.js.map +1 -1
  63. package/dist/components/builder/reuse.d.ts +113 -70
  64. package/dist/components/builder/reuse.js +123 -108
  65. package/dist/components/builder/reuse.js.map +1 -1
  66. package/dist/components/cms/asset-field.js +1 -3
  67. package/dist/components/cms/asset-field.js.map +1 -1
  68. package/dist/components/cms/cell.d.ts +2 -2
  69. package/dist/components/cms/cell.js +8 -18
  70. package/dist/components/cms/cell.js.map +1 -1
  71. package/dist/components/cms/cms-store.d.ts +9 -1
  72. package/dist/components/cms/cms-store.js +1 -0
  73. package/dist/components/cms/cms-store.js.map +1 -1
  74. package/dist/components/cms/detail-editor.js +15 -14
  75. package/dist/components/cms/detail-editor.js.map +1 -1
  76. package/dist/components/cms/field-control.js +13 -1
  77. package/dist/components/cms/field-control.js.map +1 -1
  78. package/dist/components/cms/prop-fields.js +1 -1
  79. package/dist/components/cms/prop-fields.js.map +1 -1
  80. package/dist/components/cms/rich-text/lazy.js +2 -1
  81. package/dist/components/cms/rich-text/lazy.js.map +1 -1
  82. package/dist/components/cms/rich-text/toolbar-state.d.ts +1 -1
  83. package/dist/components/cms/rich-text/toolbar-state.js +1 -1
  84. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
  85. package/dist/components/cms/use-field-focus.js +3 -3
  86. package/dist/components/cms/use-field-focus.js.map +1 -1
  87. package/dist/components/cms/version-compare-lazy.js +2 -1
  88. package/dist/components/cms/version-compare-lazy.js.map +1 -1
  89. package/dist/components/components/components-index.js +1 -1
  90. package/dist/components/components/components-index.js.map +1 -1
  91. package/dist/components/deployment-setup-gate.d.ts +1 -1
  92. package/dist/components/deployment-setup-gate.js +1 -1
  93. package/dist/components/deployment-setup-gate.js.map +1 -1
  94. package/dist/components/load-failure.d.ts +62 -0
  95. package/dist/components/load-failure.js +110 -0
  96. package/dist/components/load-failure.js.map +1 -0
  97. package/dist/components/notifications/shell-findings.js +3 -3
  98. package/dist/components/notifications/shell-findings.js.map +1 -1
  99. package/dist/customization.d.ts +3 -3
  100. package/dist/customization.js.map +1 -1
  101. package/dist/lib/ai/apply-content.js +99 -28
  102. package/dist/lib/ai/apply-content.js.map +1 -1
  103. package/dist/lib/ai/chat-context.d.ts +77 -7
  104. package/dist/lib/ai/chat-context.js +139 -14
  105. package/dist/lib/ai/chat-context.js.map +1 -1
  106. package/dist/lib/ai/chat-state.d.ts +7 -1
  107. package/dist/lib/ai/chat-state.js.map +1 -1
  108. package/dist/lib/ai/constraints.d.ts +5 -0
  109. package/dist/lib/ai/constraints.js +21 -6
  110. package/dist/lib/ai/constraints.js.map +1 -1
  111. package/dist/lib/ai/fake-provider.d.ts +76 -26
  112. package/dist/lib/ai/fake-provider.js +210 -66
  113. package/dist/lib/ai/fake-provider.js.map +1 -1
  114. package/dist/lib/ai/index.d.ts +1 -0
  115. package/dist/lib/ai/index.js +1 -0
  116. package/dist/lib/ai/index.js.map +1 -1
  117. package/dist/lib/ai/keyless-assistant.d.ts +37 -0
  118. package/dist/lib/ai/keyless-assistant.js +193 -0
  119. package/dist/lib/ai/keyless-assistant.js.map +1 -0
  120. package/dist/lib/ai/knowledge.d.ts +9 -9
  121. package/dist/lib/ai/knowledge.js +22 -0
  122. package/dist/lib/ai/knowledge.js.map +1 -1
  123. package/dist/lib/ai/shape.d.ts +25 -2
  124. package/dist/lib/ai/shape.js +58 -7
  125. package/dist/lib/ai/shape.js.map +1 -1
  126. package/dist/lib/ai/tools.d.ts +18 -1
  127. package/dist/lib/ai/tools.js +74 -11
  128. package/dist/lib/ai/tools.js.map +1 -1
  129. package/dist/lib/ai/translate-document.d.ts +8 -1
  130. package/dist/lib/ai/translate-document.js +25 -9
  131. package/dist/lib/ai/translate-document.js.map +1 -1
  132. package/dist/lib/ai/types.d.ts +22 -7
  133. package/dist/lib/ai/types.js.map +1 -1
  134. package/dist/lib/ai/update-fields.js +13 -17
  135. package/dist/lib/ai/update-fields.js.map +1 -1
  136. package/dist/lib/ai/writer-form.d.ts +72 -3
  137. package/dist/lib/ai/writer-form.js +320 -42
  138. package/dist/lib/ai/writer-form.js.map +1 -1
  139. package/dist/lib/assets/upload.d.ts +1 -2
  140. package/dist/lib/assets/upload.js +1 -2
  141. package/dist/lib/assets/upload.js.map +1 -1
  142. package/dist/lib/auth-errors.d.ts +1 -1
  143. package/dist/lib/auth-errors.js +1 -1
  144. package/dist/lib/auth-errors.js.map +1 -1
  145. package/dist/lib/engine-version.d.ts +1 -1
  146. package/dist/lib/engine-version.js +1 -1
  147. package/dist/lib/engine-version.js.map +1 -1
  148. package/dist/lib/i18n/messages.de.d.ts +1 -1
  149. package/dist/lib/i18n/messages.de.js +32 -8
  150. package/dist/lib/i18n/messages.de.js.map +1 -1
  151. package/dist/lib/i18n/messages.en.d.ts +25 -3
  152. package/dist/lib/i18n/messages.en.js +40 -10
  153. package/dist/lib/i18n/messages.en.js.map +1 -1
  154. package/dist/lib/operations/cms-persistence.d.ts +28 -0
  155. package/dist/lib/operations/cms-persistence.js +55 -0
  156. package/dist/lib/operations/cms-persistence.js.map +1 -1
  157. package/dist/lib/persistence/convex-adapter.js +7 -2
  158. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  159. package/dist/lib/workspace/index.d.ts +3 -3
  160. package/dist/lib/workspace/index.js +3 -3
  161. package/dist/lib/workspace/index.js.map +1 -1
  162. package/dist/router/navigate-once.d.ts +1 -1
  163. package/dist/router/navigate-once.js.map +1 -1
  164. package/dist/router/root.d.ts +3 -2
  165. package/dist/router/root.js +25 -4
  166. package/dist/router/root.js.map +1 -1
  167. package/package.json +3 -3
  168. package/convex/asset-alt-migration.test.ts +0 -127
  169. package/convex/redirect-migration.test.ts +0 -205
  170. package/convex/version-kind-stamping.test.ts +0 -118
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/workspace/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEV_FRONTEND_URL_ENV } from '@elytracms/core/export-sync/deployment-ready'\nimport { projectFrontendUrl, RESERVED_STUDIO_ROOT_SEGMENTS } from '@elytracms/core/studio-core'\nimport type {\n BreakpointConfig,\n ElytraConfigInput,\n ResolvedProject,\n} from '@elytracms/core/studio-core'\nimport type { CollectionDef, LocaleConfig, RouteRecord } from '@elytracms/core/cms-core'\nimport type { SidebarNode } from '../../components/app-views/module'\nimport type { CustomFieldRenderer, StudioConfig } from '../../components/app-views/studio-config'\nimport type { GeneratedPreviewLookup } from '../../components/builder/blocks-model'\nimport { isDevRuntime, provideStudioEnv, studioEnv } from '../persistence/backend-mode'\nimport { resolveWorkspaceState } from './workspace-source'\nimport type { WorkspaceState } from './workspace-source'\n\nexport {\n PROJECT_CONFIG_FILE,\n PROJECT_CONFIG_ISSUE_KEYS,\n PROJECT_CONFIG_POINT,\n projectConfigIssues,\n} from './config-issues'\nexport { resolveWorkspaceState } from './workspace-source'\nexport type { WorkspaceSources, WorkspaceState } from './workspace-source'\nexport {\n isSingleProjectWorkspace,\n launcherAutoForwardTarget,\n persistedProjects,\n selectSwitcherProjects,\n soleWorkspaceProject,\n unprovisionedProjects,\n} from './switcher-source'\nexport type { SwitcherEntry, SwitcherSourceInput } from './switcher-source'\n\nlet cached: WorkspaceState | null = null\nlet studioConfig: StudioConfig | null = null\n\n/**\n * Mount the studio against a project config (EC-223 mount contract) and an\n * optional studio-only customization config (EC-236). The consumer's entry point\n * does `import config from './elytra.config'` + `import studio from\n * './studio.config'` and calls this — an explicit init, NOT an `import.meta.glob`\n * autoload that reaches into the consumer's filesystem (Oli, 2026-06-19: \"there's\n * always an entry point that can trigger a function call\").\n *\n * Two configs, deliberately: `config` (`elytra.config.ts`) is the project\n * definition, shared with the `frontend/` delivery bundle and projected to data;\n * `studio` (`studio.config.tsx` via `defineStudio`) is live builder UI code — the\n * section rail, custom field renderers, branding — that must NEVER reach delivery.\n * Keeping them separate is the customization boundary principle in the mount\n * signature. Seeds the workspace state + the studio-config holder every later\n * query (`getActiveProjectSlug`, `resolveDeployment*`) reads.\n *\n * EC-357 — `options.env`: the consumer's `import.meta.env`, captured HERE because\n * the entry module is code Vite always transforms live. The engine's own\n * `import.meta.env` reads are unreliable inside dep-optimized package code (Vite ≥8\n * omits custom `VITE_*` values there; Vite 7 froze them at optimize time), which\n * silently broke backend URL / mode / dev-login resolution in real consumers.\n * Always pass it: `createStudio(config, studioConfig, { env: import.meta.env })`.\n */\nexport interface StudioMountOptions {\n /** The consumer entry's `import.meta.env` (see `provideStudioEnv` for the contract). */\n env?: Record<string, unknown>\n}\n\nexport function createStudio(\n config: ElytraConfigInput,\n studio?: StudioConfig,\n options?: StudioMountOptions,\n): void {\n provideStudioEnv(options?.env)\n cached = resolveWorkspaceState({\n fileModule: { default: config },\n fileName: 'elytra.config.ts',\n })\n studioConfig = studio ?? null\n}\n\n/** Test seam: set/clear the studio customization config. Never used by app code. */\nexport function setStudioConfigForTests(next: StudioConfig | null): void {\n studioConfig = next\n}\n\n/**\n * The active project slug (EC-179, vision AD-8). One project per backend, so the\n * active project selects which Convex deployment the studio talks to. The root\n * layout sets this from the URL (`/<slug>`) — or, on the sign-in screen, from\n * the `redirect` target's slug — before the auth/persistence adapters are\n * selected, so the *initial* sign-in authenticates against the project the user\n * is heading for (Sanity-style: authenticate to one workspace, then the full\n * config-based switcher is available). `null` at the launcher → the default\n * (first) configured project's deployment.\n */\nlet activeProjectSlug: string | null = null\n\n/** Set the active project slug; returns whether the resolved deployment changed. */\nexport function setActiveProjectSlug(slug: string | null): boolean {\n const before = resolveDeploymentUrl('')\n activeProjectSlug = slug && slug.trim() !== '' ? slug : null\n return resolveDeploymentUrl('') !== before\n}\n\nexport function getActiveProjectSlug(): string | null {\n return activeProjectSlug\n}\n\n/** Test seam: reset the active project slug. Never used by app code. */\nexport function setActiveProjectSlugForTests(slug: string | null): void {\n activeProjectSlug = slug\n}\n\n/**\n * Reserved first-path segments that are NOT project slugs: the studio's own\n * root routes (EC-220). Project surfaces now live at `/<slug>/...` (no `/p/`\n * prefix), so the studio's root namespace is shared between project slugs and\n * these fixed routes — they must be excluded from slug extraction. The launcher\n * (`/`) is the empty segment, excluded by the regex requiring one path char.\n *\n * EC-583 (RESIDUE-8): declared ONCE, in core — the resolver refuses a project slug in\n * this set, so a config can no longer name a project this reader would skip.\n */\nconst RESERVED_ROOT_SEGMENTS: ReadonlySet<string> = new Set(RESERVED_STUDIO_ROOT_SEGMENTS)\n\n/** Extract a project slug from a studio path (`/<slug>/...`), else `null`. */\nexport function projectSlugFromPath(pathname: string): string | null {\n const match = /^\\/([^/?#]+)/.exec(pathname)\n if (!match) return null\n const segment = decodeURIComponent(match[1]!)\n return RESERVED_ROOT_SEGMENTS.has(segment) ? null : segment\n}\n\n/**\n * The project whose backend the studio should bind for a given location\n * (EC-179). A project path (`/<slug>`) binds that project; the sign-in screen\n * binds the project from its `redirect` target so the *initial* sign-in\n * authenticates against the backend the user is heading for (Sanity-style).\n * Anywhere else (the launcher) → `null`, i.e. the default/first project.\n */\nexport function activeSlugForLocation(pathname: string, redirect?: string): string | null {\n const direct = projectSlugFromPath(pathname)\n if (direct) return direct\n if (pathname === '/sign-in' && redirect) return projectSlugFromPath(redirect)\n return null\n}\n\n/**\n * THE reader of \"the config project with this slug\" (EC-595 A2 → EC-582: one reader —\n * the operations runtime resolves an OPERATION's project through it, whether or not it\n * is the active one; the deployment lookup and the active project read it too).\n * `undefined` for a slug no config project declares (env-only wiring, fixtures).\n */\nexport function configProjectFor(slug: string): ResolvedProject | undefined {\n const state = getWorkspaceState()\n return state.kind === 'config' ? state.projects.find((p) => p.slug === slug) : undefined\n}\n\n/** The active config project (by slug), or the first configured project. */\nfunction activeConfigProject() {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return undefined\n return (activeProjectSlug ? configProjectFor(activeProjectSlug) : undefined) ?? state.projects[0]\n}\n\n/**\n * The studio's resolved workspace state. Deterministic per boot: the config is\n * static input (bundled file or deployment env), so it is resolved once and\n * cached — every consumer (gate, switcher, launcher) sees the same state.\n */\nexport function getWorkspaceState(): WorkspaceState {\n // Seeded by `createStudio(config)` at the entry point. Until then — or when the\n // consumer wires no config — resolve to the empty `none` state (the single-project\n // golden path), rather than reaching for the filesystem.\n if (!cached) cached = resolveWorkspaceState({})\n return cached\n}\n\n/** Test seam: clear/replace the cached state. Never used by app code. */\nexport function setWorkspaceStateForTests(next: WorkspaceState | null): void {\n cached = next\n}\n\n/**\n * Resolve the Convex deployment URL the studio should connect to (vision AD-8:\n * the backend is wired in config). One project per backend, so the **active**\n * project (see {@link setActiveProjectSlug}) selects the deployment via its\n * `convex.deployment` (or its production environment's). At the launcher (no\n * active project) the first configured project is the default backend. Absent\n * config — or a project without a wired deployment — falls back to\n * `VITE_CONVEX_URL` (`fallbackUrl`) for local single-deployment development.\n */\nexport function resolveDeploymentUrl(fallbackUrl: string): string {\n return projectDeploymentUrl(activeConfigProject(), fallbackUrl)\n}\n\n/** The deployment URL for a specific project slug (per-backend auth checks). */\nexport function deploymentUrlForSlug(slug: string, fallbackUrl: string): string {\n return projectDeploymentUrl(configProjectFor(slug), fallbackUrl)\n}\n\n/** A project's wired deployment: per-environment ref → project ref → env fallback. */\nfunction projectDeploymentUrl(\n project: ResolvedProject | undefined,\n fallbackUrl: string,\n): string {\n if (!project) return fallbackUrl\n const envDeployment = project.environments.find((e) => e.convex?.deployment)?.convex?.deployment\n return envDeployment ?? project.convex?.deployment ?? fallbackUrl\n}\n\n/**\n * The slug of the project bound to the active deployment (EC-179). One project\n * per backend, so the active config project's slug identifies the deployment's\n * one project; the Convex adapter stamps it onto deployment-scoped records.\n * Absent config falls back to `fallbackId` (used only when wired by env alone).\n */\nexport function resolveDeploymentProjectId(fallbackId: string): string {\n return activeConfigProject()?.slug ?? fallbackId\n}\n\n/**\n * The CMS collections of the active config project (AD-11, EC-183). CMS\n * collection schemas are config-owned: the studio reads the model from\n * `elytra.config.ts` (served through the persistence port — the backend has no\n * `cmsSchemas` table). `[]` when the active project declares none, or when the\n * studio is wired by env alone with no config.\n */\nexport function resolveDeploymentCollections(): CollectionDef[] {\n return activeConfigProject()?.collections ?? []\n}\n\n/**\n * The canvas preview breakpoints of the active config project (EC-250).\n * Config-sourced (config-as-code, derived from the site's Tailwind `screens`);\n * `undefined` when the project declares none — the canvas then falls back to\n * Tailwind's default screens (see `resolveCanvasViewports`), never a baked-in triple.\n */\nexport function resolveDeploymentBreakpoints(): BreakpointConfig[] | undefined {\n return activeConfigProject()?.breakpoints\n}\n\n/**\n * The custom field renderers from the studio config (EC-238): control name →\n * component. Studio-only live code (from `defineStudio`, never the project schema\n * or the delivery bundle); `undefined` when none — the document editor then uses\n * its built-in control for every field. Studio-wide (not per-project): the rail and\n * renderers customize the studio, not one backend.\n */\nexport function resolveDeploymentFieldRenderers(): Record<string, CustomFieldRenderer> | undefined {\n return studioConfig?.fieldRenderers\n}\n\n/**\n * The document panes from the studio config (EC-618): panes contributed to the document\n * editor's inspector. Studio-only live code (from `defineStudio`); `undefined` when none —\n * the editor then shows only the frame's own panes.\n */\nexport function resolveDeploymentDocumentPanes(): StudioConfig['documentPanes'] {\n return studioConfig?.documentPanes\n}\n\n/**\n * The studio-UI localization config from the studio config (EC-241): the default\n * locale + imported translation catalogs. Studio-only live code (from `defineStudio`,\n * never the project schema or the delivery bundle); `undefined` when none — the\n * studio then renders its built-in English chrome. Studio-wide, like the rail and\n * field renderers (localizes the studio, not one backend).\n */\nexport function resolveDeploymentI18n(): StudioConfig['i18n'] {\n return studioConfig?.i18n\n}\n\n/**\n * The AI platform config from the studio config (EC-331): provider, knowledge, and\n * limits. Studio-only live code (from `defineStudio`, never the delivery bundle);\n * `undefined` when no `ai` block is configured — the feature is then fully absent\n * (the AI subsystem never boots, so `@elytracms/studio/lib/ai`'s runtime — and the\n * `ai`/`@ai-sdk/*` SDK behind it — is never imported). Studio-wide, like the rail\n * and field renderers.\n */\nexport function resolveDeploymentAi(): StudioConfig['ai'] {\n return studioConfig?.ai\n}\n\n/**\n * The generated-preview lookup from the studio config (EC-439 AD-2, the seam\n * EC-440 fills): a pure `componentId + fixtureName → URL` function the blocks\n * panel threads into its tile model. Studio-only live code (from `defineStudio`,\n * never the delivery bundle); `undefined` when none — tiles then fall back from\n * a curated `manifest.previewImage` straight to the icon+name placeholder.\n * Studio-wide, like the rail and field renderers.\n */\nexport function resolveDeploymentGeneratedPreviews(): GeneratedPreviewLookup | undefined {\n return studioConfig?.previews?.generated\n}\n\n/**\n * The configurable section rail from the studio config (EC-237): an ordered list of\n * `nav.*` nodes (views / groups / separators). Studio-only live code (carries view\n * objects + icons); `undefined` when none — the studio then derives the default\n * rail from its built-in views. Invalid entries surface as visible issues at\n * resolution, never silent.\n */\nexport function resolveDeploymentSidebar(): SidebarNode[] | undefined {\n return studioConfig?.sidebar\n}\n\n/**\n * The canvas page-surface theme from the studio config (EC-389): body classes +\n * raw CSS for the preview iframe, so the canvas wears the SITE's chrome. Studio-only\n * (from `defineStudio`, never the delivery bundle); `undefined` when none — the\n * canvas keeps its built-in defaults (`bg-background text-foreground font-sans`\n * body). Studio-wide, like the rail and field renderers.\n */\nexport function resolveDeploymentCanvasTheme(): StudioConfig['canvas'] {\n return studioConfig?.canvas\n}\n\n/**\n * The reusable-elements declaration from the studio config (EC-467): WHICH\n * component is the project's reuse embed. Studio-only (from `defineStudio`, never\n * the delivery bundle); `undefined` when none — the studio then INFERS the embed\n * from the manifests + collections exactly as it always has (absence means infer,\n * not off). Studio-wide, like the rail and field renderers.\n */\nexport function resolveDeploymentReuse(): StudioConfig['reuse'] {\n return studioConfig?.reuse\n}\n\n/**\n * The resolved URL routes of the active config project (EC-207, \"routes as\n * code\"). Routes are config-sourced: the studio reads the materialized\n * `RouteRecord[]` from the active `elytra.config.ts` project — never from the\n * backend (the `routes` system collection is retired). The Routes surface and\n * the per-page URL preview both read through here. `[]` when the active project\n * declares none, or when the studio is wired by env alone with no config.\n */\nexport function resolveDeploymentRoutes(): RouteRecord[] {\n return activeConfigProject()?.routes ?? []\n}\n\n/**\n * The active project's PAGE collections (EC-590; EC-565 Round 3, 3g): the collections at\n * least one route addresses, in declaration order. \"A page collection\" is derived from the\n * routes — the engine dictates no collection named `page`.\n */\nexport function resolveDeploymentPageCollections(): string[] {\n return pageCollectionsOf(resolveDeploymentRoutes())\n}\n\n/** The collections a route set addresses, deduplicated in declaration order (pure). */\nexport function pageCollectionsOf(routes: readonly RouteRecord[]): string[] {\n return [...new Set(routes.flatMap((route) => (route.document ? [route.document.collection] : [])))]\n}\n\n// Redirects are editorial DATA now (EC-303), managed in the studio Redirects rail\n// and served dynamically — no longer resolved from config. `resolveDeploymentRedirects`\n// is retired; static/infra redirects are the dev's job via native next.config/vercel.\n\n/**\n * The active config project's `LocaleConfig` (EC-357 finding #3). Locales are\n * structure and structure is code: when `elytra.config.ts` declares them, they\n * drive the studio's ambient locale switcher in EVERY mode — the CMS surface must\n * not fall back to the sample `en/de` fixture (playground, or a connected first\n * run with nothing persisted) when the config says otherwise. `undefined` when no\n * config project is active (env-only wiring keeps the existing kit/sample\n * fallbacks).\n *\n * EC-519: this used to REASSEMBLE the config's flat `locales` + `defaultLocale`\n * pair into a `LocaleConfig`, and had grown its own repair rule — an absent or\n * out-of-set default fell back to `locales[0]`. That is a fourth place a locale\n * could be invented, in the surface furthest from the author. The resolver now\n * hands over the same `LocaleConfig` the delivery host takes, so there is nothing\n * to reassemble and nothing to repair: a default outside the set is a config\n * ERROR (`default-locale-not-in-locales`) and the project never resolves.\n */\nexport function resolveDeploymentLocaleConfig(): LocaleConfig | undefined {\n return activeConfigProject()?.locales\n}\n\n/**\n * The active project's components reference (EC-194, AD-11): where the consuming\n * repo's OWN real components live (`{ package }` or `{ dir }`). When set, the\n * studio registry composes with the repo's components instead of the built-in\n * demo fixtures — so the page builder edits the repo's actual component library.\n * `undefined` → the project ships no code components; the studio falls back to the\n * golden-path fixtures.\n */\nexport function resolveDeploymentComponents() {\n return activeConfigProject()?.components\n}\n\n/**\n * The active project's public site URL (EC-215): the live site's base URL the\n * studio links to (a document's live URL + open-live button, EC-216). Resolved\n * in lockstep with {@link resolveDeploymentUrl} — the environment bound to the\n * active deployment (the first with a wired deployment) provides the URL, falling\n * back to the project-level `frontendUrl`. No active-environment selector exists\n * yet, so the bound environment is the active one. `undefined` when the active\n * project (or env-only wiring) configures no frontend URL.\n *\n * EC-583: the rule is core's `projectFrontendUrl` — the delivery host advertises the\n * same answer. In a DEV studio the URL the CLI handed over wins (Q15, see\n * {@link studioDevFrontendUrl}).\n */\nexport function resolveDeploymentFrontendUrl(): string | undefined {\n const project = activeConfigProject()\n if (!project) return undefined\n const devFrontendUrl = studioDevFrontendUrl()\n return projectFrontendUrl(project, devFrontendUrl !== undefined ? { devFrontendUrl } : {})\n}\n\nlet warnedInvalidDevFrontendUrl = false\n\n/**\n * Q15 RATIFIED (EC-565 Round 5, 5b; EC-583): the dev frontend URL comes from the process\n * that knows the port. `elytra dev` starts the frontend on `3000 + offset` and hands the\n * studio dev server `ELYTRA_DEV_FRONTEND_URL`; the studio reads it through the mount env\n * ({@link studioEnv}) — and **only in a dev runtime**, so a deployed studio never links to\n * a localhost the launcher once named. `elytra.config.ts` keeps reading no env (EC-532).\n *\n * A value that is not an http(s) URL is not used, and says so once in the console\n * rather than silently linking nowhere.\n */\nexport function studioDevFrontendUrl(): string | undefined {\n if (!isDevRuntime()) return undefined\n const raw = studioEnv()[DEV_FRONTEND_URL_ENV]?.trim()\n if (raw === undefined || raw === '') return undefined\n if (/^https?:\\/\\//.test(raw) && URL.canParse(raw)) return raw\n if (!warnedInvalidDevFrontendUrl) {\n warnedInvalidDevFrontendUrl = true\n console.error(\n `[elytra] ${DEV_FRONTEND_URL_ENV} is not an http(s) URL, so live links use the config's frontendUrl instead.`,\n )\n }\n return undefined\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/workspace/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEV_FRONTEND_URL_ENV } from '@elytracms/core/export-sync/deployment-ready'\nimport { projectFrontendUrl, RESERVED_STUDIO_ROOT_SEGMENTS } from '@elytracms/core/studio-core'\nimport type {\n BreakpointConfig,\n ElytraConfigInput,\n ResolvedProject,\n} from '@elytracms/core/studio-core'\nimport type { CollectionDef, LocaleConfig, RouteRecord } from '@elytracms/core/cms-core'\nimport type { SidebarNode } from '../../components/app-views/module'\nimport type { CustomFieldRenderer, StudioConfig } from '../../components/app-views/studio-config'\nimport type { GeneratedPreviewLookup } from '../../components/builder/blocks-model'\nimport { isDevRuntime, provideStudioEnv, studioEnv } from '../persistence/backend-mode'\nimport { resolveWorkspaceState } from './workspace-source'\nimport type { WorkspaceState } from './workspace-source'\n\nexport {\n PROJECT_CONFIG_FILE,\n PROJECT_CONFIG_ISSUE_KEYS,\n PROJECT_CONFIG_POINT,\n projectConfigIssues,\n} from './config-issues'\nexport { resolveWorkspaceState } from './workspace-source'\nexport type { WorkspaceSources, WorkspaceState } from './workspace-source'\nexport {\n isSingleProjectWorkspace,\n launcherAutoForwardTarget,\n persistedProjects,\n selectSwitcherProjects,\n soleWorkspaceProject,\n unprovisionedProjects,\n} from './switcher-source'\nexport type { SwitcherEntry, SwitcherSourceInput } from './switcher-source'\n\nlet cached: WorkspaceState | null = null\nlet studioConfig: StudioConfig | null = null\n\n/**\n * Mount the studio against a project config (EC-223 mount contract) and an\n * optional studio-only customization config (EC-236). The consumer's entry point\n * does `import config from './elytra.config'` + `import studio from\n * './studio.config'` and calls this — an explicit init, NOT an `import.meta.glob`\n * autoload that reaches into the consumer's filesystem (Oli, 2026-06-19: \"there's\n * always an entry point that can trigger a function call\").\n *\n * Two configs, deliberately: `config` (`elytra.config.ts`) is the project\n * definition, shared with the `frontend/` delivery bundle and projected to data;\n * `studio` (`studio.config.tsx` via `defineStudio`) is live builder UI code — the\n * section rail, custom field renderers, branding — that must NEVER reach delivery.\n * Keeping them separate is the customization boundary principle in the mount\n * signature. Seeds the workspace state + the studio-config holder every later\n * query (`getActiveProjectSlug`, `resolveDeployment*`) reads.\n *\n * EC-357 — `options.env`: the consumer's `import.meta.env`, captured HERE because\n * the entry module is code Vite always transforms live. The engine's own\n * `import.meta.env` reads are unreliable inside dep-optimized package code (Vite ≥8\n * omits custom `VITE_*` values there; Vite 7 froze them at optimize time), which\n * silently broke backend URL / mode / dev-login resolution in real consumers.\n * Always pass it: `createStudio(config, studioConfig, { env: import.meta.env })`.\n */\nexport interface StudioMountOptions {\n /** The consumer entry's `import.meta.env` (see `provideStudioEnv` for the contract). */\n env?: Record<string, unknown>\n}\n\nexport function createStudio(\n config: ElytraConfigInput,\n studio?: StudioConfig,\n options?: StudioMountOptions,\n): void {\n provideStudioEnv(options?.env)\n cached = resolveWorkspaceState({\n fileModule: { default: config },\n fileName: 'elytra.config.ts',\n })\n studioConfig = studio ?? null\n}\n\n/** Test seam: set/clear the studio customization config. Never used by app code. */\nexport function setStudioConfigForTests(next: StudioConfig | null): void {\n studioConfig = next\n}\n\n/**\n * The active project slug (EC-179, vision AD-8). One project per backend, so the\n * active project selects which Convex deployment the studio talks to. The root\n * layout sets this from the URL (`/<slug>`) — or, on the sign-in screen, from\n * the `redirect` target's slug — before the auth/persistence adapters are\n * selected, so the *initial* sign-in authenticates against the project the user\n * is heading for (Sanity-style: authenticate to one workspace, then the full\n * config-based switcher is available). `null` at the launcher → the default\n * (first) configured project's deployment.\n */\nlet activeProjectSlug: string | null = null\n\n/** Set the active project slug; returns whether the resolved deployment changed. */\nexport function setActiveProjectSlug(slug: string | null): boolean {\n const before = resolveDeploymentUrl('')\n activeProjectSlug = slug && slug.trim() !== '' ? slug : null\n return resolveDeploymentUrl('') !== before\n}\n\nexport function getActiveProjectSlug(): string | null {\n return activeProjectSlug\n}\n\n/** Test seam: reset the active project slug. Never used by app code. */\nexport function setActiveProjectSlugForTests(slug: string | null): void {\n activeProjectSlug = slug\n}\n\n/**\n * Reserved first-path segments that are NOT project slugs: the studio's own\n * root routes (EC-220). Project surfaces now live at `/<slug>/...` (no `/p/`\n * prefix), so the studio's root namespace is shared between project slugs and\n * these fixed routes — they must be excluded from slug extraction. The launcher\n * (`/`) is the empty segment, excluded by the regex requiring one path char.\n *\n * EC-583 (RESIDUE-8): declared ONCE, in core — the resolver refuses a project slug in\n * this set, so a config can no longer name a project this reader would skip.\n */\nconst RESERVED_ROOT_SEGMENTS: ReadonlySet<string> = new Set(RESERVED_STUDIO_ROOT_SEGMENTS)\n\n/** Extract a project slug from a studio path (`/<slug>/...`), else `null`. */\nexport function projectSlugFromPath(pathname: string): string | null {\n const match = /^\\/([^/?#]+)/.exec(pathname)\n if (!match) return null\n const segment = decodeURIComponent(match[1]!)\n return RESERVED_ROOT_SEGMENTS.has(segment) ? null : segment\n}\n\n/**\n * The project whose backend the studio should bind for a given location\n * (EC-179). A project path (`/<slug>`) binds that project; the sign-in screen\n * binds the project from its `redirect` target so the *initial* sign-in\n * authenticates against the backend the user is heading for (Sanity-style).\n * Anywhere else (the launcher) → `null`, i.e. the default/first project.\n */\nexport function activeSlugForLocation(pathname: string, redirect?: string): string | null {\n const direct = projectSlugFromPath(pathname)\n if (direct) return direct\n if (pathname === '/sign-in' && redirect) return projectSlugFromPath(redirect)\n return null\n}\n\n/**\n * THE reader of \"the config project with this slug\" (EC-595 A2 → EC-582: one reader —\n * the operations runtime resolves an OPERATION's project through it, whether or not it\n * is the active one; the deployment lookup and the active project read it too).\n * `undefined` for a slug no config project declares (env-only wiring, fixtures).\n */\nexport function configProjectFor(slug: string): ResolvedProject | undefined {\n const state = getWorkspaceState()\n return state.kind === 'config' ? state.projects.find((p) => p.slug === slug) : undefined\n}\n\n/** The active config project (by slug), or the first configured project. */\nfunction activeConfigProject() {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return undefined\n return (activeProjectSlug ? configProjectFor(activeProjectSlug) : undefined) ?? state.projects[0]\n}\n\n/**\n * The studio's resolved workspace state. Deterministic per boot: the config is\n * static input (bundled file or deployment env), so it is resolved once and\n * cached — every consumer (gate, switcher, launcher) sees the same state.\n */\nexport function getWorkspaceState(): WorkspaceState {\n // Seeded by `createStudio(config)` at the entry point. Until then — or when the\n // consumer wires no config — resolve to the empty `none` state (the single-project\n // golden path), rather than reaching for the filesystem.\n if (!cached) cached = resolveWorkspaceState({})\n return cached\n}\n\n/** Test seam: clear/replace the cached state. Never used by app code. */\nexport function setWorkspaceStateForTests(next: WorkspaceState | null): void {\n cached = next\n}\n\n/**\n * Resolve the Convex deployment URL the studio should connect to (vision AD-8:\n * the backend is wired in config). One project per backend, so the **active**\n * project (see {@link setActiveProjectSlug}) selects the deployment via its\n * `convex.deployment` (or its production environment's). At the launcher (no\n * active project) the first configured project is the default backend. Absent\n * config — or a project without a wired deployment — falls back to\n * `VITE_CONVEX_URL` (`fallbackUrl`) for local single-deployment development.\n */\nexport function resolveDeploymentUrl(fallbackUrl: string): string {\n return projectDeploymentUrl(activeConfigProject(), fallbackUrl)\n}\n\n/** The deployment URL for a specific project slug (per-backend auth checks). */\nexport function deploymentUrlForSlug(slug: string, fallbackUrl: string): string {\n return projectDeploymentUrl(configProjectFor(slug), fallbackUrl)\n}\n\n/** A project's wired deployment: per-environment ref → project ref → env fallback. */\nfunction projectDeploymentUrl(\n project: ResolvedProject | undefined,\n fallbackUrl: string,\n): string {\n if (!project) return fallbackUrl\n const envDeployment = project.environments.find((e) => e.convex?.deployment)?.convex?.deployment\n return envDeployment ?? project.convex?.deployment ?? fallbackUrl\n}\n\n/**\n * The slug of the project bound to the active deployment (EC-179). One project\n * per backend, so the active config project's slug identifies the deployment's\n * one project; the Convex adapter stamps it onto deployment-scoped records.\n * Absent config falls back to `fallbackId` (used only when wired by env alone).\n */\nexport function resolveDeploymentProjectId(fallbackId: string): string {\n return activeConfigProject()?.slug ?? fallbackId\n}\n\n/**\n * The CMS collections of the active config project (AD-11, EC-183). CMS\n * collection schemas are config-owned: the studio reads the model from\n * `elytra.config.ts` (served through the persistence port — the backend has no\n * `cmsSchemas` table). `[]` when the active project declares none, or when the\n * studio is wired by env alone with no config.\n */\nexport function resolveDeploymentCollections(): CollectionDef[] {\n return activeConfigProject()?.collections ?? []\n}\n\n/**\n * The canvas preview breakpoints of the active config project (EC-250).\n * Config-sourced (config-as-code, derived from the site's Tailwind `screens`);\n * `undefined` when the project declares none — the canvas then falls back to\n * Tailwind's default screens (see `resolveCanvasViewports`), never a baked-in triple.\n */\nexport function resolveDeploymentBreakpoints(): BreakpointConfig[] | undefined {\n return activeConfigProject()?.breakpoints\n}\n\n/**\n * The custom field renderers from the studio config (EC-238): control name →\n * component. Studio-only live code (from `defineStudio`, never the project schema\n * or the delivery bundle); `undefined` when none — the document editor then uses\n * its built-in control for every field. Studio-wide (not per-project): the rail and\n * renderers customize the studio, not one backend.\n */\nexport function resolveDeploymentFieldRenderers(): Record<string, CustomFieldRenderer> | undefined {\n return studioConfig?.fieldRenderers\n}\n\n/**\n * The document panes from the studio config (EC-618): panes contributed to the document\n * editor's inspector. Studio-only live code (from `defineStudio`); `undefined` when none —\n * the editor then shows only the frame's own panes.\n */\nexport function resolveDeploymentDocumentPanes(): StudioConfig['documentPanes'] {\n return studioConfig?.documentPanes\n}\n\n/**\n * The studio-UI localization config from the studio config (EC-241): the default\n * locale + imported translation catalogs. Studio-only live code (from `defineStudio`,\n * never the project schema or the delivery bundle); `undefined` when none — the\n * studio then renders its built-in English chrome. Studio-wide, like the rail and\n * field renderers (localizes the studio, not one backend).\n */\nexport function resolveDeploymentI18n(): StudioConfig['i18n'] {\n return studioConfig?.i18n\n}\n\n/**\n * The AI platform config from the studio config (EC-331): provider, knowledge, and\n * limits. Studio-only live code (from `defineStudio`, never the delivery bundle);\n * `undefined` when no `ai` block is configured — the feature is then fully absent\n * (the AI subsystem never boots, so `@elytracms/studio/lib/ai`'s runtime — and the\n * `ai`/`@ai-sdk/*` SDK behind it — is never imported). Studio-wide, like the rail\n * and field renderers.\n */\nexport function resolveDeploymentAi(): StudioConfig['ai'] {\n return studioConfig?.ai\n}\n\n/**\n * The generated-preview lookup from the studio config (EC-439 AD-2, the seam\n * EC-440 fills): a pure `componentId + fixtureName → URL` function the blocks\n * panel threads into its tile model. Studio-only live code (from `defineStudio`,\n * never the delivery bundle); `undefined` when none — tiles then fall back from\n * a curated `manifest.previewImage` straight to the icon+name placeholder.\n * Studio-wide, like the rail and field renderers.\n */\nexport function resolveDeploymentGeneratedPreviews(): GeneratedPreviewLookup | undefined {\n return studioConfig?.previews?.generated\n}\n\n/**\n * The configurable section rail from the studio config (EC-237): an ordered list of\n * `nav.*` nodes (views / groups / separators). Studio-only live code (carries view\n * objects + icons); `undefined` when none — the studio then derives the default\n * rail from its built-in views. Invalid entries surface as visible issues at\n * resolution, never silent.\n */\nexport function resolveDeploymentSidebar(): SidebarNode[] | undefined {\n return studioConfig?.sidebar\n}\n\n/**\n * The canvas page-surface theme from the studio config (EC-389): body classes +\n * raw CSS for the preview iframe, so the canvas wears the SITE's chrome. Studio-only\n * (from `defineStudio`, never the delivery bundle); `undefined` when none — the\n * canvas keeps its built-in defaults (`bg-background text-foreground font-sans`\n * body). Studio-wide, like the rail and field renderers.\n */\nexport function resolveDeploymentCanvasTheme(): StudioConfig['canvas'] {\n return studioConfig?.canvas\n}\n\n/**\n * The reusable-elements declaration from the studio config (EC-467): WHICH\n * component is the project's reuse embed. Studio-only (from `defineStudio`, never\n * the delivery bundle); `undefined` when none — the project then has no reusable\n * elements in the studio (EC-728: declared, never inferred — absence means off).\n * Studio-wide, like the rail and field renderers.\n */\nexport function resolveDeploymentReuse(): StudioConfig['reuse'] {\n return studioConfig?.reuse\n}\n\n/**\n * The resolved URL routes of the active config project (EC-207, \"routes as\n * code\"). Routes are config-sourced: the studio reads the materialized\n * `RouteRecord[]` from the active `elytra.config.ts` project — never from the\n * backend (the `routes` system collection is retired). The Routes surface and\n * the per-page URL preview both read through here. `[]` when the active project\n * declares none, or when the studio is wired by env alone with no config.\n */\nexport function resolveDeploymentRoutes(): RouteRecord[] {\n return activeConfigProject()?.routes ?? []\n}\n\n/**\n * The active project's PAGE collections (EC-590; EC-565 Round 3, 3g): the collections at\n * least one route addresses, in declaration order. \"A page collection\" is derived from the\n * routes — the engine dictates no collection named `page`.\n */\nexport function resolveDeploymentPageCollections(): string[] {\n return pageCollectionsOf(resolveDeploymentRoutes())\n}\n\n/** The collections a route set addresses, deduplicated in declaration order (pure). */\nexport function pageCollectionsOf(routes: readonly RouteRecord[]): string[] {\n return [...new Set(routes.flatMap((route) => (route.document ? [route.document.collection] : [])))]\n}\n\n// Redirects are editorial DATA now (EC-303), managed in the studio Redirects rail\n// and served dynamically — no longer resolved from config. `resolveDeploymentRedirects`\n// is retired; static/infra redirects are the dev's job via native next.config/vercel.\n\n/**\n * The active config project's `LocaleConfig` (EC-357 finding #3). Locales are\n * structure and structure is code: when `elytra.config.ts` declares them, they\n * drive the studio's ambient locale switcher in EVERY mode — the CMS surface must\n * not fall back to the sample `en/de` fixture (playground, or a connected first\n * run with nothing persisted) when the config says otherwise. `undefined` when no\n * config project is active (env-only wiring keeps the existing kit/sample\n * fallbacks).\n *\n * EC-519: this used to REASSEMBLE the config's flat `locales` + `defaultLocale`\n * pair into a `LocaleConfig`, and had grown its own repair rule — an absent or\n * out-of-set default fell back to `locales[0]`. That is a fourth place a locale\n * could be invented, in the surface furthest from the author. The resolver now\n * hands over the same `LocaleConfig` the delivery host takes, so there is nothing\n * to reassemble and nothing to repair: a default outside the set is a config\n * ERROR (`default-locale-not-in-locales`) and the project never resolves.\n */\nexport function resolveDeploymentLocaleConfig(): LocaleConfig | undefined {\n return activeConfigProject()?.locales\n}\n\n/**\n * The active project's components reference (EC-194, AD-11): where the consuming\n * repo's OWN real components live (`{ package }` or `{ dir }`). When set, the\n * studio registry composes with the repo's components instead of the built-in\n * demo fixtures — so the page builder edits the repo's actual component library.\n * `undefined` → the project ships no code components; the studio falls back to the\n * golden-path fixtures.\n */\nexport function resolveDeploymentComponents() {\n return activeConfigProject()?.components\n}\n\n/**\n * The active project's public site URL (EC-215): the live site's base URL the\n * studio links to (a document's live URL + open-live button, EC-216). Resolved\n * in lockstep with {@link resolveDeploymentUrl} — the environment bound to the\n * active deployment (the first with a wired deployment) provides the URL, falling\n * back to the project-level `frontendUrl`. No active-environment selector exists\n * yet, so the bound environment is the active one. `undefined` when the active\n * project (or env-only wiring) configures no frontend URL.\n *\n * EC-583: the rule is core's `projectFrontendUrl` — the delivery host advertises the\n * same answer. In a DEV studio the URL the CLI handed over wins (Q15, see\n * {@link studioDevFrontendUrl}).\n */\nexport function resolveDeploymentFrontendUrl(): string | undefined {\n const project = activeConfigProject()\n if (!project) return undefined\n const devFrontendUrl = studioDevFrontendUrl()\n return projectFrontendUrl(project, devFrontendUrl !== undefined ? { devFrontendUrl } : {})\n}\n\nlet warnedInvalidDevFrontendUrl = false\n\n/**\n * Q15 RATIFIED (EC-565 Round 5, 5b; EC-583): the dev frontend URL comes from the process\n * that knows the port. `elytra dev` starts the frontend on `3000 + offset` and hands the\n * studio dev server `ELYTRA_DEV_FRONTEND_URL`; the studio reads it through the mount env\n * ({@link studioEnv}) — and **only in a dev runtime**, so a deployed studio never links to\n * a localhost the launcher once named. `elytra.config.ts` keeps reading no env (EC-532).\n *\n * A value that is not an http(s) URL is not used, and says so once in the console\n * rather than silently linking nowhere.\n */\nexport function studioDevFrontendUrl(): string | undefined {\n if (!isDevRuntime()) return undefined\n const raw = studioEnv()[DEV_FRONTEND_URL_ENV]?.trim()\n if (raw === undefined || raw === '') return undefined\n if (/^https?:\\/\\//.test(raw) && URL.canParse(raw)) return raw\n if (!warnedInvalidDevFrontendUrl) {\n warnedInvalidDevFrontendUrl = true\n console.error(\n `[elytra] ${DEV_FRONTEND_URL_ENV} is not an http(s) URL, so live links use the config's frontendUrl instead.`,\n )\n }\n return undefined\n}\n"]}
@@ -30,7 +30,7 @@
30
30
  * launcher (`/`) is still mounted at `/sign-in?redirect=/aurora` until the
31
31
  * sign-in module has loaded. Left to fire, its auto-forward and the auth
32
32
  * gate's sign-in bounce alternate, each commit re-arming the other — the
33
- * signed-out boot hang on `@tanstack/react-router` ≥ 1.170.41 (NK-050 F3).
33
+ * signed-out boot hang on `@tanstack/react-router` ≥ 1.170.41 (found on a consumer).
34
34
  */
35
35
  export interface NavigateOnceIntent {
36
36
  /** Target route path, addressed as a plain string (the router-seam contract). */
@@ -1 +1 @@
1
- {"version":3,"file":"navigate-once.js","sourceRoot":"","sources":["../../src/router/navigate-once.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useLayoutEffect, useRef } from 'react'\nimport { useLocation, useMatch, useNavigate, useRouter } from '@tanstack/react-router'\n\n/**\n * Render-time redirect that fires its navigation ONCE per redirect intent per\n * committed location (EC-441). The studio's declarative redirects (launcher\n * auto-forward, project index → landing view, auth-gate bounces, deep-link\n * normalizations) must use THIS instead of TanStack Router's `<Navigate>`.\n *\n * Why: `<Navigate>` re-fires `navigate(props)` whenever its props OBJECT\n * IDENTITY changes — and JSX builds a fresh props object on every parent\n * render. While the redirect's target transition is still pending (slow route\n * modules on a cold dev boot, slow chunks in production), the redirecting\n * component stays mounted and re-renders on every router-store tick; each\n * re-render re-navigates, each `navigate()` re-enters `commitLocation`/`load`\n * and schedules another transition state update, and the cycle compounds until\n * React throws \"Maximum update depth exceeded\" inside `commitLocation` — an\n * unhandled rejection of the router's load promise (the EC-439 finding: 104\n * launcher re-navigations on one cold playground boot).\n *\n * The decision core (`nextRedirectAttempt`) is pure and value-keyed:\n *\n * - already at the target → never fire (idempotence — covers the EC-179 keyed\n * `AuthProvider` remount, which recreates the redirecting component with\n * fresh refs while the router has already committed the target);\n * - one fire per intent per committed location — re-renders and store ticks\n * while the target transition is pending are inert;\n * - a NEW committed location re-arms the attempt, so navigating back to a URL\n * that redirects (e.g. the bare project index) redirects again;\n * - a redirect rendered by a route the committed location no longer matches is\n * inert (EC-611). While a transition is pending the router keeps rendering\n * the PREVIOUS match set, so a route component outlives its own URL: the\n * launcher (`/`) is still mounted at `/sign-in?redirect=/aurora` until the\n * sign-in module has loaded. Left to fire, its auto-forward and the auth\n * gate's sign-in bounce alternate, each commit re-arming the other — the\n * signed-out boot hang on `@tanstack/react-router` ≥ 1.170.41 (NK-050 F3).\n */\nexport interface NavigateOnceIntent {\n /** Target route path, addressed as a plain string (the router-seam contract). */\n to: string\n /** Route params (`{ projectId, _splat }` for the studio splat). */\n params?: Record<string, string>\n /** Search params for the target location. */\n search?: Record<string, unknown>\n /** Replace the current history entry (a redirect must not create one). */\n replace?: boolean\n}\n\n/**\n * Pure value identity of a redirect intent. Fresh-but-equal `params`/`search`\n * objects (the upstream failure mode: new object literal every render) map to\n * ONE key; top-level entries are order-normalized so key insertion order never\n * splits an intent. Values are JSON-serialized — the studio's redirect targets\n * are strings and flat search records, both deterministic under JSON.\n */\nexport function navigateIntentKey(intent: NavigateOnceIntent): string {\n const sorted = (record: Record<string, unknown> | undefined) =>\n record\n ? Object.entries(record).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n : null\n return JSON.stringify([\n intent.to,\n sorted(intent.params),\n sorted(intent.search),\n intent.replace === true,\n ])\n}\n\n/**\n * Href equality for \"is this redirect already committed?\", trailing-slash\n * trimmed on the path segment (matching the router's own same-URL comparison)\n * while search and hash stay significant.\n */\nexport function isSameRedirectTarget(targetHref: string, committedHref: string): boolean {\n const trim = (href: string) => {\n const [pathAndSearch = '', hash = ''] = href.split('#', 2)\n const [path = '', search = ''] = pathAndSearch.split('?', 2)\n const trimmedPath = path.length > 1 ? path.replace(/\\/+$/, '') : path\n return `${trimmedPath}${search ? `?${search}` : ''}${hash ? `#${hash}` : ''}`\n }\n return trim(targetHref) === trim(committedHref)\n}\n\n/**\n * Attempt bookkeeping for one mounted redirect: which intent keys have already\n * fired at the CURRENT committed location. A new committed href resets the set\n * (re-arm); the state is immutable so the caller can hold it in a ref.\n */\nexport interface RedirectAttemptState {\n committedHref: string\n firedKeys: ReadonlySet<string>\n}\n\n/**\n * The pure fire decision (EC-441's loop breaker, node-tested):\n *\n * 1. the rendering route is not matched by the committed location → no fire\n * (a stale render during a pending transition, EC-611);\n * 2. target already committed → no fire;\n * 3. this intent already fired at this committed location → no fire (the\n * per-render re-fire cycle, and any intent ping-pong at one location);\n * 4. otherwise → fire, and record the intent for this location.\n */\nexport function nextRedirectAttempt(\n state: RedirectAttemptState | null,\n input: {\n intentKey: string\n targetHref: string\n committedHref: string\n /**\n * Whether the route this redirect renders in is part of the committed\n * location's match set. Absent means \"not a route-scoped render\" (treated\n * as matched) so the decision stays usable from the root layout.\n */\n routeMatched?: boolean\n },\n): { fire: boolean; state: RedirectAttemptState } {\n const base: RedirectAttemptState =\n state && state.committedHref === input.committedHref\n ? state\n : { committedHref: input.committedHref, firedKeys: new Set<string>() }\n if (input.routeMatched === false) return { fire: false, state: base }\n if (isSameRedirectTarget(input.targetHref, input.committedHref)) return { fire: false, state: base }\n if (base.firedKeys.has(input.intentKey)) return { fire: false, state: base }\n return {\n fire: true,\n state: {\n committedHref: base.committedHref,\n firedKeys: new Set(base.firedKeys).add(input.intentKey),\n },\n }\n}\n\n// Layout-effect timing on the client (start the redirect before paint, like\n// upstream `<Navigate>`); plain effect on the server, where layout effects warn\n// and redirects never run (the studio's redirect surfaces render null on SSR).\nconst useIsomorphicLayoutEffect = typeof document !== 'undefined' ? useLayoutEffect : useEffect\n\nexport function NavigateOnce({ to, params, search, replace }: NavigateOnceIntent) {\n const router = useRouter()\n const navigate = useNavigate()\n // The COMMITTED location (not transient transition state): effect deps stay\n // stable across the pending-transition re-renders that used to re-fire\n // `<Navigate>`, and a commit re-runs the decision exactly once.\n const committedHref = useLocation({ select: (location) => location.href })\n // The route this redirect renders in (EC-611). `strict: false` resolves the\n // NEAREST match, so a redirect in the root layout (the auth gate) reads the\n // root route, which every location matches, and a redirect in a route\n // component reads that route — which the committed location stops matching\n // the moment a transition away from it starts.\n const routeId = useMatch({ strict: false, select: (match) => match.routeId }) as\n | string\n | undefined\n const key = navigateIntentKey({ to, params, search, replace })\n // Latest-value ref: the effect is keyed on the intent's VALUE (`key`), so it\n // reads the current props through a ref instead of listing unstable object\n // identities as dependencies.\n const intentRef = useRef<NavigateOnceIntent>({ to, params, search, replace })\n intentRef.current = { to, params, search, replace }\n const attemptRef = useRef<RedirectAttemptState | null>(null)\n\n useIsomorphicLayoutEffect(() => {\n const intent = intentRef.current\n const target = router.buildLocation({\n to: intent.to,\n params: intent.params,\n search: intent.search,\n })\n // Match the COMMITTED location against the route tree (no load, no state\n // change): is the route rendering this redirect still on the URL? During\n // a pending transition the previous match set keeps rendering, so this is\n // the one fact that tells a live redirect from a stale one.\n const routeMatched =\n routeId === undefined ||\n router.matchRoutes(router.state.location).some((match) => match.routeId === routeId)\n const decision = nextRedirectAttempt(attemptRef.current, {\n intentKey: key,\n targetHref: target.href,\n committedHref,\n routeMatched,\n })\n attemptRef.current = decision.state\n if (!decision.fire) return\n // Fire-and-forget, exactly like upstream `<Navigate>`: with the re-fire\n // cycle gone the transition settles normally, so nothing here swallows a\n // real navigation failure.\n void navigate({\n to: intent.to,\n params: intent.params,\n search: intent.search,\n replace: intent.replace,\n })\n }, [key, committedHref, navigate, router, routeId])\n\n return null\n}\n"]}
1
+ {"version":3,"file":"navigate-once.js","sourceRoot":"","sources":["../../src/router/navigate-once.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useLayoutEffect, useRef } from 'react'\nimport { useLocation, useMatch, useNavigate, useRouter } from '@tanstack/react-router'\n\n/**\n * Render-time redirect that fires its navigation ONCE per redirect intent per\n * committed location (EC-441). The studio's declarative redirects (launcher\n * auto-forward, project index → landing view, auth-gate bounces, deep-link\n * normalizations) must use THIS instead of TanStack Router's `<Navigate>`.\n *\n * Why: `<Navigate>` re-fires `navigate(props)` whenever its props OBJECT\n * IDENTITY changes — and JSX builds a fresh props object on every parent\n * render. While the redirect's target transition is still pending (slow route\n * modules on a cold dev boot, slow chunks in production), the redirecting\n * component stays mounted and re-renders on every router-store tick; each\n * re-render re-navigates, each `navigate()` re-enters `commitLocation`/`load`\n * and schedules another transition state update, and the cycle compounds until\n * React throws \"Maximum update depth exceeded\" inside `commitLocation` — an\n * unhandled rejection of the router's load promise (the EC-439 finding: 104\n * launcher re-navigations on one cold playground boot).\n *\n * The decision core (`nextRedirectAttempt`) is pure and value-keyed:\n *\n * - already at the target → never fire (idempotence — covers the EC-179 keyed\n * `AuthProvider` remount, which recreates the redirecting component with\n * fresh refs while the router has already committed the target);\n * - one fire per intent per committed location — re-renders and store ticks\n * while the target transition is pending are inert;\n * - a NEW committed location re-arms the attempt, so navigating back to a URL\n * that redirects (e.g. the bare project index) redirects again;\n * - a redirect rendered by a route the committed location no longer matches is\n * inert (EC-611). While a transition is pending the router keeps rendering\n * the PREVIOUS match set, so a route component outlives its own URL: the\n * launcher (`/`) is still mounted at `/sign-in?redirect=/aurora` until the\n * sign-in module has loaded. Left to fire, its auto-forward and the auth\n * gate's sign-in bounce alternate, each commit re-arming the other — the\n * signed-out boot hang on `@tanstack/react-router` ≥ 1.170.41 (found on a consumer).\n */\nexport interface NavigateOnceIntent {\n /** Target route path, addressed as a plain string (the router-seam contract). */\n to: string\n /** Route params (`{ projectId, _splat }` for the studio splat). */\n params?: Record<string, string>\n /** Search params for the target location. */\n search?: Record<string, unknown>\n /** Replace the current history entry (a redirect must not create one). */\n replace?: boolean\n}\n\n/**\n * Pure value identity of a redirect intent. Fresh-but-equal `params`/`search`\n * objects (the upstream failure mode: new object literal every render) map to\n * ONE key; top-level entries are order-normalized so key insertion order never\n * splits an intent. Values are JSON-serialized — the studio's redirect targets\n * are strings and flat search records, both deterministic under JSON.\n */\nexport function navigateIntentKey(intent: NavigateOnceIntent): string {\n const sorted = (record: Record<string, unknown> | undefined) =>\n record\n ? Object.entries(record).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n : null\n return JSON.stringify([\n intent.to,\n sorted(intent.params),\n sorted(intent.search),\n intent.replace === true,\n ])\n}\n\n/**\n * Href equality for \"is this redirect already committed?\", trailing-slash\n * trimmed on the path segment (matching the router's own same-URL comparison)\n * while search and hash stay significant.\n */\nexport function isSameRedirectTarget(targetHref: string, committedHref: string): boolean {\n const trim = (href: string) => {\n const [pathAndSearch = '', hash = ''] = href.split('#', 2)\n const [path = '', search = ''] = pathAndSearch.split('?', 2)\n const trimmedPath = path.length > 1 ? path.replace(/\\/+$/, '') : path\n return `${trimmedPath}${search ? `?${search}` : ''}${hash ? `#${hash}` : ''}`\n }\n return trim(targetHref) === trim(committedHref)\n}\n\n/**\n * Attempt bookkeeping for one mounted redirect: which intent keys have already\n * fired at the CURRENT committed location. A new committed href resets the set\n * (re-arm); the state is immutable so the caller can hold it in a ref.\n */\nexport interface RedirectAttemptState {\n committedHref: string\n firedKeys: ReadonlySet<string>\n}\n\n/**\n * The pure fire decision (EC-441's loop breaker, node-tested):\n *\n * 1. the rendering route is not matched by the committed location → no fire\n * (a stale render during a pending transition, EC-611);\n * 2. target already committed → no fire;\n * 3. this intent already fired at this committed location → no fire (the\n * per-render re-fire cycle, and any intent ping-pong at one location);\n * 4. otherwise → fire, and record the intent for this location.\n */\nexport function nextRedirectAttempt(\n state: RedirectAttemptState | null,\n input: {\n intentKey: string\n targetHref: string\n committedHref: string\n /**\n * Whether the route this redirect renders in is part of the committed\n * location's match set. Absent means \"not a route-scoped render\" (treated\n * as matched) so the decision stays usable from the root layout.\n */\n routeMatched?: boolean\n },\n): { fire: boolean; state: RedirectAttemptState } {\n const base: RedirectAttemptState =\n state && state.committedHref === input.committedHref\n ? state\n : { committedHref: input.committedHref, firedKeys: new Set<string>() }\n if (input.routeMatched === false) return { fire: false, state: base }\n if (isSameRedirectTarget(input.targetHref, input.committedHref)) return { fire: false, state: base }\n if (base.firedKeys.has(input.intentKey)) return { fire: false, state: base }\n return {\n fire: true,\n state: {\n committedHref: base.committedHref,\n firedKeys: new Set(base.firedKeys).add(input.intentKey),\n },\n }\n}\n\n// Layout-effect timing on the client (start the redirect before paint, like\n// upstream `<Navigate>`); plain effect on the server, where layout effects warn\n// and redirects never run (the studio's redirect surfaces render null on SSR).\nconst useIsomorphicLayoutEffect = typeof document !== 'undefined' ? useLayoutEffect : useEffect\n\nexport function NavigateOnce({ to, params, search, replace }: NavigateOnceIntent) {\n const router = useRouter()\n const navigate = useNavigate()\n // The COMMITTED location (not transient transition state): effect deps stay\n // stable across the pending-transition re-renders that used to re-fire\n // `<Navigate>`, and a commit re-runs the decision exactly once.\n const committedHref = useLocation({ select: (location) => location.href })\n // The route this redirect renders in (EC-611). `strict: false` resolves the\n // NEAREST match, so a redirect in the root layout (the auth gate) reads the\n // root route, which every location matches, and a redirect in a route\n // component reads that route — which the committed location stops matching\n // the moment a transition away from it starts.\n const routeId = useMatch({ strict: false, select: (match) => match.routeId }) as\n | string\n | undefined\n const key = navigateIntentKey({ to, params, search, replace })\n // Latest-value ref: the effect is keyed on the intent's VALUE (`key`), so it\n // reads the current props through a ref instead of listing unstable object\n // identities as dependencies.\n const intentRef = useRef<NavigateOnceIntent>({ to, params, search, replace })\n intentRef.current = { to, params, search, replace }\n const attemptRef = useRef<RedirectAttemptState | null>(null)\n\n useIsomorphicLayoutEffect(() => {\n const intent = intentRef.current\n const target = router.buildLocation({\n to: intent.to,\n params: intent.params,\n search: intent.search,\n })\n // Match the COMMITTED location against the route tree (no load, no state\n // change): is the route rendering this redirect still on the URL? During\n // a pending transition the previous match set keeps rendering, so this is\n // the one fact that tells a live redirect from a stale one.\n const routeMatched =\n routeId === undefined ||\n router.matchRoutes(router.state.location).some((match) => match.routeId === routeId)\n const decision = nextRedirectAttempt(attemptRef.current, {\n intentKey: key,\n targetHref: target.href,\n committedHref,\n routeMatched,\n })\n attemptRef.current = decision.state\n if (!decision.fire) return\n // Fire-and-forget, exactly like upstream `<Navigate>`: with the re-fire\n // cycle gone the transition settles normally, so nothing here swallows a\n // real navigation failure.\n void navigate({\n to: intent.to,\n params: intent.params,\n search: intent.search,\n replace: intent.replace,\n })\n }, [key, committedHref, navigate, router, routeId])\n\n return null\n}\n"]}
@@ -4,11 +4,12 @@ import type { ReactNode } from 'react';
4
4
  * consumer `__root.tsx` by EC-369 finding #7). It assembles the ONE canonical
5
5
  * provider/gate stack — ThemeProvider → DeploymentSetupGate → AuthProvider(keyed)
6
6
  * → AuthGate → StudioProvider → Outlet — plus the `RootDocument` shell
7
- * (html/head with the pre-hydration theme script + the chrome stylesheet).
7
+ * (html/head with the pre-hydration theme script + the chrome stylesheet), and the
8
+ * route's `errorComponent` (EC-717).
8
9
  *
9
10
  * Its whole purpose is to make the NEXT required-gate addition automatic on
10
11
  * package upgrade: when EC-258 added `DeploymentSetupGate`, every hand-copied
11
- * `__root.tsx` had to be manually re-synced, and Konditorei's silently missed
12
+ * `__root.tsx` had to be manually re-synced, and a production site's silently missed
12
13
  * it for five releases (EC-367). Owning the composition here means a consumer's
13
14
  * `__root.tsx` shrinks to a thin shell and picks up new gates for free.
14
15
  *
@@ -3,6 +3,7 @@ import { createRootRoute, HeadContent, Outlet, Scripts, useLocation, } from '@ta
3
3
  import { NavigateOnce } from './navigate-once.js';
4
4
  import { AuthProvider, useAuth } from '../components/auth-context.js';
5
5
  import { DeploymentSetupGate } from '../components/deployment-setup-gate.js';
6
+ import { StudioRouteError } from '../components/load-failure.js';
6
7
  import { SessionExpiredOverlay } from '../components/session-expired-overlay.js';
7
8
  import { StudioProvider } from '../components/studio-context.js';
8
9
  import { ThemeProvider } from '../components/theme-context.js';
@@ -20,6 +21,20 @@ export function createStudioRootRoute(options) {
20
21
  // seam — same pattern as `setActiveProjectSlug` — so the shell never has to
21
22
  // capture `document.title` and race the router's own head effect.
22
23
  setStudioBaseTitle(title);
24
+ /**
25
+ * EC-717: the document (`<html>`, head, stylesheet, scripts) is the root route's
26
+ * `shellComponent`, not part of its `component`. TanStack Router renders the shell
27
+ * AROUND the root match's error boundary (`Match.js`: `<ShellComponent>{content}</…>`),
28
+ * so the `errorComponent` below renders inside the themed document. With the document
29
+ * inside `component` — as it was — an error replaced the document itself and the notice
30
+ * came up unstyled.
31
+ */
32
+ function RootShell({ children }) {
33
+ // EC-513: keeps `<html lang>` honest — it names the language the chrome is actually
34
+ // rendered in, at every moment (see `RootComponent` for the handover itself).
35
+ const uiLocale = useUiLocale();
36
+ return _jsx(RootDocument, { htmlLang: htmlLang ?? uiLocale, children: children });
37
+ }
23
38
  function RootComponent() {
24
39
  const location = useLocation();
25
40
  // EC-513: locale resolution is client-side, so the studio boots in the server's
@@ -28,9 +43,10 @@ export function createStudioRootRoute(options) {
28
43
  // markup or React discards the tree with a mismatch error. Subscribing HERE, at the
29
44
  // one root above the whole chrome, is what makes the handover visible everywhere:
30
45
  // ~948 of the studio's localized render sites call the non-hook `translate()` and
31
- // would otherwise never re-render for it. It also keeps `<html lang>` honest —
32
- // it names the language the chrome is actually rendered in, at every moment.
33
- const uiLocale = useUiLocale();
46
+ // would otherwise never re-render for it. (The shell's own subscription would not do
47
+ // it: the shell re-rendering hands React the same `children` element, so React skips
48
+ // the subtree.)
49
+ useUiLocale();
34
50
  // EC-179: the active project selects its Convex backend. Resolve it from the
35
51
  // URL (or the sign-in redirect target) and bind it *before* the auth and
36
52
  // persistence adapters are selected, so the initial sign-in authenticates
@@ -39,7 +55,7 @@ export function createStudioRootRoute(options) {
39
55
  // re-select against the new backend and the session is restored per-backend.
40
56
  const activeSlug = activeSlugForLocation(location.pathname, location.search.redirect);
41
57
  setActiveProjectSlug(activeSlug);
42
- return (_jsx(RootDocument, { htmlLang: htmlLang ?? uiLocale, children: _jsx(ThemeProvider, { children: _jsx(DeploymentSetupGate, { children: _jsx(AuthProvider, { children: _jsx(AuthGate, { children: _jsxs(StudioProvider, { children: [providers ? providers(_jsx(Outlet, {})) : _jsx(Outlet, {}), _jsx(SessionExpiredOverlay, {})] }) }) }, activeSlug ?? '__launcher__') }) }) }));
58
+ return (_jsx(ThemeProvider, { children: _jsx(DeploymentSetupGate, { children: _jsx(AuthProvider, { children: _jsx(AuthGate, { children: _jsxs(StudioProvider, { children: [providers ? providers(_jsx(Outlet, {})) : _jsx(Outlet, {}), _jsx(SessionExpiredOverlay, {})] }) }) }, activeSlug ?? '__launcher__') }) }));
43
59
  }
44
60
  return createRootRoute({
45
61
  head: () => ({
@@ -51,7 +67,12 @@ export function createStudioRootRoute(options) {
51
67
  ],
52
68
  links: [{ rel: 'stylesheet', href: appCssUrl }, ...links],
53
69
  }),
70
+ shellComponent: RootShell,
54
71
  component: RootComponent,
72
+ // EC-717: the catch-all for any render error no surface boundary took (each lazily
73
+ // loaded surface has its own `LazySurfaceBoundary`). Without it the router's bare
74
+ // default replaced the whole studio with "Something went wrong!".
75
+ errorComponent: StudioRouteError,
55
76
  });
56
77
  }
57
78
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"root.js","sourceRoot":"","sources":["../../src/router/root.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react'\nimport {\n createRootRoute,\n HeadContent,\n Outlet,\n Scripts,\n useLocation,\n} from '@tanstack/react-router'\nimport { NavigateOnce } from './navigate-once'\nimport { AuthProvider, useAuth } from '../components/auth-context'\nimport { DeploymentSetupGate } from '../components/deployment-setup-gate'\nimport { SessionExpiredOverlay } from '../components/session-expired-overlay'\nimport { StudioProvider } from '../components/studio-context'\nimport { ThemeProvider } from '../components/theme-context'\nimport { Loader2 } from 'lucide-react'\nimport { SIGN_IN_PATH, resolveRouteGuard } from '../lib/auth'\nimport { useHydrated, useT, useUiLocale } from '../lib/i18n'\nimport { setStudioBaseTitle } from '../lib/tab-title'\nimport { activeSlugForLocation, setActiveProjectSlug } from '../lib/workspace'\nimport { THEME_INIT_SCRIPT } from '../lib/theme'\n\n/**\n * Studio root-route boot factory (EC-375; consolidated from the copy-pasted\n * consumer `__root.tsx` by EC-369 finding #7). It assembles the ONE canonical\n * provider/gate stack — ThemeProvider → DeploymentSetupGate → AuthProvider(keyed)\n * → AuthGate → StudioProvider → Outlet — plus the `RootDocument` shell\n * (html/head with the pre-hydration theme script + the chrome stylesheet).\n *\n * Its whole purpose is to make the NEXT required-gate addition automatic on\n * package upgrade: when EC-258 added `DeploymentSetupGate`, every hand-copied\n * `__root.tsx` had to be manually re-synced, and Konditorei's silently missed\n * it for five releases (EC-367). Owning the composition here means a consumer's\n * `__root.tsx` shrinks to a thin shell and picks up new gates for free.\n *\n * The consumer re-exports the returned route as `Route` (the generated route\n * tree imports `Route` from `./routes/__root`):\n *\n * ```tsx\n * import { createStudioRootRoute } from '@elytracms/studio/router/root'\n * import appCss from '../styles/app.css?url'\n * export const Route = createStudioRootRoute({ appCssUrl: appCss })\n * ```\n */\nexport interface StudioRootRouteOptions {\n /**\n * URL of the studio chrome stylesheet, from the consumer's Vite `?url` import\n * (`import appCss from '../styles/app.css?url'`). The one option every consumer\n * must pass — the sheet is project-owned (it carries the delivery theme).\n */\n appCssUrl: string\n /** Document `<title>`. Default: `'Elytra'`. */\n title?: string\n /**\n * `<html lang>`. Defaults to the **resolved studio-UI locale** (EC-513): the\n * config-pinned `i18n.locale` if there is one, otherwise the default locale on the\n * server and, once browser detection takes over after hydration, the visitor's\n * preferred locale. Pass a value only to fix `lang` to something the chrome is not\n * actually rendered in.\n */\n htmlLang?: string\n /**\n * Extra `<head>` meta entries appended after the defaults (charset, viewport,\n * title). Same shape TanStack Router's `head().meta` takes.\n */\n meta?: Array<Record<string, string>>\n /** Extra `<head>` link entries appended after the app stylesheet. */\n links?: Array<Record<string, string>>\n /**\n * Escape hatch: wrap additional project-specific providers around the routed\n * subtree. Rendered INSIDE `StudioProvider` and around `<Outlet />`, so nested\n * providers see the full studio/auth/theme context and remount with the active\n * project. Return the passed `children` wrapped in your providers.\n */\n providers?: (children: ReactNode) => ReactNode\n}\n\nexport function createStudioRootRoute(options: StudioRootRouteOptions) {\n const { appCssUrl, title = 'Elytra', htmlLang, meta = [], links = [], providers } = options\n\n // EC-402: register the base title for the shell's per-view tab-title effect\n // (`StudioTabTitle` composes `deepest crumb · view · project · <this title>`\n // and restores exactly this value when leaving a project view). Module-holder\n // seam — same pattern as `setActiveProjectSlug` — so the shell never has to\n // capture `document.title` and race the router's own head effect.\n setStudioBaseTitle(title)\n\n function RootComponent() {\n const location = useLocation()\n // EC-513: locale resolution is client-side, so the studio boots in the server's\n // locale (default, or the `i18n.locale` pin) and hands over to the browser's\n // preference AFTER hydration — the hydration render has to reproduce the server's\n // markup or React discards the tree with a mismatch error. Subscribing HERE, at the\n // one root above the whole chrome, is what makes the handover visible everywhere:\n // ~948 of the studio's localized render sites call the non-hook `translate()` and\n // would otherwise never re-render for it. It also keeps `<html lang>` honest —\n // it names the language the chrome is actually rendered in, at every moment.\n const uiLocale = useUiLocale()\n // EC-179: the active project selects its Convex backend. Resolve it from the\n // URL (or the sign-in redirect target) and bind it *before* the auth and\n // persistence adapters are selected, so the initial sign-in authenticates\n // against the project the user is heading for. Keying the auth/studio subtree\n // by the active project remounts it on a project switch, so the adapters\n // re-select against the new backend and the session is restored per-backend.\n const activeSlug = activeSlugForLocation(\n location.pathname,\n (location.search as { redirect?: string }).redirect,\n )\n setActiveProjectSlug(activeSlug)\n\n return (\n <RootDocument htmlLang={htmlLang ?? uiLocale}>\n <ThemeProvider>\n {/* EC-258: one deployment-setup gate runs before auth — a misconfigured or\n unreachable backend, an invalid workspace config, or any failing readiness\n precondition (missing auth signing key, required collections, …) is a\n single visible boot screen with copy-pasteable fixes, never a silent\n in-memory fallback or a misleading downstream error. All checks passing\n renders children as before. */}\n <DeploymentSetupGate>\n <AuthProvider key={activeSlug ?? '__launcher__'}>\n <AuthGate>\n <StudioProvider>\n {providers ? providers(<Outlet />) : <Outlet />}\n {/* EC-403: the blocking in-place re-auth overlay — mounted over\n every route, INSIDE StudioProvider (it invalidates the studio\n query cache on re-auth) and inside AuthProvider (it reads the\n sessionExpired state). Renders null unless the session died. */}\n <SessionExpiredOverlay />\n </StudioProvider>\n </AuthGate>\n </AuthProvider>\n </DeploymentSetupGate>\n </ThemeProvider>\n </RootDocument>\n )\n }\n\n return createRootRoute({\n head: () => ({\n meta: [\n { charSet: 'utf-8' },\n { name: 'viewport', content: 'width=device-width, initial-scale=1' },\n { title },\n ...meta,\n ],\n links: [{ rel: 'stylesheet', href: appCssUrl }, ...links],\n }),\n component: RootComponent,\n })\n}\n\n/**\n * Route guard (EC-150): one chokepoint in the root layout. Unauthenticated\n * visitors on protected paths are redirected to the sign-in screen (with the\n * intended target preserved); signed-in users never see the sign-in screen.\n * The decision logic is the pure, unit-tested `resolveRouteGuard`.\n */\nfunction AuthGate({ children }: { children: ReactNode }) {\n const { status } = useAuth()\n const location = useLocation()\n // EC-451: feed the still-pending `?redirect` search param into the decision.\n // Dev auto-login (or any fast real sign-in — SSO return, password-manager\n // autofill) can flip the session to signed-in while the location is still\n // `/sign-in?redirect=…`, BEFORE `SignInView`'s own post-sign-in navigation\n // runs; the gate's bounce then fires first and must consume the target\n // instead of dropping it on an unconditional `/`. The target is validated\n // by `postSignInTarget` inside `resolveRouteGuard` (same-origin relative\n // paths only — redirect is an attacker-suppliable search param).\n const rawRedirect = (location.search as { redirect?: unknown }).redirect\n const redirect = typeof rawRedirect === 'string' ? rawRedirect : undefined\n const decision = resolveRouteGuard(location.pathname, status, redirect)\n\n if (decision.kind === 'pending') return <SessionLoading />\n if (decision.kind === 'redirect-to-sign-in') {\n // EC-441: `NavigateOnce`, not `<Navigate>` — the gate stays mounted while\n // the sign-in transition is pending, and a per-render re-navigate spirals\n // into the router's update-depth rejection.\n return <NavigateOnce to={SIGN_IN_PATH} search={{ redirect: decision.redirect }} replace />\n }\n if (decision.kind === 'redirect-home') return <NavigateOnce to={decision.target} replace />\n return children\n}\n\n/**\n * The studio's boot screen: the deterministic session-restore state and — because a\n * request-less render always finds the auth gate `pending` — the entire body of the\n * build-time prerendered SPA shell (`dist/client/_shell.html`).\n *\n * **It is deliberately wordless** (EC-523; owner-ratified 2026-08-01: *\"remove the text\n * — if we really need something while loading, add a simple spinner, but not mandatory —\n * the shell should stay free of side-effects\"*). A build-time prerender has no request,\n * hence no `Accept-Language` and no session, so it cannot know the visitor's language.\n * EC-513 fixed the one string that flipped after hydration; removing the words removes\n * the possibility of the next one.\n *\n * **Accessibility**, because \"no words\" must not mean \"no accessible name\": moving the\n * text into an `aria-label` bakes the very same wrong-language string into the shell,\n * only somewhere a screen-reader user cannot notice it flip. So the shell ships an\n * **empty** `role=\"status\"` region — the correct live-region shape in any case, since a\n * region must already exist in the DOM for a later insertion to be announced — and\n * `useHydrated` fills it once hydration is done, in the visitor's actual locale. The\n * unannounced window is bounded by hydration, i.e. exactly the window in which no\n * announcement could have been correct.\n *\n * Do **not** add `aria-busy` here: on a live region it tells assistive tech to withhold\n * updates, which would suppress the very announcement this shape exists to deliver.\n *\n * Exported so the shell's pre-hydration markup is pinnable with `renderToStaticMarkup`\n * (`root.test.tsx`) — the built artefact is the real proof, but nothing else in the\n * node test pattern can reach this render.\n */\nexport function SessionLoading() {\n const hydrated = useHydrated()\n const t = useT()\n return (\n <div className=\"grid min-h-dvh place-items-center bg-background\" role=\"status\">\n <Loader2 className=\"size-6 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n {hydrated ? <span className=\"sr-only\">{t('auth.boot.checkingSession')}</span> : null}\n </div>\n )\n}\n\nfunction RootDocument({ children, htmlLang }: { children: ReactNode; htmlLang: string }) {\n // Dark-first SSR baseline; THEME_INIT_SCRIPT flips the class pre-hydration\n // from the stored preference, and ThemeProvider keeps it reactive thereafter.\n // suppressHydrationWarning: the script intentionally mutates the class before\n // React hydrates, so a className mismatch on <html> is expected, not a bug.\n // The app stylesheet is emitted via head().links and rendered by <HeadContent />.\n //\n // EC-523: `lang` is the ONE locale-dependent value that stays in the prerendered\n // shell, and it must. There is no locale-free value for it, and dropping it is a real\n // accessibility regression (a screen reader picks its voice from the document\n // language). It is baked as the build's default locale and flips to the visitor's\n // after hydration — honest at every moment, since EC-513 made it name the language the\n // chrome is actually rendered in. The mismatch it would otherwise raise is covered by\n // the same `suppressHydrationWarning` the theme script needs. Do not \"fix\" it by\n // hard-coding a value: that trades an attribute that is briefly stale for one that is\n // permanently wrong.\n return (\n <html lang={htmlLang} className=\"dark\" suppressHydrationWarning>\n <head>\n <HeadContent />\n <script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />\n </head>\n <body className=\"min-h-screen bg-background font-sans text-foreground antialiased\">\n {children}\n <Scripts />\n </body>\n </html>\n )\n}\n"]}
1
+ {"version":3,"file":"root.js","sourceRoot":"","sources":["../../src/router/root.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react'\nimport {\n createRootRoute,\n HeadContent,\n Outlet,\n Scripts,\n useLocation,\n} from '@tanstack/react-router'\nimport { NavigateOnce } from './navigate-once'\nimport { AuthProvider, useAuth } from '../components/auth-context'\nimport { DeploymentSetupGate } from '../components/deployment-setup-gate'\nimport { StudioRouteError } from '../components/load-failure'\nimport { SessionExpiredOverlay } from '../components/session-expired-overlay'\nimport { StudioProvider } from '../components/studio-context'\nimport { ThemeProvider } from '../components/theme-context'\nimport { Loader2 } from 'lucide-react'\nimport { SIGN_IN_PATH, resolveRouteGuard } from '../lib/auth'\nimport { useHydrated, useT, useUiLocale } from '../lib/i18n'\nimport { setStudioBaseTitle } from '../lib/tab-title'\nimport { activeSlugForLocation, setActiveProjectSlug } from '../lib/workspace'\nimport { THEME_INIT_SCRIPT } from '../lib/theme'\n\n/**\n * Studio root-route boot factory (EC-375; consolidated from the copy-pasted\n * consumer `__root.tsx` by EC-369 finding #7). It assembles the ONE canonical\n * provider/gate stack — ThemeProvider → DeploymentSetupGate → AuthProvider(keyed)\n * → AuthGate → StudioProvider → Outlet — plus the `RootDocument` shell\n * (html/head with the pre-hydration theme script + the chrome stylesheet), and the\n * route's `errorComponent` (EC-717).\n *\n * Its whole purpose is to make the NEXT required-gate addition automatic on\n * package upgrade: when EC-258 added `DeploymentSetupGate`, every hand-copied\n * `__root.tsx` had to be manually re-synced, and a production site's silently missed\n * it for five releases (EC-367). Owning the composition here means a consumer's\n * `__root.tsx` shrinks to a thin shell and picks up new gates for free.\n *\n * The consumer re-exports the returned route as `Route` (the generated route\n * tree imports `Route` from `./routes/__root`):\n *\n * ```tsx\n * import { createStudioRootRoute } from '@elytracms/studio/router/root'\n * import appCss from '../styles/app.css?url'\n * export const Route = createStudioRootRoute({ appCssUrl: appCss })\n * ```\n */\nexport interface StudioRootRouteOptions {\n /**\n * URL of the studio chrome stylesheet, from the consumer's Vite `?url` import\n * (`import appCss from '../styles/app.css?url'`). The one option every consumer\n * must pass — the sheet is project-owned (it carries the delivery theme).\n */\n appCssUrl: string\n /** Document `<title>`. Default: `'Elytra'`. */\n title?: string\n /**\n * `<html lang>`. Defaults to the **resolved studio-UI locale** (EC-513): the\n * config-pinned `i18n.locale` if there is one, otherwise the default locale on the\n * server and, once browser detection takes over after hydration, the visitor's\n * preferred locale. Pass a value only to fix `lang` to something the chrome is not\n * actually rendered in.\n */\n htmlLang?: string\n /**\n * Extra `<head>` meta entries appended after the defaults (charset, viewport,\n * title). Same shape TanStack Router's `head().meta` takes.\n */\n meta?: Array<Record<string, string>>\n /** Extra `<head>` link entries appended after the app stylesheet. */\n links?: Array<Record<string, string>>\n /**\n * Escape hatch: wrap additional project-specific providers around the routed\n * subtree. Rendered INSIDE `StudioProvider` and around `<Outlet />`, so nested\n * providers see the full studio/auth/theme context and remount with the active\n * project. Return the passed `children` wrapped in your providers.\n */\n providers?: (children: ReactNode) => ReactNode\n}\n\nexport function createStudioRootRoute(options: StudioRootRouteOptions) {\n const { appCssUrl, title = 'Elytra', htmlLang, meta = [], links = [], providers } = options\n\n // EC-402: register the base title for the shell's per-view tab-title effect\n // (`StudioTabTitle` composes `deepest crumb · view · project · <this title>`\n // and restores exactly this value when leaving a project view). Module-holder\n // seam — same pattern as `setActiveProjectSlug` — so the shell never has to\n // capture `document.title` and race the router's own head effect.\n setStudioBaseTitle(title)\n\n /**\n * EC-717: the document (`<html>`, head, stylesheet, scripts) is the root route's\n * `shellComponent`, not part of its `component`. TanStack Router renders the shell\n * AROUND the root match's error boundary (`Match.js`: `<ShellComponent>{content}</…>`),\n * so the `errorComponent` below renders inside the themed document. With the document\n * inside `component` — as it was — an error replaced the document itself and the notice\n * came up unstyled.\n */\n function RootShell({ children }: { children: ReactNode }) {\n // EC-513: keeps `<html lang>` honest — it names the language the chrome is actually\n // rendered in, at every moment (see `RootComponent` for the handover itself).\n const uiLocale = useUiLocale()\n return <RootDocument htmlLang={htmlLang ?? uiLocale}>{children}</RootDocument>\n }\n\n function RootComponent() {\n const location = useLocation()\n // EC-513: locale resolution is client-side, so the studio boots in the server's\n // locale (default, or the `i18n.locale` pin) and hands over to the browser's\n // preference AFTER hydration — the hydration render has to reproduce the server's\n // markup or React discards the tree with a mismatch error. Subscribing HERE, at the\n // one root above the whole chrome, is what makes the handover visible everywhere:\n // ~948 of the studio's localized render sites call the non-hook `translate()` and\n // would otherwise never re-render for it. (The shell's own subscription would not do\n // it: the shell re-rendering hands React the same `children` element, so React skips\n // the subtree.)\n useUiLocale()\n // EC-179: the active project selects its Convex backend. Resolve it from the\n // URL (or the sign-in redirect target) and bind it *before* the auth and\n // persistence adapters are selected, so the initial sign-in authenticates\n // against the project the user is heading for. Keying the auth/studio subtree\n // by the active project remounts it on a project switch, so the adapters\n // re-select against the new backend and the session is restored per-backend.\n const activeSlug = activeSlugForLocation(\n location.pathname,\n (location.search as { redirect?: string }).redirect,\n )\n setActiveProjectSlug(activeSlug)\n\n return (\n <ThemeProvider>\n {/* EC-258: one deployment-setup gate runs before auth — a misconfigured or\n unreachable backend, an invalid workspace config, or any failing readiness\n precondition (missing auth signing key, required collections, …) is a\n single visible boot screen with copy-pasteable fixes, never a silent\n in-memory fallback or a misleading downstream error. All checks passing\n renders children as before. */}\n <DeploymentSetupGate>\n <AuthProvider key={activeSlug ?? '__launcher__'}>\n <AuthGate>\n <StudioProvider>\n {providers ? providers(<Outlet />) : <Outlet />}\n {/* EC-403: the blocking in-place re-auth overlay — mounted over\n every route, INSIDE StudioProvider (it invalidates the studio\n query cache on re-auth) and inside AuthProvider (it reads the\n sessionExpired state). Renders null unless the session died. */}\n <SessionExpiredOverlay />\n </StudioProvider>\n </AuthGate>\n </AuthProvider>\n </DeploymentSetupGate>\n </ThemeProvider>\n )\n }\n\n return createRootRoute({\n head: () => ({\n meta: [\n { charSet: 'utf-8' },\n { name: 'viewport', content: 'width=device-width, initial-scale=1' },\n { title },\n ...meta,\n ],\n links: [{ rel: 'stylesheet', href: appCssUrl }, ...links],\n }),\n shellComponent: RootShell,\n component: RootComponent,\n // EC-717: the catch-all for any render error no surface boundary took (each lazily\n // loaded surface has its own `LazySurfaceBoundary`). Without it the router's bare\n // default replaced the whole studio with \"Something went wrong!\".\n errorComponent: StudioRouteError,\n })\n}\n\n/**\n * Route guard (EC-150): one chokepoint in the root layout. Unauthenticated\n * visitors on protected paths are redirected to the sign-in screen (with the\n * intended target preserved); signed-in users never see the sign-in screen.\n * The decision logic is the pure, unit-tested `resolveRouteGuard`.\n */\nfunction AuthGate({ children }: { children: ReactNode }) {\n const { status } = useAuth()\n const location = useLocation()\n // EC-451: feed the still-pending `?redirect` search param into the decision.\n // Dev auto-login (or any fast real sign-in — SSO return, password-manager\n // autofill) can flip the session to signed-in while the location is still\n // `/sign-in?redirect=…`, BEFORE `SignInView`'s own post-sign-in navigation\n // runs; the gate's bounce then fires first and must consume the target\n // instead of dropping it on an unconditional `/`. The target is validated\n // by `postSignInTarget` inside `resolveRouteGuard` (same-origin relative\n // paths only — redirect is an attacker-suppliable search param).\n const rawRedirect = (location.search as { redirect?: unknown }).redirect\n const redirect = typeof rawRedirect === 'string' ? rawRedirect : undefined\n const decision = resolveRouteGuard(location.pathname, status, redirect)\n\n if (decision.kind === 'pending') return <SessionLoading />\n if (decision.kind === 'redirect-to-sign-in') {\n // EC-441: `NavigateOnce`, not `<Navigate>` — the gate stays mounted while\n // the sign-in transition is pending, and a per-render re-navigate spirals\n // into the router's update-depth rejection.\n return <NavigateOnce to={SIGN_IN_PATH} search={{ redirect: decision.redirect }} replace />\n }\n if (decision.kind === 'redirect-home') return <NavigateOnce to={decision.target} replace />\n return children\n}\n\n/**\n * The studio's boot screen: the deterministic session-restore state and — because a\n * request-less render always finds the auth gate `pending` — the entire body of the\n * build-time prerendered SPA shell (`dist/client/_shell.html`).\n *\n * **It is deliberately wordless** (EC-523; owner-ratified 2026-08-01: *\"remove the text\n * — if we really need something while loading, add a simple spinner, but not mandatory —\n * the shell should stay free of side-effects\"*). A build-time prerender has no request,\n * hence no `Accept-Language` and no session, so it cannot know the visitor's language.\n * EC-513 fixed the one string that flipped after hydration; removing the words removes\n * the possibility of the next one.\n *\n * **Accessibility**, because \"no words\" must not mean \"no accessible name\": moving the\n * text into an `aria-label` bakes the very same wrong-language string into the shell,\n * only somewhere a screen-reader user cannot notice it flip. So the shell ships an\n * **empty** `role=\"status\"` region — the correct live-region shape in any case, since a\n * region must already exist in the DOM for a later insertion to be announced — and\n * `useHydrated` fills it once hydration is done, in the visitor's actual locale. The\n * unannounced window is bounded by hydration, i.e. exactly the window in which no\n * announcement could have been correct.\n *\n * Do **not** add `aria-busy` here: on a live region it tells assistive tech to withhold\n * updates, which would suppress the very announcement this shape exists to deliver.\n *\n * Exported so the shell's pre-hydration markup is pinnable with `renderToStaticMarkup`\n * (`root.test.tsx`) — the built artefact is the real proof, but nothing else in the\n * node test pattern can reach this render.\n */\nexport function SessionLoading() {\n const hydrated = useHydrated()\n const t = useT()\n return (\n <div className=\"grid min-h-dvh place-items-center bg-background\" role=\"status\">\n <Loader2 className=\"size-6 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n {hydrated ? <span className=\"sr-only\">{t('auth.boot.checkingSession')}</span> : null}\n </div>\n )\n}\n\nfunction RootDocument({ children, htmlLang }: { children: ReactNode; htmlLang: string }) {\n // Dark-first SSR baseline; THEME_INIT_SCRIPT flips the class pre-hydration\n // from the stored preference, and ThemeProvider keeps it reactive thereafter.\n // suppressHydrationWarning: the script intentionally mutates the class before\n // React hydrates, so a className mismatch on <html> is expected, not a bug.\n // The app stylesheet is emitted via head().links and rendered by <HeadContent />.\n //\n // EC-523: `lang` is the ONE locale-dependent value that stays in the prerendered\n // shell, and it must. There is no locale-free value for it, and dropping it is a real\n // accessibility regression (a screen reader picks its voice from the document\n // language). It is baked as the build's default locale and flips to the visitor's\n // after hydration — honest at every moment, since EC-513 made it name the language the\n // chrome is actually rendered in. The mismatch it would otherwise raise is covered by\n // the same `suppressHydrationWarning` the theme script needs. Do not \"fix\" it by\n // hard-coding a value: that trades an attribute that is briefly stale for one that is\n // permanently wrong.\n return (\n <html lang={htmlLang} className=\"dark\" suppressHydrationWarning>\n <head>\n <HeadContent />\n <script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />\n </head>\n <body className=\"min-h-screen bg-background font-sans text-foreground antialiased\">\n {children}\n <Scripts />\n </body>\n </html>\n )\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytracms/studio",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.100",
4
+ "version": "0.0.102",
5
5
  "engines": {
6
6
  "node": ">=22.18.0"
7
7
  },
@@ -153,7 +153,7 @@
153
153
  "lucide-react": "^1.23.0",
154
154
  "tailwind-merge": "^3.6.0",
155
155
  "zod": "^4.4.3",
156
- "@elytracms/core": "^0.0.100"
156
+ "@elytracms/core": "^0.0.102"
157
157
  },
158
158
  "peerDependencies": {
159
159
  "@tanstack/react-router": "^1.170.0",
@@ -179,7 +179,7 @@
179
179
  "vite": "^8.1.3",
180
180
  "zod-copy-newer": "npm:zod@4.6.7",
181
181
  "zod-copy-older": "npm:zod@4.1.13",
182
- "@elytracms/components": "^0.0.100"
182
+ "@elytracms/components": "^0.0.102"
183
183
  },
184
184
  "scripts": {
185
185
  "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node ../../scripts/fix-esm-extensions.mjs dist && node ../../scripts/check-esm-extensions.mjs dist",
@@ -1,127 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
- import { convexTest } from 'convex-test'
3
- import { makeFunctionReference } from 'convex/server'
4
- import { measureMaxDepth } from '@elytracms/core/cms-core'
5
- import { InMemoryAssetRepository, assetAlt, liftLegacyAssetAlts } from '@elytracms/core/persistence'
6
- import type { AssetRecord } from '@elytracms/core/persistence'
7
- import schema from './schema'
8
-
9
- // convex-test fires a `runAfter(0)` on a REAL timer; fake timers keep the scheduled
10
- // revalidations queued (rows written, the `webhooks:deliver` fetch never fired).
11
- beforeEach(() => {
12
- vi.useFakeTimers()
13
- })
14
- afterEach(() => {
15
- vi.useRealTimers()
16
- })
17
-
18
- /**
19
- * EC-587 — the one-shot migration of the legacy `alt` column into `meta.alt`, its one
20
- * home: the REAL internal mutation (`assets:liftLegacyAlt`, the line the operator runs
21
- * with `npx convex run`) on convex-test's backend, and the in-memory runner
22
- * (`liftLegacyAssetAlts`) held to the same expectations over the same rows.
23
- */
24
-
25
- const modules = {
26
- ...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
27
- './_generated/api.ts': async () => ({}),
28
- }
29
-
30
- const liftLegacyAlt = makeFunctionReference<'mutation'>('assets:liftLegacyAlt')
31
-
32
- const STAMP = '2026-10-26T00:00:00.000Z'
33
-
34
- /** The four shapes a stored asset row can hold its alt in. */
35
- const ROWS = [
36
- { assetId: 'column_only', alt: 'Harbour' },
37
- { assetId: 'both', alt: 'stale column', meta: { alt: 'Meta wins', credit: 'Ada' } },
38
- { assetId: 'meta_only', meta: { alt: 'Already home' } },
39
- { assetId: 'none', meta: { credit: 'Jo' } },
40
- ] as const
41
-
42
- function row(input: (typeof ROWS)[number]) {
43
- return {
44
- filename: `${input.assetId}.jpg`,
45
- contentType: 'image/jpeg',
46
- byteSize: 1,
47
- storageKey: `k_${input.assetId}`,
48
- createdAt: STAMP,
49
- updatedAt: STAMP,
50
- ...input,
51
- }
52
- }
53
-
54
- /** What every reader sees BEFORE the lift — the migration must not change it. */
55
- const READ_BEFORE = { column_only: 'Harbour', both: 'Meta wins', meta_only: 'Already home', none: null }
56
-
57
- const EXPECTED_META = {
58
- column_only: { alt: 'Harbour' },
59
- both: { alt: 'Meta wins', credit: 'Ada' },
60
- meta_only: { alt: 'Already home' },
61
- none: { credit: 'Jo' },
62
- }
63
-
64
- async function seededBackend() {
65
- const t = convexTest(schema, modules)
66
- await t.run(async (ctx) => {
67
- for (const input of ROWS) await ctx.db.insert('assets', row(input) as never)
68
- })
69
- return t
70
- }
71
-
72
- async function storedRows(t: Awaited<ReturnType<typeof seededBackend>>) {
73
- return t.run(async (ctx) => {
74
- const rows = await ctx.db.query('assets').collect()
75
- return Object.fromEntries(rows.map((r) => [r.assetId, { alt: r.alt, meta: r.meta }]))
76
- })
77
- }
78
-
79
- describe('EC-587 — assets:liftLegacyAlt moves the legacy column home, once', () => {
80
- it('lifts the column into meta.alt (meta.alt wins when both), unsets it, and keeps every alt reading', async () => {
81
- const t = await seededBackend()
82
- const before = await storedRows(t)
83
- expect(Object.fromEntries(Object.entries(before).map(([id, r]) => [id, assetAlt(r, null)]))).toEqual(READ_BEFORE)
84
-
85
- const report = (await t.mutation(liftLegacyAlt, {})) as { scanned: number; lifted: string[] }
86
- expect(report.scanned).toBe(4)
87
- expect([...report.lifted].sort()).toEqual(['both', 'column_only'])
88
-
89
- const after = await storedRows(t)
90
- for (const [id, stored] of Object.entries(after)) {
91
- expect(stored.alt, `${id}: the column is gone`).toBeUndefined()
92
- expect(stored.meta, `${id}: meta`).toEqual(EXPECTED_META[id as keyof typeof EXPECTED_META])
93
- // convex-test enforces no nesting depth — the stored shape asserts it itself.
94
- expect(measureMaxDepth(stored.meta).depth).toBeLessThanOrEqual(16)
95
- }
96
- // The lift changes no reading: every alt is what it was.
97
- expect(Object.fromEntries(Object.entries(after).map(([id, r]) => [id, assetAlt(r, null)]))).toEqual(READ_BEFORE)
98
- })
99
-
100
- it('is idempotent: a second run lifts nothing and writes nothing', async () => {
101
- const t = await seededBackend()
102
- await t.mutation(liftLegacyAlt, {})
103
- const once = await storedRows(t)
104
- const scheduledAfterFirst = await t.run(async (ctx) => (await ctx.db.system.query('_scheduled_functions').collect()).length)
105
- const again = (await t.mutation(liftLegacyAlt, {})) as { scanned: number; lifted: string[] }
106
- expect(again).toEqual({ scanned: 4, lifted: [] })
107
- expect(await storedRows(t)).toEqual(once)
108
- const scheduledAfterSecond = await t.run(async (ctx) => (await ctx.db.system.query('_scheduled_functions').collect()).length)
109
- expect(scheduledAfterSecond).toBe(scheduledAfterFirst)
110
- })
111
-
112
- it('the in-memory runner does the same to the same rows (the playground / headless equivalent)', async () => {
113
- const records: AssetRecord[] = ROWS.map((input) => {
114
- const { assetId, ...rest } = row(input)
115
- return { ...rest, id: assetId, projectId: 'p' } as AssetRecord
116
- })
117
- const repo = new InMemoryAssetRepository({ seed: records })
118
- const report = await liftLegacyAssetAlts(repo, 'p')
119
- expect(report.scanned).toBe(4)
120
- expect([...report.lifted].sort()).toEqual(['both', 'column_only'])
121
- const after = await repo.listAssets('p')
122
- expect(Object.fromEntries(after.map((r) => [r.id, { alt: r.alt, meta: r.meta }]))).toEqual(
123
- Object.fromEntries(Object.entries(EXPECTED_META).map(([id, meta]) => [id, { alt: undefined, meta }])),
124
- )
125
- expect((await liftLegacyAssetAlts(repo, 'p')).lifted).toEqual([])
126
- })
127
- })