camox 0.48.0 → 0.52.1

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 (127) hide show
  1. package/dist/core/createApp.d.ts +1 -0
  2. package/dist/core/createApp.js +1 -0
  3. package/dist/core/editing/contentEdits.js +78 -0
  4. package/dist/core/editing/createEditableBlock.js +277 -311
  5. package/dist/core/layoutContext.d.ts +1 -0
  6. package/dist/core/lib/contentType.js +2 -2
  7. package/dist/core/recordItem.js +3 -3
  8. package/dist/features/canvas/CamoxCanvas.js +15 -15
  9. package/dist/features/canvas/CanvasLoadingScreen.js +15 -11
  10. package/dist/features/canvas/CanvasPageFrame.js +81 -87
  11. package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
  12. package/dist/features/canvas/DerivedPagePathInput.js +12 -12
  13. package/dist/features/canvas/canvasOverlayGeometry.js +1 -1
  14. package/dist/features/canvas/canvasStore.js +2 -2
  15. package/dist/features/canvas/sidebar/CanvasInspector.js +1 -1
  16. package/dist/features/canvas/sidebar/CuratedPageInspector.js +1 -1
  17. package/dist/features/canvas/sidebar/LayoutPageBlocks.js +23 -31
  18. package/dist/features/canvas/useCanvasCamera.js +1 -1
  19. package/dist/features/canvas/useDiscoveredPreviewPaths.js +16 -9
  20. package/dist/features/content/ContentAssets.js +84 -88
  21. package/dist/features/page/CuratedPageContent.js +1 -1
  22. package/dist/features/preview/CamoxPreview.d.ts +8 -4
  23. package/dist/features/preview/CamoxPreview.js +258 -443
  24. package/dist/features/preview/EditablePageContent.js +244 -2
  25. package/dist/features/preview/EditablePageExperience.js +19 -13
  26. package/dist/features/preview/components/AddBlockSheet.js +55 -15
  27. package/dist/features/preview/components/BlockActionsPopover.js +23 -51
  28. package/dist/features/preview/components/BlockPeek.js +66 -0
  29. package/dist/features/preview/components/BlockThumbnail.js +134 -119
  30. package/dist/features/preview/components/CanvasPreview.js +1 -1
  31. package/dist/features/preview/components/CreatePageModal.js +150 -156
  32. package/dist/features/preview/components/FieldToolbar.js +1 -1
  33. package/dist/features/preview/components/Frame.js +27 -4
  34. package/dist/features/preview/components/MobilePreviewDrawer.js +6 -9
  35. package/dist/features/preview/components/PageInfo.js +161 -126
  36. package/dist/features/preview/components/PagePicker.js +2 -2
  37. package/dist/features/preview/components/PageTree.js +174 -190
  38. package/dist/features/preview/components/PreviewPanel.js +13 -67
  39. package/dist/features/preview/components/PreviewToolbar.js +1 -1
  40. package/dist/features/preview/components/ReferenceFieldEditor.js +4 -4
  41. package/dist/features/preview/components/ReferenceListFieldEditor.js +4 -4
  42. package/dist/features/preview/components/RemovedBlockPlaceholder.js +37 -70
  43. package/dist/features/preview/components/RepeatableItemsList.js +1 -1
  44. package/dist/features/preview/components/SelectionEditor.js +248 -256
  45. package/dist/features/preview/components/StudioEditingFallback.js +22 -26
  46. package/dist/features/preview/components/useFieldWriter.js +19 -19
  47. package/dist/features/preview/components/useRecordView.js +105 -147
  48. package/dist/features/preview/components/useRepeatableItemActions.js +0 -2
  49. package/dist/features/preview/pageRecord.js +30 -24
  50. package/dist/features/preview/previewConstants.js +2 -8
  51. package/dist/features/preview/previewStore.js +2 -0
  52. package/dist/features/preview/studioNotice.js +20 -0
  53. package/dist/features/preview/usePreviewViewportActions.js +1 -1
  54. package/dist/features/provider/AuthenticatedCamoxProvider.js +47 -31
  55. package/dist/features/provider/components/CommandPalette.js +2 -5
  56. package/dist/features/provider/signInShortcut.js +1 -2
  57. package/dist/features/provider/useAdminShortcuts.js +47 -53
  58. package/dist/features/renderer/RendererApp.js +542 -0
  59. package/dist/features/renderer/RendererFrames.js +450 -0
  60. package/dist/features/renderer/definitionAnswers.js +26 -0
  61. package/dist/features/renderer/protocol.js +35 -0
  62. package/dist/features/renderer/rendererHost.js +249 -0
  63. package/dist/features/renderer/studioConnection.js +186 -0
  64. package/dist/features/routes/pageRuntime.d.ts +2 -1
  65. package/dist/features/routes/pageRuntime.js +34 -22
  66. package/dist/features/runtime/RuntimeChrome.js +2 -5
  67. package/dist/features/runtime/assetPrefix.js +14 -0
  68. package/dist/features/runtime/breadcrumbs.js +1 -1
  69. package/dist/features/runtime/editingFallback.js +4 -8
  70. package/dist/features/runtime/livePages.js +1 -1
  71. package/dist/features/runtime/pageApp.js +57 -83
  72. package/dist/features/runtime/pageClient.js +1 -1
  73. package/dist/features/runtime/pageNavigation.js +3 -2
  74. package/dist/features/runtime/publicCache.js +95 -0
  75. package/dist/features/runtime/publishingDraft.js +23 -0
  76. package/dist/features/runtime/rendererClient.d.ts +8 -0
  77. package/dist/features/runtime/rendererClient.js +77 -0
  78. package/dist/features/runtime/runtime.d.ts +22 -3
  79. package/dist/features/runtime/runtime.js +209 -96
  80. package/dist/features/runtime/runtimeRoutes.d.ts +2 -0
  81. package/dist/features/runtime/runtimeRoutes.js +2 -0
  82. package/dist/features/runtime/worker.d.ts +14 -1
  83. package/dist/features/runtime/worker.js +34 -4
  84. package/dist/features/settings/AnalyticsSettings.js +622 -0
  85. package/dist/features/settings/CamoxSettings.js +95 -0
  86. package/dist/features/settings/FaviconSettings.js +254 -0
  87. package/dist/features/settings/ProjectNameSettings.js +145 -0
  88. package/dist/features/settings/SettingsPart.js +73 -0
  89. package/dist/features/settings/SocialProfilesSettings.js +245 -0
  90. package/dist/features/settings/useProjectMutation.js +61 -0
  91. package/dist/features/studio/FollowAgentProvider.js +10 -10
  92. package/dist/features/studio/components/DraftsControl.js +136 -131
  93. package/dist/features/studio/components/LocalePicker.js +178 -174
  94. package/dist/features/studio/components/Navbar.js +107 -66
  95. package/dist/features/studio/components/ProjectMenu.js +119 -171
  96. package/dist/features/studio/components/ReviewDraftSheet.js +1151 -991
  97. package/dist/features/studio/components/ThreadMessages.js +100 -65
  98. package/dist/features/studio/components/UserButton.js +2 -2
  99. package/dist/features/studio/routes.js +3 -1
  100. package/dist/features/studio/showInCanvas.js +129 -0
  101. package/dist/features/vite/definitionsSync.js +5 -6
  102. package/dist/features/vite/hostedChecks.js +127 -0
  103. package/dist/features/vite/runtimeDev.js +58 -17
  104. package/dist/features/vite/staticAssetHeaders.js +52 -0
  105. package/dist/features/vite/vite.d.ts +1 -1
  106. package/dist/features/vite/vite.js +49 -9
  107. package/dist/hooks/use-page-destinations.js +10 -10
  108. package/dist/lib/api-client-server.d.ts +1 -1
  109. package/dist/lib/api-client.d.ts +2 -1
  110. package/dist/lib/draft-scope.d.ts +1 -0
  111. package/dist/lib/locale-path.d.ts +3 -11
  112. package/dist/lib/locale-path.js +3 -27
  113. package/dist/lib/queries.js +7 -1
  114. package/dist/lib/revisions.js +20 -0
  115. package/dist/lib/shortcuts.js +29 -0
  116. package/dist/lib/studio-definitions.js +160 -0
  117. package/dist/lib/studio-reads.d.ts +1 -0
  118. package/dist/lib/studio-reads.js +65 -21
  119. package/dist/lib/use-project-room.js +16 -10
  120. package/dist/lib/utils.js +1 -104
  121. package/dist/package.js +1 -1
  122. package/dist/studio.css +1 -1
  123. package/package.json +9 -4
  124. package/skills/camox/SKILL.md +4 -0
  125. package/skills/camox/references/artifacts.md +1 -1
  126. package/dist/features/studio/components/FaviconDialog.js +0 -318
  127. package/dist/features/studio/components/SocialProfilesDialog.js +0 -342
