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
@@ -0,0 +1,323 @@
1
+ import { SDK_BUNDLE_TARGETS, loadsAtFirstPaint, sdkBundleEntryFile } from "./specifiers.js";
2
+ import { SDK_BUNDLE_BUILDER_VERSION, sdkBundleName, sdkBundlePath, sdkBundleSpecifiersFor } from "./sdkBundleName.js";
3
+ import { createRequire } from "node:module";
4
+ import { createHash } from "node:crypto";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
7
+ import { build } from "vite-plus";
8
+ import { readFile, writeFile } from "node:fs/promises";
9
+
10
+ //#region src/features/sdkBundle/buildSdkBundle.ts
11
+ /**
12
+ * Builds a website's SDK Bundle: Camox, React and what they reach, once for the browser and once
13
+ * for workerd, from the packages the website installed. Runs in Node, from the website's folder.
14
+ */
15
+ /** The manifest each target's folder holds, beside its files. */
16
+ const SDK_BUNDLE_MANIFEST = "sdk-bundle.json";
17
+ /** The lockfile a website installed from, and the website's importer in it. */
18
+ async function readSdkBundleLockfile(root) {
19
+ const website = resolve(root);
20
+ for (let dir = website;; dir = dirname(dir)) {
21
+ const file = join(dir, "pnpm-lock.yaml");
22
+ if (existsSync(file)) {
23
+ const importer = relative(dir, website).split(sep).join("/") || ".";
24
+ return {
25
+ lockfile: await readFile(file, "utf8"),
26
+ importer
27
+ };
28
+ }
29
+ if (dirname(dir) === dir) throw new Error(`Camox: no pnpm-lock.yaml in ${website} or any folder above it.`);
30
+ }
31
+ }
32
+ /** The names of the website's SDK Bundle, per target, from its lockfile and settings. */
33
+ async function sdkBundleNamesFor({ root, settings }) {
34
+ const { lockfile, importer } = await readSdkBundleLockfile(root);
35
+ const [browser, workerd] = await Promise.all(SDK_BUNDLE_TARGETS.map((target) => sdkBundleName({
36
+ lockfile,
37
+ importer,
38
+ target,
39
+ settings
40
+ })));
41
+ return {
42
+ browser,
43
+ workerd
44
+ };
45
+ }
46
+ /**
47
+ * Builds the SDK Bundle of the website in `root`, with its installed packages, into
48
+ * `<outDir>/browser` and `<outDir>/workerd`. Each folder is served or loaded as is, under its own
49
+ * name's path.
50
+ */
51
+ async function buildSdkBundle({ root, outDir, settings }) {
52
+ const names = await sdkBundleNamesFor({
53
+ root,
54
+ settings
55
+ });
56
+ const { lockfile, importer } = await readSdkBundleLockfile(root);
57
+ const result = {};
58
+ for (const target of SDK_BUNDLE_TARGETS) {
59
+ const dir = join(outDir, target);
60
+ const specifiers = sdkBundleSpecifiersFor({
61
+ lockfile,
62
+ importer,
63
+ target
64
+ });
65
+ const entries = await buildTarget({
66
+ root: resolve(root),
67
+ dir,
68
+ target,
69
+ specifiers,
70
+ names,
71
+ settings
72
+ });
73
+ const manifest = {
74
+ name: names[target],
75
+ target,
76
+ builderVersion: "2",
77
+ entries
78
+ };
79
+ await writeFile(join(dir, SDK_BUNDLE_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`);
80
+ result[target] = {
81
+ name: names[target],
82
+ dir
83
+ };
84
+ }
85
+ return result;
86
+ }
87
+ const ENTRY_PREFIX = "\0camox-sdk-bundle-entry:";
88
+ const VIRTUAL_STUDIO_CSS = "virtual:camox-studio-css";
89
+ const VIRTUAL_OVERLAY_CSS = "virtual:camox-overlay-css";
90
+ /** The conditions the Cloudflare plugin resolves the Worker's packages with. */
91
+ const WORKERD_CONDITIONS = [
92
+ "workerd",
93
+ "worker",
94
+ "module",
95
+ "browser",
96
+ "production"
97
+ ];
98
+ async function buildTarget({ root, dir, target, specifiers, names, settings }) {
99
+ const entryName = (specifier) => sdkBundleEntryFile(specifier).replace(/\.mjs$/, "");
100
+ const workerd = target === "workerd";
101
+ const config = {
102
+ configFile: false,
103
+ root,
104
+ mode: "production",
105
+ logLevel: "warn",
106
+ define: {
107
+ __CAMOX_ICON_IDS__: JSON.stringify(settings.iconIds),
108
+ __CAMOX_TELEMETRY_DISABLED__: JSON.stringify(settings.telemetryDisabled),
109
+ __CAMOX_ENABLE_EXPERIMENTAL_FEATURES__: JSON.stringify(settings.experimentalFeatures),
110
+ __CAMOX_API_URL__: JSON.stringify(settings.apiUrl),
111
+ "process.env.NODE_ENV": JSON.stringify("production")
112
+ },
113
+ plugins: [
114
+ entries(root),
115
+ camoxStylesheets({
116
+ root,
117
+ target,
118
+ browserName: names.browser
119
+ }),
120
+ ...workerd ? [wasmModules()] : []
121
+ ],
122
+ resolve: {
123
+ alias: [{
124
+ find: /^tslib$/,
125
+ replacement: "tslib/tslib.es6.mjs"
126
+ }],
127
+ ...workerd && { conditions: WORKERD_CONDITIONS }
128
+ },
129
+ ...workerd && { ssr: {
130
+ noExternal: true,
131
+ target: "webworker",
132
+ resolve: {
133
+ conditions: WORKERD_CONDITIONS,
134
+ externalConditions: WORKERD_CONDITIONS
135
+ }
136
+ } },
137
+ build: {
138
+ outDir: dir,
139
+ emptyOutDir: true,
140
+ ssr: workerd,
141
+ emitAssets: true,
142
+ target: workerd ? "esnext" : void 0,
143
+ minify: true,
144
+ sourcemap: false,
145
+ reportCompressedSize: false,
146
+ chunkSizeWarningLimit: Infinity,
147
+ modulePreload: false,
148
+ copyPublicDir: false,
149
+ rolldownOptions: {
150
+ input: Object.fromEntries(specifiers.map((specifier) => [entryName(specifier), `${ENTRY_PREFIX}${specifier}`])),
151
+ preserveEntrySignatures: "strict",
152
+ ...workerd && {
153
+ platform: "neutral",
154
+ external: [/^cloudflare:/, /^node:/]
155
+ },
156
+ onLog(level, log, handler) {
157
+ if (log.code === "MODULE_LEVEL_DIRECTIVE" || log.code === "EVAL") return;
158
+ handler(level, log);
159
+ },
160
+ output: {
161
+ entryFileNames: "[name].mjs",
162
+ chunkFileNames: (chunk) => !workerd && FIRST_PAINT_CHUNKS.has(chunk.name) ? "[name]-[hash].mjs" : "c-[hash].mjs",
163
+ assetFileNames: "[name]-[hash][extname]"
164
+ }
165
+ }
166
+ }
167
+ };
168
+ if (!workerd) {
169
+ const input = config.build.rolldownOptions.input;
170
+ const pageModules = await modulesLoadedStatically(config, Object.fromEntries(specifiers.filter(loadsAtFirstPaint).map((specifier) => [entryName(specifier), input[entryName(specifier)]])));
171
+ const output = config.build.rolldownOptions.output;
172
+ output.codeSplitting = { groups: [{
173
+ name: "react",
174
+ test: (id) => pageModules.has(id) && REACT_MODULE.test(id),
175
+ priority: 2
176
+ }, {
177
+ name: "page",
178
+ test: (id) => pageModules.has(id),
179
+ priority: 1
180
+ }] };
181
+ }
182
+ await build(config);
183
+ return Object.fromEntries(specifiers.map((specifier) => [specifier, sdkBundleEntryFile(specifier)]));
184
+ }
185
+ /**
186
+ * The chunks a page loads at first paint, beside the entry files it imports: its code, React, and
187
+ * the helpers Rolldown keeps in a chunk of their own.
188
+ */
189
+ const FIRST_PAINT_CHUNKS = /* @__PURE__ */ new Set([
190
+ "page",
191
+ "react",
192
+ "rolldown-runtime"
193
+ ]);
194
+ const REACT_MODULE = /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/;
195
+ /**
196
+ * The modules the browser loads when it imports `input`'s entries, without what they import
197
+ * lazily: a first build of those entries alone, so what they import but never use is left out,
198
+ * as their own build would.
199
+ */
200
+ async function modulesLoadedStatically(config, input) {
201
+ const result = await build({
202
+ ...config,
203
+ build: {
204
+ ...config.build,
205
+ write: false,
206
+ rolldownOptions: {
207
+ ...config.build.rolldownOptions,
208
+ input
209
+ }
210
+ }
211
+ });
212
+ const outputs = Array.isArray(result) ? result : [result];
213
+ const chunks = /* @__PURE__ */ new Map();
214
+ for (const { output } of outputs) for (const file of output) if (file.type === "chunk") chunks.set(file.fileName, file);
215
+ const loaded = /* @__PURE__ */ new Set();
216
+ const visit = (fileName) => {
217
+ if (loaded.has(fileName)) return;
218
+ loaded.add(fileName);
219
+ for (const imported of chunks.get(fileName)?.imports ?? []) visit(imported);
220
+ };
221
+ for (const chunk of chunks.values()) if (chunk.isEntry) visit(chunk.fileName);
222
+ const modules = /* @__PURE__ */ new Set();
223
+ for (const fileName of loaded) for (const [id, module] of Object.entries(chunks.get(fileName)?.modules ?? {})) if (module.renderedLength > 0) modules.add(id);
224
+ return modules;
225
+ }
226
+ /** One entry per specifier, exporting what it exports, CommonJS packages included. */
227
+ function entries(root) {
228
+ return {
229
+ name: "camox:sdk-bundle-entries",
230
+ enforce: "pre",
231
+ resolveId(id) {
232
+ if (id.startsWith(ENTRY_PREFIX)) return id;
233
+ },
234
+ async load(id) {
235
+ if (!id.startsWith(ENTRY_PREFIX)) return;
236
+ const specifier = id.slice(24);
237
+ const resolved = await this.resolve(specifier, join(root, "package.json"));
238
+ if (!resolved) this.error(`Camox: the website doesn't install ${specifier}.`);
239
+ const imported = JSON.stringify(specifier);
240
+ if (isCommonJs(resolved.id)) {
241
+ const names = Object.keys(createRequire(resolved.id)(resolved.id)).filter((name) => name !== "default" && name !== "__esModule" && /^[A-Za-z_$][\w$]*$/.test(name));
242
+ return [
243
+ `import * as module from ${imported};`,
244
+ "const exports = module.default ?? module;",
245
+ "export default exports;",
246
+ ...names.map((name) => `export const ${name} = exports.${name};`)
247
+ ].join("\n");
248
+ }
249
+ const source = readFileSync(resolved.id, "utf8");
250
+ return `export * from ${imported};\n${/\bexport\s+default\b|\bas\s+default\b/.test(source) ? `export { default } from ${imported};\n` : ""}`;
251
+ }
252
+ };
253
+ }
254
+ /** Whether Node would load `file` as CommonJS. */
255
+ function isCommonJs(file) {
256
+ const extension = extname(file);
257
+ if (extension === ".cjs") return true;
258
+ if (extension !== ".js") return false;
259
+ for (let dir = dirname(file); dirname(dir) !== dir; dir = dirname(dir)) {
260
+ const manifest = join(dir, "package.json");
261
+ if (existsSync(manifest)) return JSON.parse(readFileSync(manifest, "utf8")).type !== "module";
262
+ }
263
+ return true;
264
+ }
265
+ /**
266
+ * The Studio's stylesheets, which camox's modules import as virtual modules. The browser finds
267
+ * the Studio's from the module that asks, and the Worker from the browser bundle's path: the name
268
+ * is known before building, so no prefix is read at runtime.
269
+ */
270
+ function camoxStylesheets({ root, target, browserName }) {
271
+ const studioCss = createRequire(join(root, "package.json")).resolve("camox/studio.css");
272
+ const resolved = (id) => `\0${id}`;
273
+ return {
274
+ name: "camox:sdk-bundle-stylesheets",
275
+ enforce: "pre",
276
+ resolveId(id) {
277
+ if (id === VIRTUAL_STUDIO_CSS || id === VIRTUAL_OVERLAY_CSS) return resolved(id);
278
+ },
279
+ load(id) {
280
+ if (id === resolved(VIRTUAL_OVERLAY_CSS)) {
281
+ const css = readFileSync(join(dirname(studioCss), "studio-overlays.css"), "utf8");
282
+ return `export default ${JSON.stringify(css)};`;
283
+ }
284
+ if (id !== resolved(VIRTUAL_STUDIO_CSS)) return;
285
+ const css = readFileSync(studioCss, "utf8");
286
+ const fileName = `camox-studio-${createHash("sha256").update(css).digest("hex").slice(0, 8)}.css`;
287
+ if (target === "browser") {
288
+ this.emitFile({
289
+ type: "asset",
290
+ fileName,
291
+ source: css
292
+ });
293
+ return `export default new URL(/* @vite-ignore */ ${JSON.stringify(`./${fileName}`)}, import.meta.url).href;`;
294
+ }
295
+ return `export default ${JSON.stringify(sdkBundlePath(browserName) + fileName)};`;
296
+ }
297
+ };
298
+ }
299
+ /** WebAssembly the Worker imports, as modules of their own beside the bundle's. */
300
+ function wasmModules() {
301
+ return {
302
+ name: "camox:sdk-bundle-wasm",
303
+ enforce: "pre",
304
+ async resolveId(id, importer) {
305
+ if (!id.endsWith(".wasm")) return;
306
+ const resolved = await this.resolve(id, importer, { skipSelf: true });
307
+ if (!resolved) return;
308
+ const file = basename(resolved.id);
309
+ this.emitFile({
310
+ type: "asset",
311
+ fileName: file,
312
+ source: await readFile(resolved.id)
313
+ });
314
+ return {
315
+ id: `./${file}`,
316
+ external: true
317
+ };
318
+ }
319
+ };
320
+ }
321
+
322
+ //#endregion
323
+ export { SDK_BUNDLE_MANIFEST, buildSdkBundle, readSdkBundleLockfile, sdkBundleNamesFor };
@@ -0,0 +1,44 @@
1
+ import { SdkBundleNames, SdkBundleSettings } from "./sdkBundleName.js";
2
+ import { SdkBundleBuild } from "./buildSdkBundle.js";
3
+ //#region src/features/sdkBundle/localSdkBundle.d.ts
4
+ /** Turns building against an SDK Bundle on (`1`) or off (`0`) for `vite build`. */
5
+ export declare const SDK_BUNDLE_SWITCH_ENV = "CAMOX_INTERNAL_SDK_BUNDLE";
6
+ /** The folder SDK Bundles are cached in, when not the website's own. */
7
+ export declare const SDK_BUNDLE_CACHE_ENV = "CAMOX_INTERNAL_SDK_BUNDLE_CACHE";
8
+ /** Whether `vite build` builds against an SDK Bundle. */
9
+ export declare function buildsAgainstSdkBundle(env?: NodeJS.ProcessEnv): boolean;
10
+ /** What a hosted build fails with when its SDK Bundle isn't in the cache: it never builds one. */
11
+ export declare class SdkBundleMissingError extends Error {
12
+ readonly names: SdkBundleNames;
13
+ name: string;
14
+ readonly code = "CAMOX_SDK_BUNDLE_MISSING";
15
+ constructor(names: SdkBundleNames);
16
+ }
17
+ /**
18
+ * The folder a website's SDK Bundles are cached in, one folder per target's name:
19
+ * `CAMOX_INTERNAL_SDK_BUNDLE_CACHE`, or the website's `node_modules/.cache/camox/sdk-bundles`.
20
+ */
21
+ export declare function sdkBundleCacheDir(root: string, env?: NodeJS.ProcessEnv): string;
22
+ /**
23
+ * The website's SDK Bundle from the cache, built there first when it's missing and `mayBuild`.
24
+ * A hosted build passes `mayBuild: false`, and gets `SdkBundleMissingError` for a missing one.
25
+ */
26
+ export declare function ensureLocalSdkBundle({ root, settings, mayBuild, onBuild, env }: {
27
+ root: string;
28
+ settings: SdkBundleSettings;
29
+ mayBuild: boolean;
30
+ /** Called before a missing bundle is built. */
31
+ onBuild?: (names: SdkBundleNames) => void;
32
+ env?: NodeJS.ProcessEnv;
33
+ }): Promise<SdkBundleBuild>;
34
+ /**
35
+ * Adds the SDK Bundle a website's build names to its output, so it deploys on its own: the
36
+ * browser files under `/_camox/sdk/<name>/` in the static assets, and the workerd modules under
37
+ * `_camox/sdk/<name>/` beside the Worker's. A whole-app build is left as it is. Returns the names,
38
+ * or null for a whole-app build.
39
+ */
40
+ export declare function addSdkBundleToBuild({ root, env }: {
41
+ root: string;
42
+ env?: NodeJS.ProcessEnv;
43
+ }): Promise<SdkBundleNames | null>;
44
+ //#endregion
@@ -0,0 +1,162 @@
1
+ import { SDK_BUNDLE_TARGETS } from "./specifiers.js";
2
+ import { SDK_BUNDLE_BUILD_MANIFEST, sdkBundleMissingLine, sdkBundlePath } from "./sdkBundleName.js";
3
+ import { buildSdkBundle, sdkBundleNamesFor } from "./buildSdkBundle.js";
4
+ import { createHash } from "node:crypto";
5
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
6
+ import { dirname, join, relative, resolve, sep } from "node:path";
7
+ import { cp, mkdir, mkdtemp, readFile, rename, rm } from "node:fs/promises";
8
+
9
+ //#region src/features/sdkBundle/localSdkBundle.ts
10
+ /**
11
+ * A website's SDK Bundle on this machine: looked up in a local cache by name, and built there when
12
+ * it's missing, unless the build is a hosted one, which never builds one. Runs in Node.
13
+ */
14
+ /** Turns building against an SDK Bundle on (`1`) or off (`0`) for `vite build`. */
15
+ const SDK_BUNDLE_SWITCH_ENV = "CAMOX_INTERNAL_SDK_BUNDLE";
16
+ /** Off until websites build against an SDK Bundle by default (#670). */
17
+ const SDK_BUNDLE_SWITCH_DEFAULT = false;
18
+ /** The folder SDK Bundles are cached in, when not the website's own. */
19
+ const SDK_BUNDLE_CACHE_ENV = "CAMOX_INTERNAL_SDK_BUNDLE_CACHE";
20
+ /** Whether `vite build` builds against an SDK Bundle. */
21
+ function buildsAgainstSdkBundle(env = process.env) {
22
+ const value = env[SDK_BUNDLE_SWITCH_ENV];
23
+ if (value === "1") return true;
24
+ if (value === "0") return false;
25
+ return SDK_BUNDLE_SWITCH_DEFAULT;
26
+ }
27
+ /** What a hosted build fails with when its SDK Bundle isn't in the cache: it never builds one. */
28
+ var SdkBundleMissingError = class extends Error {
29
+ names;
30
+ name = "SdkBundleMissingError";
31
+ code = "CAMOX_SDK_BUNDLE_MISSING";
32
+ constructor(names) {
33
+ super(`Camox: this build's SDK Bundle isn't in its cache, and a hosted build never builds one.\n` + sdkBundleMissingLine(names));
34
+ this.names = names;
35
+ }
36
+ };
37
+ /**
38
+ * The folder a website's SDK Bundles are cached in, one folder per target's name:
39
+ * `CAMOX_INTERNAL_SDK_BUNDLE_CACHE`, or the website's `node_modules/.cache/camox/sdk-bundles`.
40
+ */
41
+ function sdkBundleCacheDir(root, env = process.env) {
42
+ const configured = env[SDK_BUNDLE_CACHE_ENV];
43
+ return configured ? resolve(configured) : join(resolve(root), "node_modules/.cache/camox/sdk-bundles");
44
+ }
45
+ /**
46
+ * Where the cache holds a target's bundle. A linked camox, as in Camox's monorepo, has no
47
+ * integrity in the lockfile, so its name stays the same when its code changes: the cache keys
48
+ * it by its built files too.
49
+ */
50
+ function cachedDir(cacheDir, name, salt) {
51
+ return join(cacheDir, salt ? `${name}-${salt}` : name);
52
+ }
53
+ /**
54
+ * A digest of the linked camox's built files, or null when camox is installed from the registry.
55
+ * Paths, sizes and modification times: enough to see a rebuild.
56
+ */
57
+ function linkedCamoxSalt(root) {
58
+ const installed = join(resolve(root), "node_modules", "camox");
59
+ if (!existsSync(installed)) return null;
60
+ const real = realpathSync(installed);
61
+ if (real.split(sep).includes("node_modules")) return null;
62
+ const dist = join(real, "dist");
63
+ const hash = createHash("sha256");
64
+ const walk = (dir) => {
65
+ for (const entry of readdirSync(dir).sort()) {
66
+ const path = join(dir, entry);
67
+ const stat = statSync(path);
68
+ if (stat.isDirectory()) walk(path);
69
+ else hash.update(`${relative(dist, path)} ${stat.size} ${stat.mtimeMs}\n`);
70
+ }
71
+ };
72
+ if (existsSync(dist)) walk(dist);
73
+ return hash.digest("hex").slice(0, 12);
74
+ }
75
+ /**
76
+ * The website's SDK Bundle from the cache, built there first when it's missing and `mayBuild`.
77
+ * A hosted build passes `mayBuild: false`, and gets `SdkBundleMissingError` for a missing one.
78
+ */
79
+ async function ensureLocalSdkBundle({ root, settings, mayBuild, onBuild, env = process.env }) {
80
+ const names = await sdkBundleNamesFor({
81
+ root,
82
+ settings
83
+ });
84
+ const cacheDir = sdkBundleCacheDir(root, env);
85
+ const salt = linkedCamoxSalt(root);
86
+ const cached = (target) => ({
87
+ name: names[target],
88
+ dir: cachedDir(cacheDir, names[target], salt)
89
+ });
90
+ const result = {
91
+ browser: cached("browser"),
92
+ workerd: cached("workerd")
93
+ };
94
+ if (SDK_BUNDLE_TARGETS.every((target) => existsSync(result[target].dir))) return result;
95
+ if (!mayBuild) throw new SdkBundleMissingError(names);
96
+ onBuild?.(names);
97
+ await mkdir(cacheDir, { recursive: true });
98
+ const staging = await mkdtemp(join(cacheDir, ".building-"));
99
+ try {
100
+ const built = await buildSdkBundle({
101
+ root,
102
+ outDir: staging,
103
+ settings
104
+ });
105
+ for (const target of SDK_BUNDLE_TARGETS) {
106
+ if (existsSync(result[target].dir)) continue;
107
+ await rename(built[target].dir, result[target].dir).catch((error) => {
108
+ if (!existsSync(result[target].dir)) throw error;
109
+ });
110
+ }
111
+ } finally {
112
+ await rm(staging, {
113
+ recursive: true,
114
+ force: true
115
+ });
116
+ }
117
+ return result;
118
+ }
119
+ /** Where the Cloudflare plugin's build put the Worker and its static assets. */
120
+ async function buildOutput(root) {
121
+ const deployDir = join(resolve(root), ".wrangler/deploy");
122
+ const { configPath } = JSON.parse(await readFile(join(deployDir, "config.json"), "utf8"));
123
+ const wranglerPath = resolve(deployDir, configPath);
124
+ const wrangler = JSON.parse(readFileSync(wranglerPath, "utf8"));
125
+ const workerDir = dirname(wranglerPath);
126
+ return {
127
+ workerDir,
128
+ clientDir: resolve(workerDir, wrangler.assets?.directory ?? "../client")
129
+ };
130
+ }
131
+ /**
132
+ * Adds the SDK Bundle a website's build names to its output, so it deploys on its own: the
133
+ * browser files under `/_camox/sdk/<name>/` in the static assets, and the workerd modules under
134
+ * `_camox/sdk/<name>/` beside the Worker's. A whole-app build is left as it is. Returns the names,
135
+ * or null for a whole-app build.
136
+ */
137
+ async function addSdkBundleToBuild({ root, env = process.env }) {
138
+ const { workerDir, clientDir } = await buildOutput(root);
139
+ const manifestPath = join(workerDir, SDK_BUNDLE_BUILD_MANIFEST);
140
+ if (!existsSync(manifestPath)) return null;
141
+ const names = JSON.parse(await readFile(manifestPath, "utf8"));
142
+ const cacheDir = sdkBundleCacheDir(root, env);
143
+ const salt = linkedCamoxSalt(root);
144
+ const destinations = {
145
+ browser: clientDir,
146
+ workerd: workerDir
147
+ };
148
+ for (const target of SDK_BUNDLE_TARGETS) {
149
+ const from = cachedDir(cacheDir, names[target], salt);
150
+ if (!existsSync(from)) throw new Error(`Camox: the build's SDK Bundle ${names[target]} isn't in ${cacheDir}.`);
151
+ const to = join(destinations[target], sdkBundlePath(names[target]).slice(1));
152
+ await rm(to, {
153
+ recursive: true,
154
+ force: true
155
+ });
156
+ await cp(from, to, { recursive: true });
157
+ }
158
+ return names;
159
+ }
160
+
161
+ //#endregion
162
+ export { SDK_BUNDLE_CACHE_ENV, SDK_BUNDLE_SWITCH_ENV, SdkBundleMissingError, addSdkBundleToBuild, buildsAgainstSdkBundle, ensureLocalSdkBundle, sdkBundleCacheDir };
@@ -0,0 +1,3 @@
1
+ import { BuiltSdkBundle, SDK_BUNDLE_MANIFEST, SdkBundleBuild, SdkBundleManifest, buildSdkBundle, readSdkBundleLockfile, sdkBundleNamesFor } from "./buildSdkBundle.js";
2
+ import { SDK_BUNDLE_CACHE_ENV, SDK_BUNDLE_SWITCH_ENV, SdkBundleMissingError, addSdkBundleToBuild, buildsAgainstSdkBundle, ensureLocalSdkBundle, sdkBundleCacheDir } from "./localSdkBundle.js";
3
+ export { BuiltSdkBundle, SDK_BUNDLE_CACHE_ENV, SDK_BUNDLE_MANIFEST, SDK_BUNDLE_SWITCH_ENV, SdkBundleBuild, SdkBundleManifest, SdkBundleMissingError, addSdkBundleToBuild, buildSdkBundle, buildsAgainstSdkBundle, ensureLocalSdkBundle, readSdkBundleLockfile, sdkBundleCacheDir, sdkBundleNamesFor };
@@ -0,0 +1,4 @@
1
+ import { SDK_BUNDLE_MANIFEST, buildSdkBundle, readSdkBundleLockfile, sdkBundleNamesFor } from "./buildSdkBundle.js";
2
+ import { SDK_BUNDLE_CACHE_ENV, SDK_BUNDLE_SWITCH_ENV, SdkBundleMissingError, addSdkBundleToBuild, buildsAgainstSdkBundle, ensureLocalSdkBundle, sdkBundleCacheDir } from "./localSdkBundle.js";
3
+
4
+ export { SDK_BUNDLE_CACHE_ENV, SDK_BUNDLE_MANIFEST, SDK_BUNDLE_SWITCH_ENV, SdkBundleMissingError, addSdkBundleToBuild, buildSdkBundle, buildsAgainstSdkBundle, ensureLocalSdkBundle, readSdkBundleLockfile, sdkBundleCacheDir, sdkBundleNamesFor };
@@ -0,0 +1,59 @@
1
+ import { LUCIDE_ICON_HELPER, SDK_BUNDLE_TARGETS, SdkBundleTarget, loadsAtFirstPaint, packageOfSpecifier, sdkBundleEntryFile, sdkBundleSpecifiers } from "./specifiers.js";
2
+ //#region src/features/sdkBundle/sdkBundleName.d.ts
3
+ /**
4
+ * The version of the code that builds SDK Bundles. Bump it whenever a change to the builder
5
+ * changes what a bundle holds for the same packages and settings.
6
+ */
7
+ export declare const SDK_BUNDLE_BUILDER_VERSION = "2";
8
+ /** Where a website's pages and Worker find an SDK Bundle's files: `/_camox/sdk/<name>/`. */
9
+ export declare function sdkBundlePath(name: string): string;
10
+ /** An SDK Bundle's name for each target: a website's build names both. */
11
+ export type SdkBundleNames = Record<SdkBundleTarget, string>;
12
+ /**
13
+ * The file a website's build names its SDK Bundle in, as `SdkBundleNames` JSON, in the server
14
+ * build's folder beside its `wrangler.json`. A build without it is whole-app.
15
+ */
16
+ export declare const SDK_BUNDLE_BUILD_MANIFEST = "camox-sdk-bundle.json";
17
+ /** What a hosted build whose SDK Bundle is missing fails with, followed by the names as JSON. */
18
+ export declare const SDK_BUNDLE_MISSING = "CAMOX_SDK_BUNDLE_MISSING";
19
+ /** The line a build that's missing its SDK Bundle prints its error with. */
20
+ export declare function sdkBundleMissingLine(names: SdkBundleNames): string;
21
+ /**
22
+ * The SDK Bundle a build's log says is missing, or null when the build failed for another reason:
23
+ * the caller ensures the bundle, then builds again.
24
+ */
25
+ export declare function sdkBundleMissingIn(log: string): SdkBundleNames | null;
26
+ /** The settings `camox()` compiles into an SDK Bundle. */
27
+ export interface SdkBundleSettings {
28
+ /** The icon ids of the collection the website picked. */
29
+ iconIds: readonly string[];
30
+ telemetryDisabled: boolean;
31
+ experimentalFeatures: boolean;
32
+ apiUrl: string;
33
+ }
34
+ export interface SdkBundleNameInputs {
35
+ /** The website's `pnpm-lock.yaml`. */
36
+ lockfile: string;
37
+ /** The website's folder, from the lockfile's: `.` unless the website is in a workspace. */
38
+ importer?: string;
39
+ target: SdkBundleTarget;
40
+ settings: SdkBundleSettings;
41
+ /** Defaults to the builder this SDK ships. */
42
+ builderVersion?: string;
43
+ }
44
+ /**
45
+ * A digest of the builder's version, the target, the settings and the lockfile's resolution
46
+ * (name, version, integrity) of every package the target's specifiers reach.
47
+ */
48
+ export declare function sdkBundleName({ lockfile, importer, target, settings, builderVersion }: SdkBundleNameInputs): Promise<string>;
49
+ /**
50
+ * The specifiers the SDK Bundle of the website at `importer` holds for `target`: the packages it
51
+ * shares with camox are in only when it installs them.
52
+ */
53
+ export declare function sdkBundleSpecifiersFor({ lockfile, importer, target }: {
54
+ lockfile: string;
55
+ importer?: string;
56
+ target: SdkBundleTarget;
57
+ }): string[];
58
+ //#endregion
59
+ export { LUCIDE_ICON_HELPER, SDK_BUNDLE_TARGETS, SdkBundleTarget, loadsAtFirstPaint, packageOfSpecifier, sdkBundleEntryFile, sdkBundleSpecifiers };