camox 0.57.0 → 0.58.0
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/createApp.d.ts +2 -2
- package/dist/core/createBlock.js +6 -2
- package/dist/core/createLayout.d.ts +1 -1
- package/dist/core/editing/createEditableBlock.d.ts +1 -1
- package/dist/core/lib/artifact.d.ts +14 -4
- package/dist/core/lib/artifact.js +57 -16
- package/dist/core/lib/artifactRuntime.js +4 -2
- package/dist/features/runtime/artifactDocuments.js +58 -0
- package/dist/features/runtime/artifactSources.js +36 -2
- package/dist/features/runtime/runtime.js +30 -12
- package/dist/features/runtime/runtimeRoutes.d.ts +6 -5
- package/dist/features/runtime/runtimeRoutes.js +12 -5
- package/dist/features/studio/components/DraftsControl.js +60 -134
- package/dist/features/studio/components/ReviewDraftSheet.js +52 -4
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +4 -4
- package/dist/features/studio/components/RenameDraftDialog.js +0 -206
package/dist/core/createApp.d.ts
CHANGED
|
@@ -416,7 +416,7 @@ export declare function createApp({ blocks, lazyBlocks, layouts, collections, do
|
|
|
416
416
|
Component: ({ blockData, mode, isFirstBlock, showAddBlockTop, showAddBlockBottom, addBlockAfterPosition }: BlockComponentProps<{
|
|
417
417
|
[x: string]: unknown;
|
|
418
418
|
}>) => import("react").JSX.Element;
|
|
419
|
-
LayoutItem: ({ collectionId, collection, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> |
|
|
419
|
+
LayoutItem: ({ collectionId, collection, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | import("react").JSX.Element | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> | null | undefined;
|
|
420
420
|
id: string;
|
|
421
421
|
title: string;
|
|
422
422
|
description: string;
|
|
@@ -858,7 +858,7 @@ export declare function createApp({ blocks, lazyBlocks, layouts, collections, do
|
|
|
858
858
|
Component: ({ blockData, mode, isFirstBlock, showAddBlockTop, showAddBlockBottom, addBlockAfterPosition }: BlockComponentProps<{
|
|
859
859
|
[x: string]: unknown;
|
|
860
860
|
}>) => import("react").JSX.Element;
|
|
861
|
-
LayoutItem: ({ collectionId, collection, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> |
|
|
861
|
+
LayoutItem: ({ collectionId, collection, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | import("react").JSX.Element | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> | null | undefined;
|
|
862
862
|
id: string;
|
|
863
863
|
title: string;
|
|
864
864
|
description: string;
|
package/dist/core/createBlock.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { artifactFrameProps, artifactStandInProps, isArtifactSource } from "./lib/artifact.js";
|
|
1
|
+
import { artifactFrameProps, artifactStandInProps, isArtifactSource, isArtifactSourceReference } from "./lib/artifact.js";
|
|
2
2
|
import { artifactData } from "./lib/artifactData.js";
|
|
3
3
|
import { resolveBlockFields, resolveToMarkdown, resolveToStructuredData, resolveToTitle } from "./lib/contentType.js";
|
|
4
4
|
import { useProjectUuid } from "../lib/auth.js";
|
|
@@ -190,9 +190,13 @@ function createViewBlock(options) {
|
|
|
190
190
|
size: useFieldSchema(name)?.size,
|
|
191
191
|
navigate: useNavigate()
|
|
192
192
|
});
|
|
193
|
-
const initialData = useInitialArtifactData(value, data);
|
|
193
|
+
const initialData = useInitialArtifactData(isArtifactSourceReference(value) ? value.artifactUrl : value, data);
|
|
194
194
|
const fontLinks = useOptionalCamoxApp()?.getArtifactFontLinks();
|
|
195
195
|
const runs = React.use(ArtifactsRunContext);
|
|
196
|
+
if (isArtifactSourceReference(value)) return children({
|
|
197
|
+
...artifactFrameProps(value, { data: initialData }),
|
|
198
|
+
...hostProps
|
|
199
|
+
});
|
|
196
200
|
if (!isArtifactSource(value)) return null;
|
|
197
201
|
return children({
|
|
198
202
|
...runs ? artifactFrameProps(value, {
|
|
@@ -236,7 +236,7 @@ declare function createLayoutDefinition(options: AnyLayoutOptions): {
|
|
|
236
236
|
Item: {
|
|
237
237
|
({ children }: {
|
|
238
238
|
children: (item: any) => React.ReactNode;
|
|
239
|
-
}): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> |
|
|
239
|
+
}): string | number | bigint | boolean | React.JSX.Element | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | null | undefined;
|
|
240
240
|
displayName: string;
|
|
241
241
|
};
|
|
242
242
|
useData(): unknown;
|
|
@@ -1694,7 +1694,7 @@ export declare function createEditableBlock<TSchemaShape extends Record<string,
|
|
|
1694
1694
|
params: unknown[] & [];
|
|
1695
1695
|
})["static"]; }, Exclude<keyof TSchemaShape, { [K_1 in keyof TSchemaShape]: TSchemaShape[K_1] extends import("@sinclair/typebox").TReadonly<TSchema> ? TSchemaShape[K_1] extends import("@sinclair/typebox").TOptional<TSchemaShape[K_1]> ? K_1 : never : never; }[keyof TSchemaShape] | { [K_2 in keyof TSchemaShape]: TSchemaShape[K_2] extends import("@sinclair/typebox").TReadonly<TSchema> ? TSchemaShape[K_2] extends import("@sinclair/typebox").TOptional<TSchemaShape[K_2]> ? never : K_2 : never; }[keyof TSchemaShape] | { [K_3 in keyof TSchemaShape]: TSchemaShape[K_3] extends import("@sinclair/typebox").TOptional<TSchema> ? TSchemaShape[K_3] extends import("@sinclair/typebox").TReadonly<TSchemaShape[K_3]> ? never : K_3 : never; }[keyof TSchemaShape]>>>>>) => React.JSX.Element;
|
|
1696
1696
|
/** Renders a record page's record; see `BlockEditingRuntime.renderLayoutItem`. */
|
|
1697
|
-
LayoutItem: ({ collectionId, collection, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> |
|
|
1697
|
+
LayoutItem: ({ collectionId, collection, properties, record, children, fallback }: LayoutItemProps) => string | number | bigint | boolean | React.JSX.Element | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | null | undefined;
|
|
1698
1698
|
id: string;
|
|
1699
1699
|
title: string;
|
|
1700
1700
|
description: string;
|
|
@@ -3,14 +3,23 @@ import "./artifactData.js";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import "@camox/api-contract";
|
|
5
5
|
//#region src/core/lib/artifact.d.ts
|
|
6
|
-
/** The frame attributes
|
|
7
|
-
|
|
8
|
-
srcDoc: string;
|
|
6
|
+
/** The frame attributes an artifact's frame has, whichever way it loads its document. */
|
|
7
|
+
type ArtifactFrameAttributes = {
|
|
9
8
|
sandbox: string;
|
|
10
9
|
allow: string;
|
|
11
10
|
title: string;
|
|
12
11
|
loading: "lazy";
|
|
13
12
|
};
|
|
13
|
+
/** The frame attributes of an artifact whose document is inlined. */
|
|
14
|
+
export type InlineArtifactFrameProps = ArtifactFrameAttributes & {
|
|
15
|
+
srcDoc: string;
|
|
16
|
+
src?: never;
|
|
17
|
+
};
|
|
18
|
+
/** The frame attributes of an artifact: its document inlined, or loaded by URL. */
|
|
19
|
+
export type ArtifactFrameProps = InlineArtifactFrameProps | (ArtifactFrameAttributes & {
|
|
20
|
+
src: string;
|
|
21
|
+
srcDoc?: never;
|
|
22
|
+
});
|
|
14
23
|
/** What `block.Artifact` passes to its render prop, to spread onto an `<iframe>`. */
|
|
15
24
|
export type ArtifactRenderProps = ArtifactFrameProps & {
|
|
16
25
|
/** Connects the frame to the `camox` runtime inside it. */
|
|
@@ -18,4 +27,5 @@ export type ArtifactRenderProps = ArtifactFrameProps & {
|
|
|
18
27
|
/** For `size: "content"`, the height the artifact's content needs, once it reports it. */
|
|
19
28
|
style?: React.CSSProperties;
|
|
20
29
|
};
|
|
21
|
-
//#endregion
|
|
30
|
+
//#endregion
|
|
31
|
+
export {};
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { escapeAttribute } from "../../lib/html.js";
|
|
2
2
|
import { ARTIFACT_RUNTIME } from "./artifactRuntime.js";
|
|
3
|
-
import { artifactTitle } from "@camox/api-contract";
|
|
3
|
+
import { artifactSourceHash, artifactTitle } from "@camox/api-contract";
|
|
4
4
|
|
|
5
5
|
//#region src/core/lib/artifact.ts
|
|
6
|
+
/** Whether a stored artifact value was replaced by a reference to its source. */
|
|
7
|
+
function isArtifactSourceReference(value) {
|
|
8
|
+
if (typeof value !== "object" || value === null) return false;
|
|
9
|
+
const { artifactUrl, title } = value;
|
|
10
|
+
return typeof artifactUrl === "string" && typeof title === "string";
|
|
11
|
+
}
|
|
6
12
|
const FONT_LINK_RELS = /* @__PURE__ */ new Set(["stylesheet", "preconnect"]);
|
|
7
13
|
const UNHEAD_TAG_OPTIONS = /* @__PURE__ */ new Set([
|
|
8
14
|
"key",
|
|
@@ -32,6 +38,15 @@ function artifactDataScript(data) {
|
|
|
32
38
|
return `${DATA_SCRIPT_OPEN}${json}<\/script>`;
|
|
33
39
|
}
|
|
34
40
|
/**
|
|
41
|
+
* The fragment, `#` included, that carries the starting Artifact Data in the URL of a frame that
|
|
42
|
+
* loads its artifact by URL instead of `srcdoc`. The runtime decodes it as `camox.data`. Fragments
|
|
43
|
+
* never reach the server, so one cached document serves every placement. Empty data needs none.
|
|
44
|
+
*/
|
|
45
|
+
function artifactDataFragment(data) {
|
|
46
|
+
const json = JSON.stringify(data);
|
|
47
|
+
return json === "{}" ? "" : `#${encodeURIComponent(json)}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
35
50
|
* The stylesheet and preconnect links of the project's document definition, as HTML, so artifacts
|
|
36
51
|
* use the website's fonts from their first render. Nothing else from the document's head: its
|
|
37
52
|
* title, meta, scripts and icons belong to the website, not to the artifact.
|
|
@@ -47,12 +62,35 @@ function artifactFontLinks(document) {
|
|
|
47
62
|
}).map(renderLink).join("");
|
|
48
63
|
}
|
|
49
64
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
65
|
+
* What every artifact's document holds before its source: the `camox` runtime, then the website's
|
|
66
|
+
* font links. The doctype comes first so the runtime's script never puts the page in quirks mode;
|
|
67
|
+
* the source's own doctype, if any, is then ignored by the parser.
|
|
68
|
+
*/
|
|
69
|
+
function artifactShell(fontLinks) {
|
|
70
|
+
return `<!doctype html><script>${ARTIFACT_RUNTIME}<\/script>${fontLinks}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The hash of the shell around every artifact source, which names it in the URLs public pages
|
|
74
|
+
* load artifacts from. It changes only with the runtime or the website's font links, so a release
|
|
75
|
+
* that changes neither keeps every artifact cached.
|
|
76
|
+
*/
|
|
77
|
+
function artifactShellHash(fontLinks) {
|
|
78
|
+
return artifactSourceHash(artifactShell(fontLinks));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The document an inline (`srcdoc`) artifact frame loads: the shell, the initial Artifact Data,
|
|
82
|
+
* then the stored source.
|
|
53
83
|
*/
|
|
54
84
|
function artifactDocument(source, { fontLinks = "", data = {} }) {
|
|
55
|
-
return
|
|
85
|
+
return `${artifactShell(fontLinks)}${artifactDataScript(data)}${source}`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The document served at an artifact's URL: the shell, then the stored source. No Artifact Data:
|
|
89
|
+
* the frame's URL fragment carries it (`artifactDataFragment`), so one cached document serves
|
|
90
|
+
* every placement, and an inline data script would win over the fragment.
|
|
91
|
+
*/
|
|
92
|
+
function artifactUrlDocument(source, fontLinks) {
|
|
93
|
+
return `${artifactShell(fontLinks)}${source}`;
|
|
56
94
|
}
|
|
57
95
|
/**
|
|
58
96
|
* The artifact source inside an artifact's document, the reverse of `artifactDocument`, or `null`
|
|
@@ -60,7 +98,7 @@ function artifactDocument(source, { fontLinks = "", data = {} }) {
|
|
|
60
98
|
* JSON escapes it too, so the first `<\/script>` after the data's opening tag ends the data.
|
|
61
99
|
*/
|
|
62
100
|
function artifactSourceOf(document) {
|
|
63
|
-
const head =
|
|
101
|
+
const head = artifactShell("");
|
|
64
102
|
if (!document.startsWith(head)) return null;
|
|
65
103
|
const data = document.indexOf(DATA_SCRIPT_OPEN, head.length);
|
|
66
104
|
if (data < 0 || document.slice(head.length, data).includes("<script")) return null;
|
|
@@ -68,19 +106,22 @@ function artifactSourceOf(document) {
|
|
|
68
106
|
if (end < 0) return null;
|
|
69
107
|
return document.slice(end + 9);
|
|
70
108
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* links. This getter is the boundary that hides how the source reaches the frame (inline `srcdoc`
|
|
74
|
-
* today), so blocks never depend on it.
|
|
75
|
-
*/
|
|
76
|
-
function artifactFrameProps(source, options = {}) {
|
|
77
|
-
return {
|
|
78
|
-
srcDoc: artifactDocument(source, options),
|
|
109
|
+
function artifactFrameProps(artifact, options = {}) {
|
|
110
|
+
const attributes = {
|
|
79
111
|
sandbox: "allow-scripts",
|
|
80
112
|
allow: "fullscreen; clipboard-write",
|
|
81
|
-
title: artifactTitle(source),
|
|
82
113
|
loading: "lazy"
|
|
83
114
|
};
|
|
115
|
+
if (typeof artifact === "string") return {
|
|
116
|
+
srcDoc: artifactDocument(artifact, options),
|
|
117
|
+
...attributes,
|
|
118
|
+
title: artifactTitle(artifact)
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
src: `${artifact.artifactUrl}${artifactDataFragment(options.data ?? {})}`,
|
|
122
|
+
...attributes,
|
|
123
|
+
title: artifact.title
|
|
124
|
+
};
|
|
84
125
|
}
|
|
85
126
|
const PLACEHOLDER_SOURCE = `<!doctype html>
|
|
86
127
|
<html>
|
|
@@ -151,4 +192,4 @@ function isArtifactSource(value) {
|
|
|
151
192
|
}
|
|
152
193
|
|
|
153
194
|
//#endregion
|
|
154
|
-
export { artifactFontLinks, artifactFrameProps, artifactPlaceholderProps, artifactSourceOf, artifactStandInProps, artifactTitle, isArtifactSource };
|
|
195
|
+
export { artifactDataFragment, artifactFontLinks, artifactFrameProps, artifactPlaceholderProps, artifactShellHash, artifactSourceOf, artifactStandInProps, artifactTitle, artifactUrlDocument, isArtifactSource, isArtifactSourceReference };
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* change. The host converts the colors, so the runtime that every frame inlines stays small.
|
|
14
14
|
* - host → frame: `data` with the latest Artifact Data. The initial data is inlined in the
|
|
15
15
|
* `srcDoc` as a `<script type="application/json" data-camox-data>` after the runtime and the
|
|
16
|
-
* font links,
|
|
16
|
+
* font links, or, for a frame loaded by URL, encoded in the URL's fragment (see
|
|
17
|
+
* `artifactDataFragment`). It is read lazily, on first access, but the fragment is captured as
|
|
18
|
+
* the runtime starts, before the artifact source can change it.
|
|
17
19
|
* - runtime → page: `observe-viewport` with `observe`, when the artifact's first `onViewport`
|
|
18
20
|
* subscription starts or its last one ends. While observed, the host sends `viewport` with
|
|
19
21
|
* `progress` and `visibleRatio` whenever they change.
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
* `plugins/minifyArtifactRuntime.ts`), so it must stay one template literal without
|
|
31
33
|
* interpolations or backslashes.
|
|
32
34
|
*/
|
|
33
|
-
const ARTIFACT_RUNTIME = "(()=>{\"use strict\";let e=window.parent,t=null,n=null,r,i,a=new Set,o=()=>{if(i!==void 0)return r;let e=document.querySelector(`script[data-camox-data]`);try{r=
|
|
35
|
+
const ARTIFACT_RUNTIME = "(()=>{\"use strict\";let e=window.parent,t=null,n=null,r,i,a=new Set,o=location.hash.slice(1),s=()=>{if(i!==void 0)return r;let e=document.querySelector(`script[data-camox-data]`);try{r=JSON.parse(e?e.textContent:decodeURIComponent(o)),(!r||typeof r!=`object`||Array.isArray(r))&&(r={})}catch{r={}}return i=JSON.stringify(r),r},c=(()=>{let e=new Set,t=[],n=Object.freeze({tokens:Object.freeze({}),colors:Object.freeze({})}),r=document.createElement(`style`);r.textContent=`body { visibility: hidden !important; }`,document.head.append(r);let i=()=>r.remove();setTimeout(i,300);let a=e=>Number.isFinite(e)?Math.min(255,Math.max(0,Math.round(e))):0;return{get current(){return n},receive(r){let o={},s={},c=r.colors||{};for(let[e,t]of Object.entries(r.tokens||{})){if(!e.startsWith(`--`)||typeof t!=`string`)continue;o[e]=t;let n=Object.hasOwn(c,e)?c[e]:null;n&&typeof n==`object`&&(s[e]=Object.freeze({r:a(n.r),g:a(n.g),b:a(n.b)}))}let l=document.documentElement.style;for(let e of t)Object.hasOwn(o,e)||l.removeProperty(e);for(let[e,t]of Object.entries(o))l.setProperty(e,t);t=Object.keys(o),i(),n=Object.freeze({tokens:Object.freeze(o),colors:Object.freeze(s)});for(let t of[...e])t(n)},subscribe(t){return e.add(t),()=>{e.delete(t)}}}})(),l=new Set,u=null,d=null,f=null,p=()=>{let e=Math.ceil(document.documentElement.getBoundingClientRect().height);e!==f&&(f=e,m(`height`,{height:f}))},m=(n,r)=>{t&&e.postMessage({...r,camox:n,channel:t},`*`)},h=()=>m(`observe-viewport`,{observe:l.size>0}),g={studio(e){let t=e.studio;n=t?Object.freeze({editing:!!t.editing,canvas:!!t.canvas}):null},data(e){s();let t=JSON.stringify(e.data??{});if(t!==i){r=JSON.parse(t),i=t;for(let e of[...a])e(r)}},theme:c.receive,measure(){f=null,d||(d=new ResizeObserver(p),d.observe(document.documentElement)),p()},viewport(e){let{progress:t,visibleRatio:n}=e;if(typeof t==`number`&&typeof n==`number`){u=Object.freeze({progress:t,visibleRatio:n}),document.documentElement.style.setProperty(`--camox-progress`,String(t));for(let e of l)e(u)}}};window.addEventListener(`message`,e=>{let n=e.data;if(n&&typeof n.camox==`string`){if(n.camox===`connect`){if(typeof n.channel!=`string`||n.channel===t)return;t=n.channel,m(`ready`),l.size>0&&h()}else t&&n.channel===t&&Object.hasOwn(g,n.camox)&&g[n.camox](n)}});let _=Object.freeze({get studio(){return n},get data(){return s()},onData(e){return a.add(e),()=>{a.delete(e)}},get theme(){return c.current},onTheme:c.subscribe,navigate(e){m(`navigate`,{href:String(e)})},onViewport(e){if(typeof e!=`function`)throw TypeError(`camox.onViewport needs a callback`);let t=t=>e(t);return l.add(t),l.size===1?h():u&&queueMicrotask(()=>l.has(t)&&e(u)),()=>{l.delete(t)&&l.size===0&&h()}}});Object.defineProperty(window,\"camox\",{value:_,enumerable:!0}),e!==window&&e.postMessage({camox:`hello`},`*`)})();";
|
|
34
36
|
|
|
35
37
|
//#endregion
|
|
36
38
|
export { ARTIFACT_RUNTIME };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { artifactFontLinks, artifactShellHash, artifactUrlDocument } from "../../core/lib/artifact.js";
|
|
2
|
+
import { ARTIFACTS_PATH_PREFIX } from "./runtimeRoutes.js";
|
|
3
|
+
|
|
4
|
+
//#region src/features/runtime/artifactDocuments.ts
|
|
5
|
+
const shells = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
/**
|
|
7
|
+
* The website's artifact shell, computed once per runtime: its font links come from the build's
|
|
8
|
+
* document definition, so they never change while it runs.
|
|
9
|
+
*/
|
|
10
|
+
function readArtifactShell(options) {
|
|
11
|
+
let shell = shells.get(options);
|
|
12
|
+
if (!shell) {
|
|
13
|
+
shell = (async () => {
|
|
14
|
+
const fontLinks = artifactFontLinks(await options.getDocument?.());
|
|
15
|
+
return {
|
|
16
|
+
fontLinks,
|
|
17
|
+
hash: await artifactShellHash(fontLinks)
|
|
18
|
+
};
|
|
19
|
+
})();
|
|
20
|
+
shell.catch(() => shells.delete(options));
|
|
21
|
+
shells.set(options, shell);
|
|
22
|
+
}
|
|
23
|
+
return shell;
|
|
24
|
+
}
|
|
25
|
+
const YEAR = 31536e3;
|
|
26
|
+
/**
|
|
27
|
+
* An artifact's document, served at `/_camox/artifacts/<shellHash>/<sourceHash>`: the source the
|
|
28
|
+
* API stores under its hash, inside the website's current shell. It's the same for every visitor,
|
|
29
|
+
* so cookies are ignored. It keeps an opaque origin even when opened top-level, and only a
|
|
30
|
+
* document whose shell matches its URL is cached: one from a previous release still loads.
|
|
31
|
+
*/
|
|
32
|
+
async function createArtifactDocumentResponse(pathname, options) {
|
|
33
|
+
const [shellHash, sourceHash, ...rest] = pathname.slice(ARTIFACTS_PATH_PREFIX.length).split("/");
|
|
34
|
+
const { apiUrl, projectUuid } = options;
|
|
35
|
+
if (!apiUrl || !projectUuid) return Response.json({ message: "Camox artifacts are not configured." }, { status: 500 });
|
|
36
|
+
if (!shellHash || !sourceHash || rest.length) return notFound();
|
|
37
|
+
const [shell, stored] = await Promise.all([readArtifactShell(options), fetch(`${apiUrl}/artifact-sources/${encodeURIComponent(projectUuid)}/${encodeURIComponent(sourceHash)}`)]);
|
|
38
|
+
if (stored.status === 404) return notFound();
|
|
39
|
+
if (!stored.ok) return new Response("Artifact source unavailable", {
|
|
40
|
+
status: 502,
|
|
41
|
+
headers: { "Cache-Control": "no-store" }
|
|
42
|
+
});
|
|
43
|
+
return new Response(artifactUrlDocument(await stored.text(), shell.fontLinks), { headers: {
|
|
44
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
45
|
+
"Cache-Control": shellHash === shell.hash ? `public, max-age=${YEAR}, immutable` : "no-store",
|
|
46
|
+
"Content-Security-Policy": "sandbox allow-scripts",
|
|
47
|
+
"X-Robots-Tag": "noindex"
|
|
48
|
+
} });
|
|
49
|
+
}
|
|
50
|
+
function notFound() {
|
|
51
|
+
return new Response("Not found", {
|
|
52
|
+
status: 404,
|
|
53
|
+
headers: { "Cache-Control": "no-store" }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { createArtifactDocumentResponse, readArtifactShell };
|
|
@@ -1,6 +1,40 @@
|
|
|
1
|
-
import { artifactSourceOf } from "../../core/lib/artifact.js";
|
|
1
|
+
import { artifactSourceOf, artifactTitle } from "../../core/lib/artifact.js";
|
|
2
2
|
|
|
3
3
|
//#region src/features/runtime/artifactSources.ts
|
|
4
|
+
function isStringRecord(value) {
|
|
5
|
+
return isPlainObject(value) && Object.values(value).every((hash) => typeof hash === "string");
|
|
6
|
+
}
|
|
7
|
+
/** A copy of a JSON value with every object replaced, deepest first. */
|
|
8
|
+
function mapObjects(value, replace) {
|
|
9
|
+
if (Array.isArray(value)) return value.map((item) => mapObjects(item, replace));
|
|
10
|
+
if (!isPlainObject(value)) return value;
|
|
11
|
+
return replace(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, mapObjects(item, replace)])));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The page's dehydrated state with each artifact source replaced by a reference to the URL its
|
|
15
|
+
* document loads from, `<artifactsPath><sourceHash>`, and its title: public pages load their
|
|
16
|
+
* artifacts by URL, so neither their HTML nor their hydration data carries a source. Every block
|
|
17
|
+
* and repeatable item the API reads names its sources' hashes beside its content.
|
|
18
|
+
*/
|
|
19
|
+
function referenceArtifactSources(dehydratedState, artifactsPath) {
|
|
20
|
+
return mapObjects(dehydratedState, (owner) => {
|
|
21
|
+
const { artifactHashes, content } = owner;
|
|
22
|
+
if (!isStringRecord(artifactHashes) || !isPlainObject(content)) return owner;
|
|
23
|
+
const referenced = { ...content };
|
|
24
|
+
for (const [field, hash] of Object.entries(artifactHashes)) {
|
|
25
|
+
const source = content[field];
|
|
26
|
+
if (typeof source !== "string") continue;
|
|
27
|
+
referenced[field] = {
|
|
28
|
+
artifactUrl: `${artifactsPath}${hash}`,
|
|
29
|
+
title: artifactTitle(source)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
...owner,
|
|
34
|
+
content: referenced
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
4
38
|
/**
|
|
5
39
|
* A page's HTML holds each rendered artifact source in its frame's `srcdoc`, so the server leaves
|
|
6
40
|
* them out of the hydration data, and the client takes them back from the frames before
|
|
@@ -84,4 +118,4 @@ function restoreArtifactSources(dehydratedState, root) {
|
|
|
84
118
|
}
|
|
85
119
|
|
|
86
120
|
//#endregion
|
|
87
|
-
export { omitArtifactSources, restoreArtifactSources };
|
|
121
|
+
export { omitArtifactSources, referenceArtifactSources, restoreArtifactSources };
|
|
@@ -3,7 +3,7 @@ import { createRequestContext } from "../../core/requestContext.js";
|
|
|
3
3
|
import { buildClearServerAuthCookieHeader, getServerAuthCookieHeader } from "../../lib/server-auth-cookie.js";
|
|
4
4
|
import { UNNAMED_DEFAULT_LOCALE, localizedPath, parseLocalePath } from "../../lib/locale-path.js";
|
|
5
5
|
import { draftNamedBy } from "../../lib/draft-scope.js";
|
|
6
|
-
import { RUNTIME_ROUTE_PATHS, matchRuntimeOwnedRoute } from "./runtimeRoutes.js";
|
|
6
|
+
import { ARTIFACTS_PATH_PREFIX, RUNTIME_ROUTE_PATHS, matchRuntimeOwnedRoute } from "./runtimeRoutes.js";
|
|
7
7
|
import { reads } from "../../lib/studio-reads.js";
|
|
8
8
|
import { asCollectionItem, recordMarkdownTemplate, recordPageValues } from "../../core/recordItem.js";
|
|
9
9
|
import { PLATFORM_SCRIPT } from "../../lib/platform.js";
|
|
@@ -17,7 +17,8 @@ import { assetPrefix } from "./assetPrefix.js";
|
|
|
17
17
|
import { documentGoogleFonts } from "../og/documentFonts.js";
|
|
18
18
|
import { alternatesByPage, findAlternates, hreflangLinks } from "./alternates.js";
|
|
19
19
|
import { configuredProviders, renderAnalyticsScripts } from "./analytics.js";
|
|
20
|
-
import {
|
|
20
|
+
import { createArtifactDocumentResponse, readArtifactShell } from "./artifactDocuments.js";
|
|
21
|
+
import { omitArtifactSources, referenceArtifactSources } from "./artifactSources.js";
|
|
21
22
|
import { readBreadcrumb } from "./breadcrumbs.js";
|
|
22
23
|
import { RouteDiscoveryError, listLivePages } from "./livePages.js";
|
|
23
24
|
import { formatLlmsTxt } from "./llmsTxt.js";
|
|
@@ -519,8 +520,9 @@ function preparePreviewDocument(input, options, document) {
|
|
|
519
520
|
input.previewDocument = `<!doctype html><html${siteHead.htmlAttrs}><head><base href="${escapeXml(href)}">${siteHead.headTags}</head><body${siteHead.bodyAttrs}>${siteHead.bodyTagsOpen}<div id="root" data-camox-preview-root></div>${siteHead.bodyTags}</body></html>`;
|
|
520
521
|
}
|
|
521
522
|
/**
|
|
522
|
-
* The render input a page's HTML embeds for hydration.
|
|
523
|
-
* already holds each rendered
|
|
523
|
+
* The render input a page's HTML embeds for hydration. Outside the studio, a page whose HTML
|
|
524
|
+
* inlines its artifacts (in dev mode) already holds each rendered source in its frame, so their
|
|
525
|
+
* copy leaves those out. Public pages load artifacts by URL and hold no source at all.
|
|
524
526
|
*/
|
|
525
527
|
function hydrationInputFor(input, html) {
|
|
526
528
|
if (input.presentation === "studio") return input;
|
|
@@ -529,6 +531,21 @@ function hydrationInputFor(input, html) {
|
|
|
529
531
|
dehydratedState: omitArtifactSources(input.dehydratedState, html)
|
|
530
532
|
};
|
|
531
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* The render input with its artifacts loaded by URL, where a visitor reads Live: each artifact
|
|
536
|
+
* source becomes a reference to the document the website serves, so neither the page's HTML nor
|
|
537
|
+
* its data carries a source, and the client renders the frames the server did. The studio, drafts
|
|
538
|
+
* and dev mode keep inlined sources, so an agent's change shows at once.
|
|
539
|
+
*/
|
|
540
|
+
async function withArtifactsByUrl(input, options) {
|
|
541
|
+
if (input.presentation !== "public" || input.draftId || options.developmentDraft) return input;
|
|
542
|
+
const shell = await readArtifactShell(options);
|
|
543
|
+
const artifactsPath = `${input.runtimeBasePath}${ARTIFACTS_PATH_PREFIX}${shell.hash}/`;
|
|
544
|
+
return {
|
|
545
|
+
...input,
|
|
546
|
+
dehydratedState: referenceArtifactSources(input.dehydratedState, artifactsPath)
|
|
547
|
+
};
|
|
548
|
+
}
|
|
532
549
|
function renderStudioThemeScript(enabled) {
|
|
533
550
|
return enabled ? `<script data-camox-studio>${STUDIO_THEME_SCRIPT}${PLATFORM_SCRIPT}<\/script>` : "";
|
|
534
551
|
}
|
|
@@ -841,7 +858,7 @@ async function createPageHtmlResponse({ options, pathname, located, request, pag
|
|
|
841
858
|
studio,
|
|
842
859
|
draftId: result.draftId
|
|
843
860
|
}, result.data.page.project);
|
|
844
|
-
const pageRenderInput = {
|
|
861
|
+
const pageRenderInput = await withArtifactsByUrl({
|
|
845
862
|
alternates,
|
|
846
863
|
project,
|
|
847
864
|
presentation: studio ? "studio" : "public",
|
|
@@ -860,7 +877,7 @@ async function createPageHtmlResponse({ options, pathname, located, request, pag
|
|
|
860
877
|
runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
|
|
861
878
|
locale: located.locale,
|
|
862
879
|
locales: located.locales
|
|
863
|
-
};
|
|
880
|
+
}, options);
|
|
864
881
|
preparePreviewDocument(pageRenderInput, options, document);
|
|
865
882
|
const appHtml = await options.renderPage(pageRenderInput);
|
|
866
883
|
const renderedHead = renderHead(studio ? {} : document, createPageHeadInput(head), studio ? void 0 : options.stylesheetUrl, studio ? options.studioStylesheetUrl : void 0, studio ? void 0 : located.locale);
|
|
@@ -1170,7 +1187,7 @@ async function createDerivedResponse(options, pathname, located, request, { data
|
|
|
1170
1187
|
studio,
|
|
1171
1188
|
draftId: read.draftId
|
|
1172
1189
|
}, shared.project);
|
|
1173
|
-
const input = {
|
|
1190
|
+
const input = await withArtifactsByUrl({
|
|
1174
1191
|
alternates,
|
|
1175
1192
|
project,
|
|
1176
1193
|
presentation: studio ? "studio" : "public",
|
|
@@ -1199,7 +1216,7 @@ async function createDerivedResponse(options, pathname, located, request, { data
|
|
|
1199
1216
|
result: serializedResult,
|
|
1200
1217
|
layout: shared.layout
|
|
1201
1218
|
}
|
|
1202
|
-
};
|
|
1219
|
+
}, options);
|
|
1203
1220
|
const document = await options.getDocument?.() ?? {};
|
|
1204
1221
|
preparePreviewDocument(input, options, document);
|
|
1205
1222
|
if (dataOnly) return Response.json(input, { headers: {
|
|
@@ -1350,7 +1367,7 @@ async function createPageDataResponse({ options, pathname, located, request, pag
|
|
|
1350
1367
|
localeReads: localeReads(result.draftId)
|
|
1351
1368
|
});
|
|
1352
1369
|
const dehydratedState = dehydrate(queryClient);
|
|
1353
|
-
const
|
|
1370
|
+
const navigationInput = await withArtifactsByUrl({
|
|
1354
1371
|
alternates,
|
|
1355
1372
|
apiUrl: options.apiUrl,
|
|
1356
1373
|
authenticationUrl: options.authenticationUrl ?? "",
|
|
@@ -1368,9 +1385,9 @@ async function createPageDataResponse({ options, pathname, located, request, pag
|
|
|
1368
1385
|
loaderData: result.data,
|
|
1369
1386
|
head,
|
|
1370
1387
|
dehydratedState
|
|
1371
|
-
};
|
|
1372
|
-
preparePreviewDocument(
|
|
1373
|
-
return Response.json(
|
|
1388
|
+
}, options);
|
|
1389
|
+
preparePreviewDocument(navigationInput, options, await options.getDocument?.() ?? {});
|
|
1390
|
+
return Response.json(navigationInput, { headers });
|
|
1374
1391
|
} catch (error) {
|
|
1375
1392
|
if (isNotFoundError(error)) {
|
|
1376
1393
|
const derived = await createDerivedResponse(options, pathname, located, request, { dataOnly: true });
|
|
@@ -1397,6 +1414,7 @@ async function handleCamoxRequest(request, options = {}) {
|
|
|
1397
1414
|
layouts: camoxApp.getLayouts().map((layout) => layout._internal.id)
|
|
1398
1415
|
});
|
|
1399
1416
|
}
|
|
1417
|
+
if (match.kind === "artifact") return createArtifactDocumentResponse(pathname, options);
|
|
1400
1418
|
if (match.kind === "og") return createOgResponse(request, options);
|
|
1401
1419
|
if (match.kind === "renderer") return createRendererDocumentResponse(options);
|
|
1402
1420
|
if (match.kind === "logo") return createProjectLogoResponse(request, options);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
//#region src/features/runtime/runtimeRoutes.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* The URLs the runtime answers that aren't pages or the Studio
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* The URLs the runtime answers that aren't pages or the Studio, besides artifact documents
|
|
4
|
+
* (`ARTIFACTS_PATH_PREFIX`). Worker request filtering, runtime route matching and client
|
|
5
|
+
* navigation all match them through `matchRuntimeOwnedRoute`, so a new one can't be forgotten in
|
|
6
|
+
* any of them. Other prefix rules stay with their readers: the Worker also answers every
|
|
7
|
+
* `/camox/` Studio path, and client navigation also skips any other `/_camox/` path.
|
|
7
8
|
* No imports: client navigation ships this module in the browser bundle.
|
|
8
9
|
*/
|
|
9
10
|
export declare const RUNTIME_ROUTE_PATHS: {
|
|
@@ -18,5 +19,5 @@ export declare const RUNTIME_ROUTE_PATHS: {
|
|
|
18
19
|
readonly sitemap: "/sitemap.xml";
|
|
19
20
|
readonly "llms-txt": "/llms.txt";
|
|
20
21
|
};
|
|
21
|
-
export type RuntimeOwnedRouteKind = keyof typeof RUNTIME_ROUTE_PATHS;
|
|
22
|
+
export type RuntimeOwnedRouteKind = keyof typeof RUNTIME_ROUTE_PATHS | "artifact";
|
|
22
23
|
//#endregion
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
//#region src/features/runtime/runtimeRoutes.ts
|
|
2
2
|
/**
|
|
3
|
-
* The URLs the runtime answers that aren't pages or the Studio
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* The URLs the runtime answers that aren't pages or the Studio, besides artifact documents
|
|
4
|
+
* (`ARTIFACTS_PATH_PREFIX`). Worker request filtering, runtime route matching and client
|
|
5
|
+
* navigation all match them through `matchRuntimeOwnedRoute`, so a new one can't be forgotten in
|
|
6
|
+
* any of them. Other prefix rules stay with their readers: the Worker also answers every
|
|
7
|
+
* `/camox/` Studio path, and client navigation also skips any other `/_camox/` path.
|
|
7
8
|
* No imports: client navigation ships this module in the browser bundle.
|
|
8
9
|
*/
|
|
9
10
|
const RUNTIME_ROUTE_PATHS = {
|
|
@@ -18,11 +19,17 @@ const RUNTIME_ROUTE_PATHS = {
|
|
|
18
19
|
sitemap: "/sitemap.xml",
|
|
19
20
|
"llms-txt": "/llms.txt"
|
|
20
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Where public pages load artifacts from: `<prefix><shellHash>/<sourceHash>`. Each document is
|
|
24
|
+
* cached by its own URL, never as a page.
|
|
25
|
+
*/
|
|
26
|
+
const ARTIFACTS_PATH_PREFIX = "/_camox/artifacts/";
|
|
21
27
|
/** Which runtime-owned route a path inside the runtime (base path removed) is, if any. */
|
|
22
28
|
function matchRuntimeOwnedRoute(pathname) {
|
|
23
29
|
for (const [kind, path] of Object.entries(RUNTIME_ROUTE_PATHS)) if (pathname === path) return kind;
|
|
30
|
+
if (pathname.startsWith("/_camox/artifacts/")) return "artifact";
|
|
24
31
|
return null;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
//#endregion
|
|
28
|
-
export { RUNTIME_ROUTE_PATHS, matchRuntimeOwnedRoute };
|
|
35
|
+
export { ARTIFACTS_PATH_PREFIX, RUNTIME_ROUTE_PATHS, matchRuntimeOwnedRoute };
|