@@ -22,6 +22,10 @@ const VIRTUAL_STUDIO_CLIENT = "virtual:camox/studio-client";
22
22
  const RESOLVED_VIRTUAL_STUDIO_CLIENT = "\0" + VIRTUAL_STUDIO_CLIENT;
23
23
  const VIRTUAL_STUDIO_CLIENT_URL = "virtual:camox/studio-client-url";
24
24
  const RESOLVED_VIRTUAL_STUDIO_CLIENT_URL = "\0" + VIRTUAL_STUDIO_CLIENT_URL;
25
+ const VIRTUAL_RENDERER_CLIENT = "virtual:camox/renderer-client";
26
+ const RESOLVED_VIRTUAL_RENDERER_CLIENT = "\0" + VIRTUAL_RENDERER_CLIENT;
27
+ const VIRTUAL_RENDERER_CLIENT_URL = "virtual:camox/renderer-client-url";
28
+ const RESOLVED_VIRTUAL_RENDERER_CLIENT_URL = "\0" + VIRTUAL_RENDERER_CLIENT_URL;
25
29
  const VIRTUAL_BLOCK_CHUNKS = "virtual:camox/block-chunks";
26
30
  const RESOLVED_VIRTUAL_BLOCK_CHUNKS = "\0" + VIRTUAL_BLOCK_CHUNKS;
27
31
  const VIRTUAL_APP_STYLESHEET_URL = "virtual:camox/app-stylesheet-url";
