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.
- package/dist/core/createBlock.js +1 -1
- package/dist/core/editing/createEditableBlock.js +2 -2
- package/dist/core/lib/recordScope.js +1 -1
- package/dist/features/canvas/CamoxCanvas.js +2 -2
- package/dist/features/canvas/CanvasOverlays.js +1 -1
- package/dist/features/canvas/sidebar/LayoutPageSeo.js +1 -1
- package/dist/features/canvas/sidebar/PageInspector.js +1 -1
- package/dist/features/content/CamoxContent.js +1 -1
- package/dist/features/content/components/MissingFromLocale.js +1 -1
- package/dist/features/page/CuratedPageContent.js +1 -1
- package/dist/features/preview/CamoxPreview.js +3 -3
- package/dist/features/preview/components/AddBlockSheet.js +1 -1
- package/dist/features/preview/components/AssetLightbox.js +1 -1
- package/dist/features/preview/components/BlockActionsPopover.js +1 -1
- package/dist/features/preview/components/CreatePageModal.js +2 -2
- package/dist/features/preview/components/FieldOverlayStyles.js +1 -1
- package/dist/features/preview/components/MobilePreviewDrawer.js +1 -1
- package/dist/features/preview/components/Overlays.js +1 -1
- package/dist/features/preview/components/PageInfo.js +1 -1
- package/dist/features/preview/components/PagePicker.js +1 -1
- package/dist/features/preview/components/PageTree.js +1 -1
- package/dist/features/preview/components/PreviewPanel.js +1 -1
- package/dist/features/preview/components/PreviewToolbar.js +1 -1
- package/dist/features/preview/components/SelectionEditor.js +2 -2
- package/dist/features/preview/components/StudioEditingFallback.js +1 -1
- package/dist/features/provider/AuthenticatedCamoxProvider.js +1 -1
- package/dist/features/provider/CamoxProvider.js +1 -1
- package/dist/features/provider/CoreCamoxProvider.js +3 -3
- package/dist/features/provider/components/CommandPalette.js +1 -1
- package/dist/features/renderer/RendererApp.js +187 -103
- package/dist/features/renderer/rendererHost.js +3 -7
- package/dist/features/routes/pageRuntime.d.ts +2 -5
- package/dist/features/routes/pageRuntime.js +1 -2
- package/dist/features/runtime/RuntimeChrome.js +1 -1
- package/dist/features/runtime/loopbackFetch.d.ts +8 -0
- package/dist/features/runtime/navigationTarget.js +1 -2
- package/dist/features/runtime/pageApp.js +1 -1
- package/dist/features/runtime/publicCache.d.ts +21 -0
- package/dist/features/runtime/publishingDraft.d.ts +4 -0
- package/dist/features/runtime/rendererClient.js +1 -1
- package/dist/features/runtime/runtime.d.ts +0 -5
- package/dist/features/runtime/runtime.js +80 -33
- package/dist/features/runtime/worker.js +4 -7
- package/dist/features/runtime/workerRequest.d.ts +8 -0
- package/dist/features/runtime/workerRuntime.d.ts +5 -0
- package/dist/features/runtime/workerRuntime.js +6 -0
- package/dist/features/sdkBundle/buildSdkBundle.d.ts +40 -0
- package/dist/features/sdkBundle/buildSdkBundle.js +323 -0
- package/dist/features/sdkBundle/localSdkBundle.d.ts +44 -0
- package/dist/features/sdkBundle/localSdkBundle.js +162 -0
- package/dist/features/sdkBundle/sdkBundle.d.ts +3 -0
- package/dist/features/sdkBundle/sdkBundle.js +4 -0
- package/dist/features/sdkBundle/sdkBundleName.d.ts +59 -0
- package/dist/features/sdkBundle/sdkBundleName.js +164 -0
- package/dist/features/sdkBundle/specifiers.d.ts +34 -0
- package/dist/features/sdkBundle/specifiers.js +146 -0
- package/dist/features/settings/FaviconSettings.js +1 -1
- package/dist/features/settings/SocialProfilesSettings.js +284 -151
- package/dist/features/studio/CamoxStudio.js +1 -1
- package/dist/features/studio/FollowAgentProvider.js +72 -13
- package/dist/features/studio/components/ChatWidget.js +86 -44
- package/dist/features/studio/components/DraftsControl.js +232 -201
- package/dist/features/studio/components/LocalePicker.js +2 -2
- package/dist/features/studio/components/Navbar.js +3 -3
- package/dist/features/studio/components/Patrol.js +312 -209
- package/dist/features/studio/components/ProjectMenu.js +2 -2
- package/dist/features/studio/components/ReviewDraftSheet.js +9 -15
- package/dist/features/studio/components/Squareling.js +164 -66
- package/dist/features/studio/components/ThreadComposer.js +139 -69
- package/dist/features/studio/components/ThreadMessages.js +216 -64
- package/dist/features/studio/components/UserButton.js +2 -2
- package/dist/features/studio/draftLabel.js +1 -2
- package/dist/features/studio/draftNotice.js +0 -1
- package/dist/features/studio/editorWhereContext.js +11 -0
- package/dist/features/studio/showInCanvas.js +1 -1
- package/dist/features/studio/useEditorSelection.js +135 -0
- package/dist/features/vite/definitionsSync.js +14 -60
- package/dist/features/vite/devSignIn.js +1 -1
- package/dist/features/vite/hostedChecks.js +1 -1
- package/dist/features/vite/runtimeDev.js +2 -2
- package/dist/features/vite/sdkBundleBuild.js +74 -0
- package/dist/features/vite/staticAssetHeaders.js +7 -4
- package/dist/features/vite/vite.d.ts +8 -7
- package/dist/features/vite/vite.js +67 -28
- package/dist/lib/api-client-server.js +25 -8
- package/dist/lib/api-client.js +2 -6
- package/dist/lib/auth.js +25 -59
- package/dist/lib/draft-scope.js +5 -25
- package/dist/lib/preview-handoff.js +1 -1
- package/dist/lib/server-auth-cookie.js +23 -11
- package/dist/lib/stored-auth-cookies.js +76 -0
- package/dist/lib/studio-reads.js +1 -1
- package/dist/lib/use-thread.js +40 -40
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +17 -4
- package/skills/camox/references/block-definitions.md +1 -1
- package/dist/features/vite/publishSeededContent.js +0 -30
- package/skills/camox-cli/SKILL.md +0 -15
- package/skills/camox-cli/references/cli-common.md +0 -58
- package/skills/camox-cli/references/cli-content.md +0 -181
- 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
|
-
*
|
|
40
|
-
* It goes in one `definitions.sync` call, which either fully applies it or changes
|
|
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,
|
|
44
|
-
const client =
|
|
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 (
|
|
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
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
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
|
|
121
|
+
/** What Live last received, so an unchanged set isn't sent again. */
|
|
148
122
|
let lastSynced;
|
|
149
|
-
|
|
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(
|
|
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
|
|
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).
|
|
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.
|
|
15
|
-
*
|
|
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
|
-
/**
|
|
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:
|
|
49
|
-
* releases the
|
|
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
|
-
|
|
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.
|
|
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 (!
|
|
268
|
-
|
|
269
|
-
|
|
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: {
|
|
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 = "
|
|
387
|
+
let target = "Live, not released";
|
|
346
388
|
if (isDryRun) target = "Live, dry run";
|
|
347
389
|
else if (isRelease) target = "Live";
|
|
348
|
-
else if (
|
|
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 (!
|
|
373
|
-
const authToken = localAuth.token;
|
|
416
|
+
if (!localDeployToken) return;
|
|
374
417
|
syncDefinitions(server, {
|
|
375
418
|
projectUuid,
|
|
376
419
|
apiUrl,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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,
|
|
425
|
+
client: createServerApiClient(apiUrl, localAuth.token, generateUuidv7()),
|
|
384
426
|
projectUuid,
|
|
385
427
|
logger: server.config.logger
|
|
386
428
|
};
|
|
387
|
-
|
|
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.
|
|
8
|
-
*
|
|
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,
|
|
11
|
-
const
|
|
12
|
-
if (authToken)
|
|
13
|
-
|
|
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
|
}
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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 {
|
|
82
|
+
export { getApiUrl, getContentWriteHeaders, getDraftReadClient, getOrpc, initApiClient };
|