camox 0.58.1 → 0.60.2

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 (102) hide show
  1. package/dist/core/createBlock.js +1 -1
  2. package/dist/core/editing/createEditableBlock.js +2 -2
  3. package/dist/core/lib/recordScope.js +1 -1
  4. package/dist/features/canvas/CamoxCanvas.js +2 -2
  5. package/dist/features/canvas/CanvasOverlays.js +1 -1
  6. package/dist/features/canvas/sidebar/LayoutPageSeo.js +1 -1
  7. package/dist/features/canvas/sidebar/PageInspector.js +1 -1
  8. package/dist/features/content/CamoxContent.js +1 -1
  9. package/dist/features/content/components/MissingFromLocale.js +1 -1
  10. package/dist/features/page/CuratedPageContent.js +1 -1
  11. package/dist/features/preview/CamoxPreview.js +3 -3
  12. package/dist/features/preview/components/AddBlockSheet.js +1 -1
  13. package/dist/features/preview/components/AssetLightbox.js +1 -1
  14. package/dist/features/preview/components/BlockActionsPopover.js +1 -1
  15. package/dist/features/preview/components/CreatePageModal.js +2 -2
  16. package/dist/features/preview/components/FieldOverlayStyles.js +1 -1
  17. package/dist/features/preview/components/MobilePreviewDrawer.js +1 -1
  18. package/dist/features/preview/components/Overlays.js +1 -1
  19. package/dist/features/preview/components/PageInfo.js +1 -1
  20. package/dist/features/preview/components/PagePicker.js +1 -1
  21. package/dist/features/preview/components/PageTree.js +1 -1
  22. package/dist/features/preview/components/PreviewPanel.js +1 -1
  23. package/dist/features/preview/components/PreviewToolbar.js +1 -1
  24. package/dist/features/preview/components/SelectionEditor.js +2 -2
  25. package/dist/features/preview/components/StudioEditingFallback.js +1 -1
  26. package/dist/features/provider/AuthenticatedCamoxProvider.js +1 -1
  27. package/dist/features/provider/CamoxProvider.js +1 -1
  28. package/dist/features/provider/CoreCamoxProvider.js +3 -3
  29. package/dist/features/provider/components/CommandPalette.js +1 -1
  30. package/dist/features/renderer/RendererApp.js +187 -103
  31. package/dist/features/renderer/rendererHost.js +3 -7
  32. package/dist/features/routes/pageRuntime.d.ts +2 -5
  33. package/dist/features/routes/pageRuntime.js +1 -2
  34. package/dist/features/runtime/RuntimeChrome.js +1 -1
  35. package/dist/features/runtime/loopbackFetch.d.ts +8 -0
  36. package/dist/features/runtime/navigationTarget.js +1 -2
  37. package/dist/features/runtime/pageApp.js +1 -1
  38. package/dist/features/runtime/publicCache.d.ts +21 -0
  39. package/dist/features/runtime/publishingDraft.d.ts +4 -0
  40. package/dist/features/runtime/rendererClient.js +1 -1
  41. package/dist/features/runtime/runtime.d.ts +0 -5
  42. package/dist/features/runtime/runtime.js +80 -33
  43. package/dist/features/runtime/worker.js +4 -7
  44. package/dist/features/runtime/workerRequest.d.ts +8 -0
  45. package/dist/features/runtime/workerRuntime.d.ts +5 -0
  46. package/dist/features/runtime/workerRuntime.js +6 -0
  47. package/dist/features/sdkBundle/buildSdkBundle.d.ts +40 -0
  48. package/dist/features/sdkBundle/buildSdkBundle.js +323 -0
  49. package/dist/features/sdkBundle/localSdkBundle.d.ts +44 -0
  50. package/dist/features/sdkBundle/localSdkBundle.js +162 -0
  51. package/dist/features/sdkBundle/sdkBundle.d.ts +3 -0
  52. package/dist/features/sdkBundle/sdkBundle.js +4 -0
  53. package/dist/features/sdkBundle/sdkBundleName.d.ts +59 -0
  54. package/dist/features/sdkBundle/sdkBundleName.js +164 -0
  55. package/dist/features/sdkBundle/specifiers.d.ts +34 -0
  56. package/dist/features/sdkBundle/specifiers.js +146 -0
  57. package/dist/features/settings/FaviconSettings.js +1 -1
  58. package/dist/features/settings/SocialProfilesSettings.js +284 -151
  59. package/dist/features/studio/CamoxStudio.js +1 -1
  60. package/dist/features/studio/FollowAgentProvider.js +72 -13
  61. package/dist/features/studio/components/ChatWidget.js +86 -44
  62. package/dist/features/studio/components/DraftsControl.js +232 -201
  63. package/dist/features/studio/components/LocalePicker.js +2 -2
  64. package/dist/features/studio/components/Navbar.js +3 -3
  65. package/dist/features/studio/components/Patrol.js +312 -209
  66. package/dist/features/studio/components/ProjectMenu.js +2 -2
  67. package/dist/features/studio/components/ReviewDraftSheet.js +9 -15
  68. package/dist/features/studio/components/Squareling.js +164 -66
  69. package/dist/features/studio/components/ThreadComposer.js +139 -69
  70. package/dist/features/studio/components/ThreadMessages.js +216 -64
  71. package/dist/features/studio/components/UserButton.js +2 -2
  72. package/dist/features/studio/draftLabel.js +1 -2
  73. package/dist/features/studio/draftNotice.js +0 -1
  74. package/dist/features/studio/editorWhereContext.js +11 -0
  75. package/dist/features/studio/showInCanvas.js +1 -1
  76. package/dist/features/studio/useEditorSelection.js +135 -0
  77. package/dist/features/vite/definitionsSync.js +14 -60
  78. package/dist/features/vite/devSignIn.js +1 -1
  79. package/dist/features/vite/hostedChecks.js +1 -1
  80. package/dist/features/vite/runtimeDev.js +2 -2
  81. package/dist/features/vite/sdkBundleBuild.js +74 -0
  82. package/dist/features/vite/staticAssetHeaders.js +7 -4
  83. package/dist/features/vite/vite.d.ts +8 -7
  84. package/dist/features/vite/vite.js +67 -28
  85. package/dist/lib/api-client-server.js +25 -8
  86. package/dist/lib/api-client.js +2 -6
  87. package/dist/lib/auth.js +25 -59
  88. package/dist/lib/draft-scope.js +5 -25
  89. package/dist/lib/preview-handoff.js +1 -1
  90. package/dist/lib/server-auth-cookie.js +23 -11
  91. package/dist/lib/stored-auth-cookies.js +76 -0
  92. package/dist/lib/studio-reads.js +1 -1
  93. package/dist/lib/use-thread.js +40 -40
  94. package/dist/package.js +1 -1
  95. package/dist/studio.css +1 -1
  96. package/package.json +17 -4
  97. package/skills/camox/references/block-definitions.md +1 -1
  98. package/dist/features/vite/publishSeededContent.js +0 -30
  99. package/skills/camox-cli/SKILL.md +0 -15
  100. package/skills/camox-cli/references/cli-common.md +0 -58
  101. package/skills/camox-cli/references/cli-content.md +0 -181
  102. package/skills/camox-cli/references/cli-publishing.md +0 -26