@@ -64,6 +68,7 @@ function resolveCamoxSourceImport(id) {
64
68
  "camox/_internal/pageServer": resolve(sdkRoot, "src/features/runtime/pageServer.tsx"),
65
69
  "camox/_internal/pageClient": resolve(sdkRoot, "src/features/runtime/pageClient.tsx"),
66
70
  "camox/_internal/studioClient": resolve(sdkRoot, "src/features/runtime/studioClient.tsx"),
71
+ "camox/_internal/rendererClient": resolve(sdkRoot, "src/features/runtime/rendererClient.tsx"),
67
72
  "camox/_internal/studioServer": resolve(sdkRoot, "src/features/runtime/studioServer.tsx"),
68
73
  "camox/_internal/runtime": resolve(sdkRoot, "src/features/runtime/runtime.ts"),
69
74
  "camox/worker": resolve(sdkRoot, "src/features/runtime/worker.ts")
@@ -167,6 +172,13 @@ import { camoxApp } from "virtual:camox/app";
167
172
  hydrateStudioWithApp(camoxApp);
168
173
  `;
169
174
  }
175
+ function generateVirtualRendererClient() {
176
+ return `import { startRendererWithApp } from "camox/_internal/rendererClient";
177
+ import { camoxApp } from "virtual:camox/app";
178
+
179
+ startRendererWithApp(camoxApp);
180
+ `;
181
+ }
170
182
  function wrapViteDevId(id) {
171
183
  return `/@id/${id.replace("\0", "__x00__")}`;
172
184
  }
@@ -176,6 +188,9 @@ function generateVirtualPageClientUrl() {
176
188
  function generateVirtualStudioClientUrl() {
177
189
  return `export default ${JSON.stringify(wrapViteDevId(RESOLVED_VIRTUAL_STUDIO_CLIENT))};`;
178
190
  }
191
+ function generateVirtualRendererClientUrl() {
192
+ return `export default ${JSON.stringify(wrapViteDevId(RESOLVED_VIRTUAL_RENDERER_CLIENT))};`;
193
+ }
179
194
  function generateVirtualAppStylesheetUrl() {
180
195
  return `import stylesheetUrl from "/src/styles.css?url";
181
196
 
@@ -183,7 +198,10 @@ export default stylesheetUrl;
183
198
  `;
184
199
  }
185
200
  function generateVirtualCamoxServer(runtimeBasePath) {
186
- return `import { handleCamoxRequest as handleRuntimeRequest } from "camox/_internal/runtime";
201
+ return `import {
202
+ handleCamoxRequest as handleRuntimeRequest,
203
+ isPublicRequest as isPublicRuntimeRequest,
204
+ } from "camox/_internal/runtime";
187
205
  import { blockModulePaths, camoxApp } from "virtual:camox/app";
188
206
  import blockChunks from "virtual:camox/block-chunks";
189
207
  import { camoxDocument } from "virtual:camox/document";
@@ -191,6 +209,7 @@ import appStylesheetUrl from "virtual:camox/app-stylesheet-url";
191
209
  import studioStylesheetUrl from "virtual:camox-studio-css";
192
210
  import pageClientEntryUrl from "virtual:camox/page-client-url";
193
211
  import studioClientEntryUrl from "virtual:camox/studio-client-url";
212
+ import rendererClientEntryUrl from "virtual:camox/renderer-client-url";
194
213
  import { renderPage } from "virtual:camox/page-server";
195
214
  import { renderStudio } from "virtual:camox/studio-server";
196
215
  import devHtml from "virtual:camox/dev-html";
@@ -198,23 +217,41 @@ import devHtml from "virtual:camox/dev-html";
198
217
  export { devHtml };
199
218
  export const runtimeBasePath = ${JSON.stringify(normalizeRuntimeBasePath(runtimeBasePath))};
200
219
 
220
+ const options = {
221
+ apiUrl: __CAMOX_API_URL__,
222
+ authenticationUrl: __CAMOX_AUTHENTICATION_URL__,
223
+ blockPreloads: { modulePaths: blockModulePaths, chunks: blockChunks },
224
+ pageClientEntryUrl,
225
+ studioClientEntryUrl,
226
+ rendererClientEntryUrl,
227
+ developmentDraft: __CAMOX_DEVELOPMENT_DRAFT__,
228
+ getCamoxApp: async () => camoxApp,
229
+ getDocument: async () => camoxDocument,
230
+ projectUuid: __CAMOX_PROJECT_ID__,
231
+ renderPage,
232
+ renderStudio,
233
+ runtimeBasePath: ${JSON.stringify(runtimeBasePath)},
234
+ stylesheetUrl: appStylesheetUrl,
235
+ studioStylesheetUrl,
236
+ };
237
+
238
+ export const publicCacheSource = {
239
+ apiUrl: __CAMOX_API_URL__,
240
+ projectUuid: __CAMOX_PROJECT_ID__,
241
+ build: __CAMOX_BUILD_ID__,
242
+ };
243
+
201
244
  export async function handleCamoxRequest(request) {
202
- return handleRuntimeRequest(request, {
203
- apiUrl: __CAMOX_API_URL__,
204
- authenticationUrl: __CAMOX_AUTHENTICATION_URL__,
205
- blockPreloads: { modulePaths: blockModulePaths, chunks: blockChunks },
206
- pageClientEntryUrl,
207
- studioClientEntryUrl,
208
- developmentDraft: __CAMOX_DEVELOPMENT_DRAFT__,
209
- getCamoxApp: async () => camoxApp,
210
- getDocument: async () => camoxDocument,
211
- projectUuid: __CAMOX_PROJECT_ID__,
212
- renderPage,
213
- renderStudio,
214
- runtimeBasePath: ${JSON.stringify(runtimeBasePath)},
215
- stylesheetUrl: appStylesheetUrl,
216
- studioStylesheetUrl,
217
- });
245
+ return handleRuntimeRequest(request, options);
246
+ }
247
+
248
+ export function isPublicRequest(request) {
249
+ return isPublicRuntimeRequest(request, options);
250
+ }
251
+
252
+ // JSON, as \`definitions.sync\` receives it: what isn't JSON never reaches the API.
253
+ export async function readDefinitionSet() {
254
+ return JSON.parse(JSON.stringify(await camoxApp.getDefinitionSet()));
218
255
  }
219
256
  `;
220
257
  }
