camox 0.59.0 → 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/editing/createEditableBlock.js +1 -1
- package/dist/features/preview/components/SelectionEditor.js +1 -1
- package/dist/features/renderer/RendererApp.js +187 -103
- package/dist/features/runtime/loopbackFetch.d.ts +8 -0
- package/dist/features/runtime/publicCache.d.ts +21 -0
- package/dist/features/runtime/publishingDraft.d.ts +4 -0
- package/dist/features/runtime/runtime.js +54 -3
- package/dist/features/runtime/worker.js +1 -4
- 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/SocialProfilesSettings.js +284 -151
- package/dist/features/studio/FollowAgentProvider.js +71 -12
- package/dist/features/studio/components/ChatWidget.js +85 -43
- package/dist/features/studio/components/Patrol.js +311 -208
- 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 +115 -45
- package/dist/features/studio/editorWhereContext.js +11 -0
- package/dist/features/studio/useEditorSelection.js +135 -0
- package/dist/features/vite/hostedChecks.js +1 -1
- package/dist/features/vite/runtimeDev.js +2 -1
- package/dist/features/vite/sdkBundleBuild.js +74 -0
- package/dist/features/vite/staticAssetHeaders.js +7 -4
- package/dist/features/vite/vite.d.ts +0 -2
- package/dist/features/vite/vite.js +33 -3
- package/dist/lib/auth.js +24 -58
- package/dist/lib/server-auth-cookie.js +12 -2
- package/dist/lib/stored-auth-cookies.js +76 -0
- package/dist/lib/use-thread.js +9 -5
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +17 -4
|
@@ -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 };
|