@@ -1,32 +1,14 @@
1
1
  import { createServerApiClient } from "../../lib/api-client-server.js";
2
- import { DEVELOPMENT_DRAFT } from "@camox/api-contract/drafts";
3
2
  import { isRunnableDevEnvironment } from "vite-plus";
4
3
 
5
4
  //#region src/features/vite/definitionsSync.ts
6
5
  const SYNC_DEBOUNCE_DELAY_MS = 100;
7
- /**
8
- * The client definitions sync with. A release (deploy token) syncs Live's definitions; dev mode
9
- * sends the developer's session and syncs their development draft, which its first sync
10
- * creates (ADR 0006).
11
- */
12
- function createSyncClient(options) {
13
- const draftId = options.deployToken ? void 0 : DEVELOPMENT_DRAFT;
14
- return createServerApiClient(options.apiUrl, options.authToken, draftId);
15
- }
16
6
  function throwIfSyncAuthError(error) {
17
7
  if (error instanceof Error && error.name === "ORPCError" && error.message.toLowerCase().includes("unauthorized")) throw new Error("[camox] Definition sync failed: invalid or missing credentials.");
18
8
  }
19
9
  function isNotFoundError(error) {
20
10
  return error instanceof Error && error.name === "ORPCError" && error.message.toLowerCase().includes("not found");
21
11
  }
22
- /**
23
- * A sync that may not create the development draft finds nothing to write to: a release names a
24
- * project that doesn't exist, or dev mode has no development draft yet (ADR 0006).
25
- */
26
- function throwSyncTargetNotFoundError(projectUuid, deployToken) {
27
- if (deployToken) throw new Error(`[camox] No project has the Project ID "${projectUuid}". Check the camox() projectId.`);
28
- throw new Error(`[camox] You have no development draft in the project with Project ID "${projectUuid}" yet. Run the dev server while authenticated to create it first.`);
29
- }
30
12
  /** `count` of `noun`, pluralized: "1 block", "2 blocks". */