@@ -232,6 +269,8 @@ function resolveRuntimeDevId(id, importer) {
232
269
  if (id === VIRTUAL_PAGE_CLIENT_URL) return RESOLVED_VIRTUAL_PAGE_CLIENT_URL;
233
270
  if (id === VIRTUAL_STUDIO_CLIENT) return RESOLVED_VIRTUAL_STUDIO_CLIENT;
234
271
  if (id === VIRTUAL_STUDIO_CLIENT_URL) return RESOLVED_VIRTUAL_STUDIO_CLIENT_URL;
272
+ if (id === VIRTUAL_RENDERER_CLIENT) return RESOLVED_VIRTUAL_RENDERER_CLIENT;
273
+ if (id === VIRTUAL_RENDERER_CLIENT_URL) return RESOLVED_VIRTUAL_RENDERER_CLIENT_URL;
235
274
  if (id === VIRTUAL_APP_STYLESHEET_URL) return RESOLVED_VIRTUAL_APP_STYLESHEET_URL;
236
275
  if (id === VIRTUAL_BLOCK_CHUNKS) return RESOLVED_VIRTUAL_BLOCK_CHUNKS;
237
276
  if (id === VIRTUAL_STUDIO_CSS) return RESOLVED_VIRTUAL_STUDIO_CSS;
@@ -249,6 +288,8 @@ function loadRuntimeDevModule(id, options = {}) {
249
288
  if (id === RESOLVED_VIRTUAL_PAGE_CLIENT_URL) return generateVirtualPageClientUrl();
250
289
  if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT) return generateVirtualStudioClient();
251
290
  if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT_URL) return generateVirtualStudioClientUrl();
291
+ if (id === RESOLVED_VIRTUAL_RENDERER_CLIENT) return generateVirtualRendererClient();
292
+ if (id === RESOLVED_VIRTUAL_RENDERER_CLIENT_URL) return generateVirtualRendererClientUrl();
252
293
  if (id === RESOLVED_VIRTUAL_APP_STYLESHEET_URL) return generateVirtualAppStylesheetUrl();