31
13
  function counted(count, noun) {
32
14
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
@@ -36,26 +18,25 @@ function quoted(names) {
36
18
  return names.map((name) => `"${name}"`).join(", ");
37
19
  }
38
20
  /**
39
- * Send a Definition Set to the API: Live's for a release, the development draft's in dev mode.
40
- * It goes in one `definitions.sync` call, which either fully applies it or changes nothing.
21
+ * Release a Definition Set to Live with the Deploy Token, adding the content it declares that
22
+ * Live lacks. It goes in one `definitions.sync` call, which either fully applies it or changes
23
+ * nothing.
41
24
  */
42
25
  async function syncDefinitionsToApi(options) {
43
- const { definitionSet, projectUuid, autoCreate, deployToken, logger } = options;
44
- const client = createSyncClient(options);
26
+ const { definitionSet, projectUuid, apiUrl, deployToken, logger } = options;
27
+ const client = createServerApiClient(apiUrl);
45
28
  let result;
46
29
  try {
47
30
  result = await client.definitions.sync({
48
31
  projectUuid,
49
32
  deployToken,
50
- autoCreate,
51
33
  definitionSet
52
34
  });
53
35
  } catch (error) {
54
36
  throwIfSyncAuthError(error);
55
- if (!autoCreate && isNotFoundError(error)) throwSyncTargetNotFoundError(projectUuid, deployToken);
37
+ if (isNotFoundError(error)) throw new Error(`[camox] No project has the Project ID "${projectUuid}". Check the camox() projectId.`);
56
38
  throw error;
57
39
  }
58
- if (result.draftCreated) logger.info("[camox] Created your development draft", { timestamp: true });
59
40
  const definitionCount = result.blocks.count + result.collections.count + (result.layouts?.count ?? 0);
60
41
  logger.info(`[camox] Synced ${counted(definitionCount, "Camox definition")}`, { timestamp: true });
61
42
  const { deletedDefinitionTypes } = result.blocks;
@@ -130,53 +111,29 @@ function isAppDependency(server, file) {
130
111
  function isScriptFile(file) {
131
112
  return /\.[cm]?[jt]sx?$/.test(file) && !file.endsWith(".d.ts");
132
113
  }
133
- /** Whether a project's Live has no pages, as on a fresh database. */
134
- async function liveHasNoPages(apiUrl, projectUuid) {
135
- const { pages } = await createServerApiClient(apiUrl).pages.listBySlug({ projectUuid });
136
- return pages.length === 0;
137
- }
138
114
  /**
139
- * Sync the app's whole Definition Set to the development draft now, then again whenever a module
140
- * the app depends on changes (debounced), skipping a sync when the set equals the last one sent
141
- * (ADR 0006). Resolves once the initial sync settles: `synced` if definitions reached the API,
142
- * and `releasedToLive` if it first released them to an empty Live (`releaseToEmptyLive`).
115
+ * Release the app's whole Definition Set to Live now, then again whenever a module the app depends
116
+ * on changes (debounced), skipping a release when the set equals the last one sent (ADR 0027).
117
+ * Resolves once the initial release settles: `synced` if definitions reached the API.
143
118
  */
144
119
  async function syncDefinitions(server, options) {
145
- const { projectUuid, apiUrl, autoCreate, authToken, deployToken, releaseToEmptyLive } = options;
146
120
  const logger = server.config.logger;
147
- /** What the development draft last received, so an unchanged set isn't sent again. */
121
+ /** What Live last received, so an unchanged set isn't sent again. */
148
122
  let lastSynced;
149
- let releasedToLive = false;
150
- async function sync({ initial = false } = {}) {
123
+ async function sync() {
151
124
  const definitionSet = await loadDefinitionSet(server);
152
125
  const serialized = serializeDefinitionSet(definitionSet);
153
126
  if (serialized === lastSynced) return;
154
- if (initial && releaseToEmptyLive && await liveHasNoPages(apiUrl, projectUuid)) {
155
- logger.info("[camox] Live is empty: releasing your definitions to it", { timestamp: true });
156
- await syncDefinitionsToApi({
157
- definitionSet,
158
- projectUuid,
159
- apiUrl,
160
- autoCreate: false,
161
- deployToken: releaseToEmptyLive,
162
- logger
163
- });
164
- releasedToLive = true;
165
- }
166
127
  await syncDefinitionsToApi({
128
+ ...options,
167
129
  definitionSet,
168
- projectUuid,
169
- apiUrl,
170
- autoCreate,
171
- authToken,
172
- deployToken,
173
130
  logger
174
131
  });
175
132
  lastSynced = serialized;
176
133
  }
177
134
  let synced = false;
178
135
  try {
179
- await sync({ initial: true });
136
+ await sync();
180
137
  synced = true;
181
138
  } catch (error) {
182
139
  logger.error(`[camox] Failed to sync Camox definitions: ${String(error)}`, { timestamp: true });
@@ -200,10 +157,7 @@ async function syncDefinitions(server, options) {
200
157
  server.watcher.on("unlink", (file) => {
201
158
  if (isScriptFile(file) || isAppDependency(server, file)) scheduleSync();
202
159
  });
203
- return {
204
- synced,
205
- releasedToLive
206
- };
160
+ return { synced };
207
161
  }
208
162
 
209
163
  //#endregion
@@ -58,7 +58,7 @@ function installDevSignInLink(server, options) {
58
58
  }
59
59
  }
60
60
  if (controller.signal.aborted) return;
61
- server.config.logger.warn("Camox sign-in link unavailable. Use the normal local URL to sign in, or run camox preview for a fresh link.");
61
+ server.config.logger.warn("Camox sign-in link unavailable. Use the normal local URL to sign in, or restart the dev server for a fresh link.");
62
62
  }
63
63
  async function openSignInLink(url) {
64
64
  let warned = false;
@@ -6,7 +6,7 @@ import { Visitor, parseSync } from "vite-plus";
6
6
  //#region src/features/vite/hostedChecks.ts
7
7
  /**
8
8
  * Set by Camox's revision and release sandboxes: a hosted project's build checks the code an
9
- * agent may have written (ADR 0021). `apps/landing` and dev mode never set it.
9
+ * agent may have written (ADR 0021). Dev mode never sets it.
10
10
  */
11
11
  const HOSTED_BUILD_ENV = "CAMOX_INTERNAL_HOSTED";
12
12
  const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
@@ -73,7 +73,8 @@ function resolveCamoxSourceImport(id) {
73
73
  "camox/_internal/rendererClient": resolve(sdkRoot, "src/features/runtime/rendererClient.tsx"),
74
74
  "camox/_internal/studioServer": resolve(sdkRoot, "src/features/runtime/studioServer.tsx"),
75
75
  "camox/_internal/runtime": resolve(sdkRoot, "src/features/runtime/runtime.ts"),
76
- "camox/worker": resolve(sdkRoot, "src/features/runtime/worker.ts")
76
+ "camox/worker": resolve(sdkRoot, "src/features/runtime/worker.ts"),
77
+ "camox/_internal/workerRuntime": resolve(sdkRoot, "src/features/runtime/workerRuntime.ts")
77
78
  }[id];
78
79
  if (!sourceImport || !existsSync(sourceImport)) return;
79
80
  return sourceImport;
@@ -231,7 +232,6 @@ const options = {
231
232
  pageClientEntryUrl,
232
233
  studioClientEntryUrl,
233
234
  rendererClientEntryUrl,
234
- developmentDraft: __CAMOX_DEVELOPMENT_DRAFT__,
235
235
  getCamoxApp: async () => camoxApp,
236
236
  getDocument: async () => camoxDocument,
237
237
  // Loaded by the first page image, so the stylesheet stays out of the Worker's main chunk.
@@ -0,0 +1,74 @@
1
+ import { sdkBundleEntryFile } from "../sdkBundle/specifiers.js";
2
+ import { SDK_BUNDLE_BUILD_MANIFEST, sdkBundlePath } from "../sdkBundle/sdkBundleName.js";
3
+ import { SDK_BUNDLE_MANIFEST } from "../sdkBundle/buildSdkBundle.js";
4
+ import { buildsAgainstSdkBundle, ensureLocalSdkBundle } from "../sdkBundle/localSdkBundle.js";
5
+ import { createRequire } from "node:module";
6
+ import { readFileSync, readdirSync } from "node:fs";
7
+ import { dirname, join, posix, resolve } from "node:path";
8
+
9
+ //#region src/features/vite/sdkBundleBuild.ts
10
+ /**
11
+ * The SDK Bundle a build builds against, from the local cache, or undefined when the switch is
12
+ * off and the build is whole-app.
13
+ */
14
+ async function sdkBundleBuildState({ root, settings, mayBuild }) {
15
+ if (!buildsAgainstSdkBundle()) return;
16
+ let built = false;
17
+ const bundle = await ensureLocalSdkBundle({
18
+ root,
19
+ settings,
20
+ mayBuild,
21
+ onBuild: () => {
22
+ built = true;
23
+ }
24
+ });
25
+ const names = {
26
+ browser: bundle.browser.name,
27
+ workerd: bundle.workerd.name
28
+ };
29
+ const stylesheet = readdirSync(bundle.browser.dir).find((file) => /^camox-studio-.*\.css$/.test(file));
30
+ if (!stylesheet) throw new Error(`Camox: the SDK Bundle ${names.browser} has no Studio stylesheet.`);
31
+ const websiteRequire = createRequire(join(root, "package.json"));
32
+ /** What each target holds: its entry files by specifier, and by the file a path specifier names. */
33
+ const held = (dir) => {
34
+ const { entries } = JSON.parse(readFileSync(join(dir, SDK_BUNDLE_MANIFEST), "utf8"));
35
+ const byFile = /* @__PURE__ */ new Map();
36
+ for (const specifier of Object.keys(entries)) if (/\.[cm]?js$/.test(specifier)) byFile.set(websiteRequire.resolve(specifier), sdkBundleEntryFile(specifier));
37
+ return {
38
+ entries,
39
+ byFile
40
+ };
41
+ };
42
+ const holds = {
43
+ browser: held(bundle.browser.dir),
44
+ workerd: held(bundle.workerd.dir)
45
+ };
46
+ return {
47
+ names,
48
+ built,
49
+ studioStylesheetUrl: sdkBundlePath(names.browser) + stylesheet,
50
+ manifest: {
51
+ fileName: SDK_BUNDLE_BUILD_MANIFEST,
52
+ source: `${JSON.stringify(names)}\n`
53
+ },
54
+ resolve(id, importer, consumer) {
55
+ const target = consumer === "client" ? "browser" : "workerd";
56
+ const { entries, byFile } = holds[target];
57
+ const file = Object.hasOwn(entries, id) ? entries[id] : id.startsWith(".") && importer ? byFile.get(resolve(dirname(importer), id)) : void 0;
58
+ if (!file) return;
59
+ return {
60
+ id: sdkBundlePath(names[target]) + file,
61
+ external: "absolute"
62
+ };
63
+ },
64
+ relativizeWorkerImports(code, fileName) {
65
+ const prefix = sdkBundlePath(names.workerd);
66
+ if (!code.includes(prefix)) return;
67
+ const toRoot = posix.relative(posix.dirname(fileName), "") || ".";
68
+ return code.replaceAll(new RegExp(`(["'\`])${prefix.replaceAll("/", "\\/")}`, "g"), `$1${toRoot}${prefix}`);
69
+ }
70
+ };
71
+ }
72
+
73
+ //#endregion
74
+ export { sdkBundleBuildState };
@@ -5,17 +5,20 @@ import { join, relative } from "node:path";
5
5
  /** Where the client build puts the files it names by their content's hash. */
6
6
  const HASHED_DIRS = ["assets", "_chunks"];
7
7
  const IMMUTABLE = "public, max-age=31536000, immutable";
8
+ /** Where a build against an SDK Bundle serves its files, under the bundle's name (ADR 0029). */
9
+ const SDK_BUNDLE_DIR = "_camox/sdk";
8
10
  /** Workers Static Assets reads at most this many rules. */
9
11
  const MAX_RULES = 100;
10
12
  /**
11
13
  * The `_headers` file Workers Static Assets serves the client build with. Files under
12
14
  * `/assets/` and `/_chunks/` are named by their content's hash, so browsers keep them for good;
13
15
  * 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
+ * app's public directory puts there, keep the default, which revalidates. A build against an
17
+ * SDK Bundle has its files under `/_camox/sdk/<name>/`, which never change under a name either.
18
+ * The app's own `_headers` comes first, as it wrote it.
16
19
  */
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`)];
20
+ function staticAssetHeaders({ ownHeaders, unhashedFiles, sdkBundle = false }) {
21
+ const rules = [...(sdkBundle ? [...HASHED_DIRS, SDK_BUNDLE_DIR] : HASHED_DIRS).map((dir) => `/${dir}/*\n Cache-Control: ${IMMUTABLE}`), ...[...unhashedFiles].sort().map((file) => `/${file}\n ! Cache-Control`)];
19
22
  const own = ownHeaders?.trimEnd();
20
23
  const ownRules = own ? own.split("\n").filter((line) => /^\S/.test(line) && !line.startsWith("#")) : [];
21
24
  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.`);
@@ -3,7 +3,11 @@ import { Logger, Plugin } from "vite-plus";
3
3
  //#region src/features/vite/vite.d.ts
4
4
  /** What `_internal.afterSync` receives: an API client authenticated as the dev user. */
5
5
  export interface CamoxAfterSyncContext {
6
- /** Sends the dev session and reads and writes the developer's development draft. */
6
+ /**
7
+ * Sends the dev session. Its first content write starts a regular draft, as an edit in the
8
+ * studio does, and it reads and writes that draft from then on. Until then it reads Live, so
9
+ * seeding that finds its content starts nothing.
10
+ */
7
11
  client: ServerApiClient;
8
12
  /** The project's Project ID, which API inputs name `projectUuid`. */
9
13
  projectUuid: string;
@@ -25,8 +29,6 @@ export interface CamoxPluginOptions {
25
29
  apiUrl?: string;
26
30
  /** URL of the Camox authentication backend (default: https://app.camox.ai) */
27
31
  authenticationUrl?: string;
28
- /** Show Tanstack query devtools (default: false) */
29
- enableTanstackDevtools?: boolean;
30
32
  /** Enable experimental features (default: false). */
31
33
  enableExperimentalFeatures?: boolean;
32
34
  /** Disable automatic code generation (route and layout files) (default: false) */
@@ -45,11 +47,10 @@ export interface CamoxPluginOptions {
45
47
  */
46
48
  afterSync?: (context: CamoxAfterSyncContext) => void | Promise<void>;
47
49
  /**
48
- * Dev-only: a deploy token for seeding Live. When Live has no pages yet, the first sync
49
- * releases the local definitions to Live, and the development draft is published after
50
- * `afterSync`, so seeded content becomes Live instead of the draft's changes.
50
+ * Dev-only: the local API's Deploy Token. Dev mode runs only against a local API with it,
51
+ * and releases the app's definitions to local Live on every save (ADR 0027).
51
52
  */
52
- seedLiveDeployToken?: string;
53
+ localDeployToken?: string;
53
54
  };
54
55
  }
55
56
  export declare const CAMOX_OPTIMIZED_DEPS: string[];
@@ -10,14 +10,13 @@ import { installDevSignInLink } from "./devSignIn.js";
10
10
  import { HOSTED_BUILD_ENV, forbiddenImport, misnamedBlock, readHostedProject } from "./hostedChecks.js";
11
11
  import { generateIconTypes } from "./iconGeneration.js";
12
12
  import { generateLayoutFiles, watchLayoutFiles } from "./layoutGeneration.js";
13
+ import { sdkBundleBuildState } from "./sdkBundleBuild.js";
13
14
  import { publicFilesInHashedDirs, readOwnHeaders, staticAssetHeaders, unhashedEntries } from "./staticAssetHeaders.js";
14
15
  import { buildClientThenWorker } from "./workerBuild.js";
15
16
  import { VIRTUAL_CAMOX_APP, syncDefinitions, syncDefinitionsToApi } from "./definitionsSync.js";
16
- import { publishSeededContent } from "./publishSeededContent.js";
17
17
  import { cleanupGeneratedRouteFiles } from "./routeGeneration.js";
18
18
  import { RESOLVED_VIRTUAL_DEV_HTML, createRuntimeRegistryPlugin, loadRuntimeDevModule, resolveRuntimeDevId } from "./runtimeDev.js";
19
- import { projectIdSchema } from "@camox/api-contract/ids";
20
- import { DEVELOPMENT_DRAFT } from "@camox/api-contract/drafts";
19
+ import { generateUuidv7, projectIdSchema } from "@camox/api-contract/ids";
21
20
  import { createHash, randomUUID } from "node:crypto";
22
21
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
22
  import { dirname, join, posix, resolve } from "node:path";
@@ -62,8 +61,7 @@ function findClientOnLog(config) {
62
61
  * CLI can pick up the same Project ID / apiUrl / authenticationUrl the
63
62
  * plugin actually used. The CLI treats the vite config as the source of
64
63
  * truth for those values — there is no other reliable way to recover them
65
- * from outside vite. No draft is written here: the CLI always works in the
66
- * developer's development draft (ADR 0010).
64
+ * from outside vite.
67
65
  */
68
66
  function writeRuntimeSidecar(root, data) {
69
67
  const dir = join(root, "node_modules", ".camox");
@@ -170,11 +168,24 @@ function projectIdOf(options) {
170
168
  if (!projectIdSchema.safeParse(options.projectId).success) throw new Error(`Camox: ${JSON.stringify(options.projectId)} is not a Project ID. Copy your project's Project ID, a lowercase UUIDv7, from the Camox dashboard into your camox() config.`);
171
169
  return options.projectId;
172
170
  }
171
+ /** Whether an API URL is on this machine, as each checkout's own local API is. */
172
+ function isLocalApi(apiUrl) {
173
+ const { hostname } = new URL(apiUrl);
174
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
175
+ }
176
+ /**
177
+ * The Deploy Token dev mode releases with, refused when it's missing or the API isn't local:
178
+ * dev mode exists for Camox contributors, and a hosted project's code never comes from a machine.
179
+ */
180
+ function localDeployTokenOf(apiUrl, options) {
181
+ const token = options._internal?.localDeployToken;
182
+ if (token && isLocalApi(apiUrl)) return token;
183
+ throw new Error("Camox: dev mode only runs against a local Camox API, with its internal Deploy Token. A hosted project's code reaches Camox through drafts and Upgrades.");
184
+ }
173
185
  function camox(options) {
174
186
  const projectUuid = projectIdOf(options);
175
187
  const apiUrl = options._internal?.apiUrl ?? PRODUCTION_API_URL;
176
188
  const authenticationUrl = options._internal?.authenticationUrl ?? DEFAULT_AUTHENTICATION_URL;
177
- const enableTanstackDevtools = options._internal?.enableTanstackDevtools ?? false;
178
189
  const disableCodeGen = options._internal?.disableCodeGen ?? false;
179
190
  const runtimeBasePath = options._internal?.runtimeBasePath;
180
191
  let isBuild = false;
@@ -185,8 +196,12 @@ function camox(options) {
185
196
  let hostedProject;
186
197
  let resolvedConfig;
187
198
  let localAuth = null;
199
+ /** Set in dev mode, which releases to local Live with it. */
200
+ let localDeployToken;
188
201
  let blockChunks = {};
189
202
  let devServer;
203
+ /** Set when the build builds against an SDK Bundle. */
204
+ let sdkBundle;
190
205
  return {
191
206
  name: "camox",
192
207
  enforce: "pre",
@@ -196,6 +211,10 @@ function camox(options) {
196
211
  const forbidden = forbiddenImport(hostedProject, id, importer);
197
212
  if (forbidden) this.error(forbidden);
198
213
  }
214
+ if (sdkBundle) {
215
+ const external = sdkBundle.resolve(id, importer, this.environment.config.consumer);
216
+ if (external) return external;
217
+ }
199
218
  const runtimeId = resolveRuntimeDevId(id, importer);
200
219
  if (runtimeId) return runtimeId;
201
220
  if (id === VIRTUAL_STUDIO_CSS) return RESOLVED_VIRTUAL_STUDIO_CSS;
@@ -203,6 +222,7 @@ function camox(options) {
203
222
  },
204
223
  async load(id, loadOptions) {
205
224
  const stylesDir = resolve(sdkRoot, !isBuild && isSdkSource ? ".dev" : "dist");
225
+ if (id === RESOLVED_VIRTUAL_STUDIO_CSS && sdkBundle) return `export default ${JSON.stringify(sdkBundle.studioStylesheetUrl)};`;
206
226
  if (id === RESOLVED_VIRTUAL_STUDIO_CSS) {
207
227
  const cssPath = resolve(stylesDir, "studio.css");
208
228
  if (isBuild) {
@@ -244,7 +264,19 @@ function camox(options) {
244
264
  }
245
265
  return transformArtifactRuntime(code, id);
246
266
  },
267
+ renderChunk(code, chunk) {
268
+ if (!sdkBundle || this.environment.config.consumer !== "server") return;
269
+ const relative = sdkBundle.relativizeWorkerImports(code, chunk.fileName);
270
+ return relative === void 0 ? void 0 : {
271
+ code: relative,
272
+ map: null
273
+ };
274
+ },
247
275
  generateBundle(_outputOptions, bundle) {
276
+ if (sdkBundle && this.environment.config.consumer === "server") this.emitFile({
277
+ type: "asset",
278
+ ...sdkBundle.manifest
279
+ });
248
280
  if (this.environment.name !== "client") return;
249
281
  blockChunks = collectBlockChunks(bundle, resolvedConfig.root);
250
282
  if (!isBuild) return;
@@ -254,7 +286,8 @@ function camox(options) {
254
286
  fileName: "_headers",
255
287
  source: staticAssetHeaders({
256
288
  ownHeaders: readOwnHeaders(publicDir),
257
- unhashedFiles: [...unhashedEntries(bundle), ...publicFilesInHashedDirs(publicDir)]
289
+ unhashedFiles: [...unhashedEntries(bundle), ...publicFilesInHashedDirs(publicDir)],
290
+ sdkBundle: sdkBundle !== void 0
258
291
  })
259
292
  });
260
293
  },
@@ -264,10 +297,18 @@ function camox(options) {
264
297
  const release = process.env.CAMOX_INTERNAL_RELEASE;
265
298
  isRelease = isBuild && (release === "1" || release === "dry-run");
266
299
  isDryRun = isRelease && release === "dry-run";
267
- if (!isRelease) {
268
- localAuth = readAuthTokenForUrl(authenticationUrl);
269
- if (isBuild && !localAuth) throw new Error("Camox: not authenticated. Run `npx camox login` before building the site.");
270
- }
300
+ if (env.command === "serve" && !env.isPreview) localDeployToken = localDeployTokenOf(apiUrl, options);
301
+ if (!isBuild) localAuth = readAuthTokenForUrl(authenticationUrl);
302
+ if (isBuild) sdkBundle = await sdkBundleBuildState({
303
+ root: resolve(_config.root ?? process.cwd()),
304
+ settings: {
305
+ iconIds,
306
+ telemetryDisabled: !!options.disableTelemetry,
307
+ experimentalFeatures: options._internal?.enableExperimentalFeatures ?? false,
308
+ apiUrl
309
+ },
310
+ mayBuild: !(isRelease && process.env["CAMOX_INTERNAL_HOSTED"] === "1")
311
+ });
271
312
  return {
272
313
  ...isBuild && {
273
314
  base: "./",
@@ -277,14 +318,15 @@ function camox(options) {
277
318
  return { runtime: serverAssetUrl(filename) };
278
319
  } }
279
320
  },
280
- build: { emitAssets: true },
321
+ build: {
322
+ emitAssets: true,
323
+ rolldownOptions: { experimental: { lazyBarrel: true } }
324
+ },
281
325
  ..._config.builder?.buildApp ? {} : { builder: { buildApp: buildClientThenWorker } },
282
326
  define: {
283
327
  __CAMOX_ICON_IDS__: JSON.stringify(iconIds),
284
328
  __CAMOX_TELEMETRY_DISABLED__: JSON.stringify(!!options.disableTelemetry),
285
- __ENABLE_TANSTACK_DEVTOOLS__: JSON.stringify(enableTanstackDevtools),
286
329
  __CAMOX_ENABLE_EXPERIMENTAL_FEATURES__: JSON.stringify(options._internal?.enableExperimentalFeatures ?? false),
287
- __CAMOX_DEVELOPMENT_DRAFT__: JSON.stringify(!isRelease),
288
330
  __CAMOX_API_URL__: JSON.stringify(apiUrl),
289
331
  __CAMOX_AUTHENTICATION_URL__: JSON.stringify(authenticationUrl),
290
332
  __CAMOX_PROJECT_ID__: JSON.stringify(projectUuid),
@@ -342,11 +384,13 @@ function camox(options) {
342
384
  }
343
385
  if (disableCodeGen) config.logger.warn("⚠️ Code generation is disabled (_internal.disableCodeGen). This option is only meant for momentary debugging — do not deploy or commit your app with it enabled.", { timestamp: true });
344
386
  const mode = config.command === "serve" ? "Running" : "Building";
345
- let target = "your development draft";
387
+ let target = "Live, not released";
346
388
  if (isDryRun) target = "Live, dry run";
347
389
  else if (isRelease) target = "Live";
348
- else if (!localAuth) target = "authentication required";
390
+ else if (localDeployToken) target = "local Live";
391
+ else if (!isBuild && !localAuth) target = "authentication required";
349
392
  config.logger.info(`${mode} Camox app (${target})`, { timestamp: true });
393
+ if (sdkBundle?.built) config.logger.info(`Built the SDK Bundle ${sdkBundle.names.browser} (browser) and ${sdkBundle.names.workerd} (workerd) into its cache`, { timestamp: true });
350
394
  },
351
395
  configureServer(server) {
352
396
  devServer = server;
@@ -369,28 +413,24 @@ function camox(options) {
369
413
  watchNewBlockFiles(server);
370
414
  }
371
415
  server.httpServer?.once("listening", () => {
372
- if (!localAuth) return;
373
- const authToken = localAuth.token;
416
+ if (!localDeployToken) return;
374
417
  syncDefinitions(server, {
375
418
  projectUuid,
376
419
  apiUrl,
377
- autoCreate: true,
378
- authToken,
379
- releaseToEmptyLive: options._internal?.seedLiveDeployToken
380
- }).then(async ({ synced, releasedToLive }) => {
381
- if (!synced) return;
420
+ deployToken: localDeployToken
421
+ }).then(async ({ synced }) => {
422
+ const afterSync = options._internal?.afterSync;
423
+ if (!synced || !afterSync || !localAuth) return;
382
424
  const context = {
383
- client: createServerApiClient(apiUrl, authToken, DEVELOPMENT_DRAFT),
425
+ client: createServerApiClient(apiUrl, localAuth.token, generateUuidv7()),
384
426
  projectUuid,
385
427
  logger: server.config.logger
386
428
  };
387
- const afterSync = options._internal?.afterSync;
388
- if (afterSync) try {
429
+ try {
389
430
  await afterSync(context);
390
431
  } catch (error) {
391
432
  context.logger.warn(`[camox] afterSync failed: ${String(error)}`, { timestamp: true });
392
433
  }
393
- if (releasedToLive) await publishSeededContent(context);
394
434
  });
395
435
  });
396
436
  },
@@ -414,7 +454,6 @@ function camox(options) {
414
454
  definitionSet: await camoxModule.camoxApp.getDefinitionSet(),
415
455
  projectUuid,
416
456
  apiUrl,
417
- autoCreate: false,
418
457
  deployToken,
419
458
  logger: resolvedConfig.logger
420
459
  });
@@ -1,19 +1,36 @@
1
- import { DRAFT_HEADER } from "@camox/api-contract/drafts";
1
+ import { DRAFT_CREATE_HEADER, DRAFT_HEADER, isContentWrite } from "@camox/api-contract/drafts";
2
2
  import { createORPCClient } from "@orpc/client";
3
3
  import { RPCLink } from "@orpc/client/fetch";
4
4
 
5
5
  //#region src/lib/api-client-server.ts
6
6
  /**
7
- * An API client for the plugin's own requests. `draftId` names the draft they read and write:
8
- * dev mode sends `development`, and a release names none, so it writes Live.
7
+ * An API client for the plugin's own requests. A release names no draft, so it writes Live. With
8
+ * `startsDraft`, its first content write starts that draft, as the studio's first write on Live
9
+ * does: reads see Live until a write has started it, and the draft from then on.
9
10
  */
10
- function createServerApiClient(apiUrl, authToken, draftId) {
11
- const headers = {};
12
- if (authToken) headers.Authorization = `Bearer ${authToken}`;
13
- if (draftId) headers[DRAFT_HEADER] = draftId;
11
+ function createServerApiClient(apiUrl, authToken, startsDraft) {
12
+ const base = {};
13
+ if (authToken) base.Authorization = `Bearer ${authToken}`;
14
+ let started = false;
14
15
  const link = new RPCLink({
15
16
  url: `${apiUrl}/rpc`,
16
- headers
17
+ headers: (_options, path) => {
18
+ if (!startsDraft) return base;
19
+ if (isContentWrite(path)) return {
20
+ ...base,
21
+ [DRAFT_HEADER]: startsDraft,
22
+ [DRAFT_CREATE_HEADER]: "1"
23
+ };
24
+ return started ? {
25
+ ...base,
26
+ [DRAFT_HEADER]: startsDraft
27
+ } : base;
28
+ },
29
+ fetch: async (request, init, _options, path) => {
30
+ const response = await fetch(request, init);
31
+ if (response.ok && isContentWrite(path)) started = true;
32
+ return response;
33
+ }
17
34
  });
18
35
  return createORPCClient(link);
19
36
  }
@@ -1,5 +1,5 @@
1
- import { getAuthCookieHeader, getAuthRequestCredentials } from "./auth.js";
2
1
  import { localeHeaders } from "./locale-path.js";
2
+ import { getAuthCookieHeader, getAuthRequestCredentials } from "./auth.js";
3
3
  import { DRAFT_HEADER, isContentWrite } from "@camox/api-contract/drafts";
4
4
  import { createORPCClient } from "@orpc/client";
5
5
  import { RPCLink } from "@orpc/client/fetch";
@@ -61,10 +61,6 @@ function createStudioClient(apiUrl, draftHeaders) {
61
61
  });
62
62
  return createORPCClient(link);
63
63
  }
64
- function getApiClient() {
65
- if (!_client) throw new Error("API client not initialized — call initApiClient first");
66
- return _client;
67
- }
68
64
  function getOrpc() {
69
65
  if (!_orpc) throw new Error("API client not initialized — call initApiClient first");
70
66
  return _orpc;
@@ -83,4 +79,4 @@ function getContentWriteHeaders() {
83
79
  }
84
80
 
85
81
  //#endregion
86
- export { getApiClient, getApiUrl, getContentWriteHeaders, getDraftReadClient, getOrpc, initApiClient };
82
+ export { getApiUrl, getContentWriteHeaders, getDraftReadClient, getOrpc, initApiClient };