253
294
  if (id === RESOLVED_VIRTUAL_STUDIO_CSS) {
254
295
  const cssUrl = `/@fs/${resolve(sdkRoot, "dist/studio.css").replaceAll("\\", "/").replace(/^\/+/, "")}`;
@@ -0,0 +1,52 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+
4
+ //#region src/features/vite/staticAssetHeaders.ts
5
+ /** Where the client build puts the files it names by their content's hash. */
6
+ const HASHED_DIRS = ["assets", "_chunks"];
7
+ const IMMUTABLE = "public, max-age=31536000, immutable";
8
+ /** Workers Static Assets reads at most this many rules. */
9
+ const MAX_RULES = 100;
10
+ /**
11
+ * The `_headers` file Workers Static Assets serves the client build with. Files under
12
+ * `/assets/` and `/_chunks/` are named by their content's hash, so browsers keep them for good;
13
+ * the files there named otherwise, the entry chunks pages load by a fixed name and whatever the
14
+ * app's public directory puts there, keep the default, which revalidates. The app's own
15
+ * `_headers` comes first, as it wrote it.
16
+ */
17
+ function staticAssetHeaders({ ownHeaders, unhashedFiles }) {
18
+ const rules = [...HASHED_DIRS.map((dir) => `/${dir}/*\n Cache-Control: ${IMMUTABLE}`), ...[...unhashedFiles].sort().map((file) => `/${file}\n ! Cache-Control`)];
19
+ const own = ownHeaders?.trimEnd();
20
+ const ownRules = own ? own.split("\n").filter((line) => /^\S/.test(line) && !line.startsWith("#")) : [];
21
+ if (ownRules.length + rules.length > MAX_RULES) throw new Error(`Camox: _headers would hold ${ownRules.length + rules.length} rules, past Workers Static Assets' ${MAX_RULES}. Move the public directory's files out of /assets/ and /_chunks/, or drop rules from public/_headers.`);
22
+ return [
23
+ ...own ? [own] : [],
24
+ "# Camox: files named by their content's hash never change.",
25
+ ...rules,
26
+ ""
27
+ ].join("\n");
28
+ }
29
+ /** The entry chunks of a bundle under the hashed directories, which pages load by name. */
30
+ function unhashedEntries(bundle) {
31
+ return Object.values(bundle).flatMap((output) => output.type === "chunk" && output.isEntry ? [output.fileName] : []).filter(inHashedDir);
32
+ }
33
+ /** The files a public directory puts under the hashed directories, relative to it. */
34
+ function publicFilesInHashedDirs(publicDir) {
35
+ if (!publicDir) return [];
36
+ return HASHED_DIRS.flatMap((dir) => filesUnder(join(publicDir, dir))).map((file) => relative(publicDir, file).replaceAll("\\", "/"));
37
+ }
38
+ /** The app's own `_headers`, if its public directory has one. */
39
+ function readOwnHeaders(publicDir) {
40
+ const path = publicDir && join(publicDir, "_headers");
41
+ return path && existsSync(path) ? readFileSync(path, "utf-8") : void 0;
42
+ }
43
+ function inHashedDir(file) {
44
+ return HASHED_DIRS.some((dir) => file.startsWith(`${dir}/`));
45
+ }
46
+ function filesUnder(dir) {
47
+ if (!existsSync(dir)) return [];
48
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? filesUnder(join(dir, entry.name)) : [join(dir, entry.name)]);
49
+ }
50
+
51
+ //#endregion
52
+ export { publicFilesInHashedDirs, readOwnHeaders, staticAssetHeaders, unhashedEntries };
@@ -23,7 +23,7 @@ export interface CamoxPluginOptions {
23
23
  _internal?: {
24
24
  /** URL of the Camox API backend, used for data fetching */
25
25
  apiUrl?: string;
26
- /** URL of the Camox authentication backend (default: https://app.camox.dev) */
26
+ /** URL of the Camox authentication backend (default: https://app.camox.ai) */
27
27
  authenticationUrl?: string;
28
28
  /** Show Tanstack query devtools (default: false) */
29
29
  enableTanstackDevtools?: boolean;
@@ -1,4 +1,5 @@
1
1
  import { createServerApiClient } from "../../lib/api-client-server.js";
2
+ import { ASSET_PREFIX_GLOBAL } from "../runtime/assetPrefix.js";
2
3
  import { transformArtifactRuntime } from "./artifactRuntimeMinification.js";
3
4
  import { readAuthTokenForUrl } from "./auth.js";
4
5
  import { watchNewBlockFiles } from "./blockBoilerplate.js";
@@ -6,8 +7,10 @@ import { collectBlockChunks } from "./blockChunks.js";
6
7
  import { withoutExpectedCamoxLogs } from "./bundlerLogs.js";
7
8
  import { installDevAuthenticationMiddleware } from "./devAuthentication.js";
8
9
  import { installDevSignInLink } from "./devSignIn.js";
10
+ import { HOSTED_BUILD_ENV, forbiddenImport, misnamedBlock, readHostedProject } from "./hostedChecks.js";
9
11
  import { generateIconTypes } from "./iconGeneration.js";
10
12
  import { generateLayoutFiles, watchLayoutFiles } from "./layoutGeneration.js";
13
+ import { publicFilesInHashedDirs, readOwnHeaders, staticAssetHeaders, unhashedEntries } from "./staticAssetHeaders.js";
11
14
  import { buildClientThenWorker } from "./workerBuild.js";
12
15
  import { VIRTUAL_CAMOX_APP, syncDefinitions, syncDefinitionsToApi } from "./definitionsSync.js";
13
16
  import { publishSeededContent } from "./publishSeededContent.js";
@@ -15,9 +18,9 @@ import { cleanupGeneratedRouteFiles } from "./routeGeneration.js";
15
18
  import { RESOLVED_VIRTUAL_DEV_HTML, createRuntimeRegistryPlugin, loadRuntimeDevModule, resolveRuntimeDevId } from "./runtimeDev.js";
16
19
  import { projectIdSchema } from "@camox/api-contract/ids";
17
20
  import { DEVELOPMENT_DRAFT } from "@camox/api-contract/drafts";
18
- import { createHash } from "node:crypto";
21
+ import { createHash, randomUUID } from "node:crypto";
19
22
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
- import { dirname, join, resolve } from "node:path";
23
+ import { dirname, join, posix, resolve } from "node:path";
21
24
  import { fileURLToPath } from "node:url";
22
25
  import { createServer } from "vite-plus";
23
26
 
@@ -33,10 +36,14 @@ const VIRTUAL_PAGE_CLIENT = "virtual:camox/page-client";
33
36
  const RESOLVED_VIRTUAL_PAGE_CLIENT_URL = "\0virtual:camox/page-client-url";
34
37
  const VIRTUAL_STUDIO_CLIENT = "virtual:camox/studio-client";
35
38
  const RESOLVED_VIRTUAL_STUDIO_CLIENT_URL = "\0virtual:camox/studio-client-url";
39
+ const VIRTUAL_RENDERER_CLIENT = "virtual:camox/renderer-client";
40
+ const RESOLVED_VIRTUAL_RENDERER_CLIENT_URL = "\0virtual:camox/renderer-client-url";
36
41
  const RESOLVED_VIRTUAL_BLOCK_CHUNKS = "\0virtual:camox/block-chunks";
37
- const PRODUCTION_API_URL = "https://api.camox.dev";
42
+ /** A built file's URL in the server bundle: under the asset prefix, read when the bundle loads. */
43
+ const serverAssetUrl = (file) => `(globalThis.${ASSET_PREFIX_GLOBAL} ?? "/") + ${JSON.stringify(file)}`;
44
+ const PRODUCTION_API_URL = "https://api.camox.ai";
38
45
  /** Authentication URL to use for Camox authentication (production Camox Dashboard) */
39
- const DEFAULT_AUTHENTICATION_URL = "https://app.camox.dev";
46
+ const DEFAULT_AUTHENTICATION_URL = "https://app.camox.ai";
40
47
  /** What Vite injects into HTML in dev, which the Worker can't ask Vite for itself. */
41
48
  async function getDevHtml(server) {
42
49
  const html = await server.transformIndexHtml("/", "<html><head></head><body></body></html>");
@@ -173,6 +180,8 @@ function camox(options) {
173
180
  let isRelease = false;
174
181
  /** A release's build, without syncing to Live: what the upgrade job checks a project with. */
175
182
  let isDryRun = false;
183
+ /** Set when the build checks a hosted project's code. */
184
+ let hostedProject;
176
185
  let resolvedConfig;
177
186
  let localAuth = null;
178
187
  let blockChunks = {};
@@ -181,6 +190,10 @@ function camox(options) {
181
190
  name: "camox",
182
191
  enforce: "pre",
183
192
  resolveId(id, importer) {
193
+ if (hostedProject) {
194
+ const forbidden = forbiddenImport(hostedProject, id, importer);
195
+ if (forbidden) this.error(forbidden);
196
+ }
184
197
  const runtimeId = resolveRuntimeDevId(id, importer);
185
198
  if (runtimeId) return runtimeId;
186
199
  if (id === VIRTUAL_STUDIO_CSS) return RESOLVED_VIRTUAL_STUDIO_CSS;
@@ -198,7 +211,8 @@ function camox(options) {
198
211
  fileName,
199
212
  source: css
200
213
  });
201
- return `export default ${JSON.stringify(`/${fileName}`)};`;
214
+ if (this.environment.config.consumer === "client") return `export default new URL(/* @vite-ignore */ ${JSON.stringify(`../${fileName}`)}, import.meta.url).href;`;
215
+ return `export default ${serverAssetUrl(fileName)};`;
202
216
  }
203
217
  const cssUrl = `/@fs/${cssPath.replaceAll("\\", "/").replace(/^\/+/, "")}`;
204
218
  return `export default ${JSON.stringify(cssUrl)};`;
@@ -208,8 +222,9 @@ function camox(options) {
208
222
  const css = readFileSync(cssPath, "utf-8");
209
223
  return `export default ${JSON.stringify(css)};`;
210
224
  }
211
- if (id === RESOLVED_VIRTUAL_PAGE_CLIENT_URL && isBuild) return "export default \"/_chunks/camox-page-client.mjs\";";
212
- if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT_URL && isBuild) return "export default \"/_chunks/camox-studio-client.mjs\";";
225
+ if (id === RESOLVED_VIRTUAL_PAGE_CLIENT_URL && isBuild) return `export default ${serverAssetUrl("_chunks/camox-page-client.mjs")};`;
226
+ if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT_URL && isBuild) return `export default ${serverAssetUrl("_chunks/camox-studio-client.mjs")};`;
227
+ if (id === RESOLVED_VIRTUAL_RENDERER_CLIENT_URL && isBuild) return `export default ${serverAssetUrl("_chunks/camox-renderer-client.mjs")};`;
213
228
  if (id === RESOLVED_VIRTUAL_BLOCK_CHUNKS && isBuild) return `export default ${JSON.stringify(blockChunks)};`;
214
229
  if (id === "\0virtual:camox/dev-html") {
215
230
  if (!devServer) return "export default null;";
@@ -221,11 +236,25 @@ function camox(options) {
221
236
  });
222
237
  },
223
238
  transform(code, id) {
239
+ if (hostedProject) {
240
+ const misnamed = misnamedBlock(hostedProject, id, code);
241
+ if (misnamed) this.error(misnamed.message, misnamed.at);
242
+ }
224
243
  return transformArtifactRuntime(code, id);
225
244
  },
226
245
  generateBundle(_outputOptions, bundle) {
227
246
  if (this.environment.name !== "client") return;
228
247
  blockChunks = collectBlockChunks(bundle, resolvedConfig.root);
248
+ if (!isBuild) return;
249
+ const { publicDir } = resolvedConfig;
250
+ this.emitFile({
251
+ type: "asset",
252
+ fileName: "_headers",
253
+ source: staticAssetHeaders({
254
+ ownHeaders: readOwnHeaders(publicDir),
255
+ unhashedFiles: [...unhashedEntries(bundle), ...publicFilesInHashedDirs(publicDir)]
256
+ })
257
+ });
229
258
  },
230
259
  async config(_config, env) {
231
260
  const iconIds = await generateIconTypes(resolve(_config.root ?? process.cwd()), options.icons);
@@ -238,6 +267,14 @@ function camox(options) {
238
267
  if (isBuild && !localAuth) throw new Error("Camox: not authenticated. Run `npx camox login` before building the site.");
239
268
  }
240
269
  return {
270
+ ...isBuild && {
271
+ base: "./",
272
+ experimental: { renderBuiltUrl(filename, { ssr, hostType }) {
273
+ if (hostType === "css") return posix.relative("assets", filename);
274
+ if (!ssr) return { relative: true };
275
+ return { runtime: serverAssetUrl(filename) };
276
+ } }
277
+ },
241
278
  build: { emitAssets: true },
242
279
  ..._config.builder?.buildApp ? {} : { builder: { buildApp: buildClientThenWorker } },
243
280
  define: {
@@ -248,13 +285,15 @@ function camox(options) {
248
285
  __CAMOX_DEVELOPMENT_DRAFT__: JSON.stringify(!isRelease),
249
286
  __CAMOX_API_URL__: JSON.stringify(apiUrl),
250
287
  __CAMOX_AUTHENTICATION_URL__: JSON.stringify(authenticationUrl),
251
- __CAMOX_PROJECT_ID__: JSON.stringify(projectUuid)
288
+ __CAMOX_PROJECT_ID__: JSON.stringify(projectUuid),
289
+ __CAMOX_BUILD_ID__: JSON.stringify(randomUUID())
252
290
  },
253
291
  environments: { client: { build: { rollupOptions: {
254
292
  onLog: withoutExpectedCamoxLogs(findClientOnLog(_config)),
255
293
  input: {
256
294
  "camox-page-client": VIRTUAL_PAGE_CLIENT,
257
- "camox-studio-client": VIRTUAL_STUDIO_CLIENT
295
+ "camox-studio-client": VIRTUAL_STUDIO_CLIENT,
296
+ "camox-renderer-client": VIRTUAL_RENDERER_CLIENT
258
297
  },
259
298
  output: {
260
299
  assetFileNames: "assets/[name]-[hash][extname]",
@@ -286,6 +325,7 @@ function camox(options) {
286
325
  },
287
326
  configResolved(config) {
288
327
  resolvedConfig = config;
328
+ if (process.env["CAMOX_INTERNAL_HOSTED"] === "1") hostedProject = readHostedProject(config.root);
289
329
  if (!config.plugins.some((plugin) => plugin.name.startsWith("vite-plugin-cloudflare"))) throw new Error("Camox: Camox apps run on Cloudflare Workers. Add `cloudflare()` from `@cloudflare/vite-plugin` to your Vite plugins, and point `main` in `wrangler.jsonc` at `camox/worker`.");
290
330
  const routesDir = resolve(config.root, "src/routes");
291
331
  writeRuntimeSidecar(config.root, {
@@ -1,7 +1,7 @@
1
1
  import { useProjectUuid } from "../lib/auth.js";
2
- import { useCamoxApp } from "../features/provider/components/CamoxAppContext.js";
3
2
  import { reads, useReadQuery } from "../lib/studio-reads.js";
4
3
  import { linkablePages, useLinkablePages } from "../lib/link-pages.js";
4
+ import { useRoutableLayouts } from "../lib/studio-definitions.js";
5
5
  import { getPageDestinations } from "../core/pageDestinations.js";
6
6
  import { c } from "react/compiler-runtime";
7
7
 
@@ -9,7 +9,7 @@ import { c } from "react/compiler-runtime";
9
9
  /** The pages of the locale viewed, which the studio navigates to. */
10
10
  function usePageDestinations() {
11
11
  const $ = c(11);
12
- const app = useCamoxApp();
12
+ const layouts = useRoutableLayouts();
13
13
  const projectUuid = useProjectUuid();
14
14
  let t0;
15
15
  if ($[0] !== projectUuid) {
@@ -39,9 +39,9 @@ function usePageDestinations() {
39
39
  $[7] = t4;
40
40
  } else t4 = $[7];
41
41
  let t5;
42
- if ($[8] !== app || $[9] !== t4) {
43
- t5 = getPageDestinations(t4, app.getLayouts());
44
- $[8] = app;
42
+ if ($[8] !== layouts || $[9] !== t4) {
43
+ t5 = getPageDestinations(t4, layouts);
44
+ $[8] = layouts;
45
45
  $[9] = t4;
46
46
  $[10] = t5;
47
47
  } else t5 = $[10];
@@ -53,7 +53,7 @@ function usePageDestinations() {
53
53
  */
54
54
  function useLinkDestinations() {
55
55
  const $ = c(7);
56
- const app = useCamoxApp();
56
+ const layouts = useRoutableLayouts();
57
57
  let t0;
58
58
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
59
59
  t0 = { fetchProject: true };
@@ -61,16 +61,16 @@ function useLinkDestinations() {
61
61
  } else t0 = $[0];
62
62
  const { pages, fallbackPages } = useLinkablePages(useProjectUuid(), t0);
63
63
  let t1;
64
- if ($[1] !== app || $[2] !== fallbackPages || $[3] !== pages) {
64
+ if ($[1] !== fallbackPages || $[2] !== layouts || $[3] !== pages) {
65
65
  let t2;
66
66
  if ($[5] !== pages) {
67
67
  t2 = pages ?? [];
68
68
  $[5] = pages;
69
69
  $[6] = t2;
70
70
  } else t2 = $[6];
71
- t1 = getPageDestinations(linkablePages(t2, fallbackPages).map(_temp), app.getLayouts());
72
- $[1] = app;
73
- $[2] = fallbackPages;
71
+ t1 = getPageDestinations(linkablePages(t2, fallbackPages).map(_temp), layouts);
72
+ $[1] = fallbackPages;
73
+ $[2] = layouts;
74
74
  $[3] = pages;
75
75
  $[4] = t1;
76
76
  } else t1 = $[4];
@@ -1,4 +1,4 @@
1
- import { Router } from "@camox/api-contract";
1
+ import { Router } from "@camox/api-contract/router";
2
2
  import { RouterClient } from "@orpc/server";
3
3
  //#region src/lib/api-client-server.d.ts
4
4
  export type ServerApiClient = RouterClient<Router>;
@@ -1,5 +1,6 @@
1
+ import "./locale-path.js";
1
2
  import "./draft-scope.js";
2
- import { Router } from "@camox/api-contract";
3
+ import { Router } from "@camox/api-contract/router";
3
4
  import { RouterClient } from "@orpc/server";
4
5
  //#region src/lib/api-client.d.ts
5
6
  export type ApiClient = RouterClient<Router>;
@@ -1,3 +1,4 @@
1
+ import "./locale-path.js";
1
2
  import "../features/routes/pageRuntime.js";
2
3
  import "react";
3
4
  import { EndedDraft } from "@camox/api-contract/drafts";
@@ -1,14 +1,5 @@
1
+ import { LocaleView } from "@camox/api-contract/locales";
1
2
  //#region src/lib/locale-path.d.ts
2
- /**
3
- * The URL names the locale: a non-default locale's paths sit under its prefix (`/de/preise`), the
4
- * default locale's have none. The studio's routes follow the same rule (`/de/camox/content`).
5
- * Pure, so the runtime and the browser share it.
6
- */
7
- /** A locale of a project, as `locales.list` returns it. */
8
- export type LocaleView = {
9
- tag: string;
10
- isDefault: boolean;
11
- };
12
3
  /**
13
4
  * Where a page sits in one locale it exists in: the locale's tag and the page's path there, under
14
5
  * the locale's prefix. The runtime finds them for `hreflang` links and the sitemap; the
@@ -19,4 +10,5 @@ export interface AlternatePath {
19
10
  /** The page's runtime path, with its locale's prefix and without the runtime base path. */
20
11
  path: string;
21
12
  }
22
- //#endregion
13
+ //#endregion
14
+ export type { LocaleView };
@@ -1,10 +1,11 @@
1
1
  import { LOCALE_HEADER } from "@camox/api-contract/drafts";
2
+ import { localePrefix, parseLocalePath } from "@camox/api-contract/locales";
2
3
 
3
4
  //#region src/lib/locale-path.ts
4
5
  /**
5
6
  * The URL names the locale: a non-default locale's paths sit under its prefix (`/de/preise`), the
6
7
  * default locale's have none. The studio's routes follow the same rule (`/de/camox/content`).
7
- * Pure, so the runtime and the browser share it.
8
+ * Pure, so the runtime and the browser share it, and the API reads paths by the same rule.
8
9
  */
9
10
  /**
10
11
  * The default locale where nothing says its tag yet: before a render shows a locale, outside the
@@ -21,31 +22,6 @@ const UNNAMED_DEFAULT_LOCALE = {
21
22
  function localeHeaders(locale) {
22
23
  return locale && !locale.isDefault ? { [LOCALE_HEADER]: locale.tag } : {};
23
24
  }
24
- /** A locale's URL prefix: its tag in lowercase. The default locale has none. */
25
- function localePrefix(locale) {
26
- return locale.isDefault ? "" : `/${locale.tag.toLowerCase()}`;
27
- }
28
- /**
29
- * The locale a runtime path names, and the path after its prefix, without search or hash. A first
30
- * segment that isn't a non-default locale's prefix leaves the path the default locale's. A
31
- * locale's home is `/de`; `/de/` names it too.
32
- */
33
- function parseLocalePath(pathname, locales) {
34
- const path = pathname.replace(/[?#].*$/s, "") || "/";
35
- const end = path.indexOf("/", 1);
36
- const segment = end === -1 ? path.slice(1) : path.slice(1, end);
37
- const named = locales.find((locale) => !locale.isDefault && localePrefix(locale) === `/${segment}`);
38
- if (named) return {
39
- locale: named,
40
- path: end === -1 ? "/" : path.slice(end)
41
- };
42
- const fallback = locales.find((locale) => locale.isDefault);
43
- if (!fallback) throw new Error("A project's locales include its default locale.");
44
- return {
45
- locale: fallback,
46
- path
47
- };
48
- }
49
25
  /** A locale-relative path under its locale's prefix, keeping its search and hash: `/` is `/de`. */
50
26
  function localizedPath(path, locale) {
51
27
  const prefix = localePrefix(locale);
@@ -54,4 +30,4 @@ function localizedPath(path, locale) {
54
30
  }
55
31
 
56
32
  //#endregion
57
- export { UNNAMED_DEFAULT_LOCALE, localeHeaders, localePrefix, localizedPath, parseLocalePath };
33
+ export { UNNAMED_DEFAULT_LOCALE, localeHeaders, localizedPath, parseLocalePath };
@@ -25,6 +25,8 @@ async function sendFaviconChange(path, init) {
25
25
  return res;
26
26
  }
27
27
  const projectMutations = {
28
+ update: () => getOrpc().projects.update.mutationOptions(),
29
+ updateAnalytics: () => getOrpc().projects.updateAnalytics.mutationOptions(),
28
30
  updateSocialProfiles: () => getOrpc().projects.updateSocialProfiles.mutationOptions(),
29
31
  uploadFavicon: () => ({ mutationFn: async ({ projectId, file }) => {
30
32
  const formData = new FormData();
@@ -41,7 +43,11 @@ const projectMutations = {
41
43
  };
42
44
  const draftMutations = {
43
45
  publish: () => getOrpc().drafts.publish.mutationOptions(),
44
- discard: () => getOrpc().drafts.discard.mutationOptions()
46
+ discard: () => getOrpc().drafts.discard.mutationOptions(),
47
+ /** Requests the draft's overview again right away, once it failed. */
48
+ retryOverview: () => getOrpc().drafts.retryOverview.mutationOptions(),
49
+ /** Drops the draft's copy of an entity it conflicts on, to use Live's version instead. */
50
+ revertToLive: () => getOrpc().drafts.revertToLive.mutationOptions()
45
51
  };
46
52
  /** Adding and removing locales: content writes, so on Live they start a draft (ADR 0015). */
47
53
  const localeMutations = {
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+
3
+ //#region src/lib/revisions.ts
4
+ function createRevisions() {
5
+ const listeners = /* @__PURE__ */ new Set();
6
+ return {
7
+ announce(revision) {
8
+ for (const listener of listeners) listener(revision);
9
+ },
10
+ subscribe(listener) {
11
+ listeners.add(listener);
12
+ return () => listeners.delete(listener);
13
+ }
14
+ };
15
+ }
16
+ /** The studio's announced revisions. The signed-in studio provides it. */
17
+ const RevisionsContext = React.createContext(null);
18
+
19
+ //#endregion
20
+ export { RevisionsContext, createRevisions };
@@ -0,0 +1,29 @@
1
+ import { PlatformLabel } from "../components/PlatformLabel.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { Kbd } from "@camox/ui/kbd";
4
+ import { formatForDisplay } from "@tanstack/react-hotkeys";
5
+
6
+ //#region src/lib/shortcuts.tsx
7
+ /**
8
+ * Formats a shortcut as a keyboard shortcut display component, labelled for the user's platform.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * formatShortcut("Mod+E") // ⌘ E on Mac, Ctrl E elsewhere
13
+ * formatShortcut("L") // L
14
+ * ```
15
+ */
16
+ function formatShortcut(shortcut) {
17
+ if (!shortcut) return null;
18
+ const label = (platform) => formatForDisplay(shortcut, {
19
+ platform,
20
+ separatorToken: " "
21
+ });
22
+ return /* @__PURE__ */ jsx(Kbd, { children: /* @__PURE__ */ jsx(PlatformLabel, {
23
+ mac: label("mac"),
24
+ other: label("windows")
25
+ }) });
26
+ }
27
+
28
+ //#endregion
29
+ export { formatShortcut };