@jxsuite/studio 2.4.2 → 3.0.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/chunks/{cf-settings-g5fbf2zp.js → cf-settings-zj2cj1ar.js} +6 -3
- package/dist/chunks/{content-source-k9724vcv.js → content-source-7fy9qrm0.js} +10 -9
- package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-6vvx96n2.js} +9 -8
- package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-kwzvpemr.js} +13 -12
- package/dist/chunks/{files-yew27sn9.js → files-xcybanxe.js} +14 -13
- package/dist/chunks/{format-host-wewba4nq.js → format-host-8fr3545e.js} +2 -2
- package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-g01d93vc.js} +9 -8
- package/dist/chunks/{layers-panel-qnsghwwd.js → layers-panel-27essgkc.js} +12 -11
- package/dist/chunks/{publish-panel-a0pdft5w.js → publish-panel-09sy2bab.js} +8 -7
- package/dist/chunks/{quick-search-9avyc4pc.js → quick-search-sefb61v8.js} +15 -14
- package/dist/chunks/{right-panel-kcnhtc5j.js → right-panel-9r32jd5j.js} +11 -10
- package/dist/chunks/{studio-aweyr4v1.js → studio-2x9zbxmx.js} +2 -2
- package/dist/chunks/{studio-ef8t53vc.js → studio-3dkcd4c5.js} +3 -3
- package/dist/chunks/{studio-hb7c46jy.js → studio-5efrtn1m.js} +9 -7
- package/dist/chunks/{studio-v1va0vhd.js → studio-6z5eyjwn.js} +24 -25
- package/dist/chunks/{studio-ke244pam.js → studio-7p35254k.js} +33 -11
- package/dist/chunks/{studio-f5yyvqxv.js → studio-8ezdegcx.js} +13 -11
- package/dist/chunks/{studio-cz2j1c0g.js → studio-a0yp7syn.js} +55 -18
- package/dist/chunks/{studio-26sv44br.js → studio-dc8s8heb.js} +34 -23
- package/dist/chunks/{studio-jnakhg87.js → studio-g5gdqnyd.js} +4 -4
- package/dist/chunks/{studio-rqemfhz6.js → studio-hcxzyk3x.js} +2 -2
- package/dist/chunks/{studio-ws6ef6js.js → studio-hj2se583.js} +12 -10
- package/dist/chunks/{studio-pyrw8h0w.js → studio-j595e748.js} +407 -167
- package/dist/chunks/{studio-dgj7gq44.js → studio-k4jvjcfx.js} +4 -4
- package/dist/chunks/studio-kfpek536.js +28 -0
- package/dist/chunks/{studio-sccge2wf.js → studio-p5dja96s.js} +2 -2
- package/dist/chunks/{studio-0b6fs05q.js → studio-pkp61mh8.js} +5 -5
- package/dist/chunks/studio-qpfq7xhm.js +1802 -0
- package/dist/chunks/{studio-ra7ar50f.js → studio-r5xbtctf.js} +51 -1565
- package/dist/chunks/{studio-v2c5pmjj.js → studio-rh1cnamv.js} +17 -15
- package/dist/chunks/{studio-28xc8ws4.js → studio-ry1vz5sv.js} +10 -8
- package/dist/chunks/{studio-hyn3vhwr.js → studio-xntfdy1x.js} +5 -5
- package/dist/iframe-entry.js +344 -26
- package/dist/studio.js +374 -248
- package/package.json +9 -9
- package/src/browse/library-layouts.ts +3 -3
- package/src/browse/library-pane.ts +2 -2
- package/src/browse/library-preview.ts +19 -3
- package/src/canvas/asset-refs.ts +119 -0
- package/src/canvas/asset-resolve.ts +256 -0
- package/src/canvas/canvas-live-render.ts +23 -15
- package/src/canvas/iframe-entry.ts +1 -0
- package/src/canvas/iframe-host.ts +4 -0
- package/src/canvas/iframe-protocol.ts +9 -0
- package/src/canvas/iframe-render.ts +28 -10
- package/src/canvas/site-style-css.ts +2 -6
- package/src/editor/file-drop-action.ts +2 -1
- package/src/files/files.ts +2 -2
- package/src/files/media-paths.ts +37 -14
- package/src/files/media-upload.ts +47 -6
- package/src/grid/grid-idle.ts +45 -0
- package/src/grid/grid-view.ts +21 -0
- package/src/grid/schema-columns.ts +2 -1
- package/src/grid/sources/content-source.ts +24 -3
- package/src/new-project/import-tab.ts +3 -2
- package/src/panels/ai-chat/composer.ts +30 -13
- package/src/panels/frontmatter-fields.ts +6 -2
- package/src/panels/properties-panel.ts +8 -2
- package/src/panels/seo-modal.ts +5 -6
- package/src/panels/signals-panel.ts +2 -1
- package/src/platforms/cloud.ts +75 -5
- package/src/platforms/devserver.ts +5 -1
- package/src/services/ai-models.ts +101 -11
- package/src/services/ai-settings.ts +69 -71
- package/src/services/cf-settings.ts +19 -37
- package/src/services/document-assistant.ts +4 -3
- package/src/services/idle.ts +6 -1
- package/src/services/settings/definitions.ts +164 -0
- package/src/services/settings/kernel.ts +335 -0
- package/src/services/settings/write-queue.ts +116 -0
- package/src/settings/preferences-accounts.ts +6 -19
- package/src/settings/preferences-keymap.ts +8 -18
- package/src/settings/schema-field-ui.ts +2 -2
- package/src/shell.ts +6 -14
- package/src/studio.ts +32 -1
- package/src/types.ts +61 -3
- package/src/ui/ai-credentials-form.ts +52 -27
- package/src/ui/media-picker.ts +6 -11
- package/src/utils/studio-utils.ts +45 -3
- package/dist/chunks/studio-a7pzveq1.js +0 -110
- package/src/canvas/content-assets.ts +0 -263
- package/src/services/settings-store.ts +0 -99
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -57,14 +57,14 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@atlaskit/pragmatic-drag-and-drop": "^3.0.0",
|
|
59
59
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.1.0",
|
|
60
|
-
"@jxsuite/ai": "^0.36.
|
|
61
|
-
"@jxsuite/collab": "^0.8.
|
|
62
|
-
"@jxsuite/create": "^1.3.
|
|
63
|
-
"@jxsuite/formulas": "^0.0.
|
|
64
|
-
"@jxsuite/markup": "^0.4.
|
|
65
|
-
"@jxsuite/protocol": "^
|
|
66
|
-
"@jxsuite/runtime": "^2.0
|
|
67
|
-
"@jxsuite/schema": "^1.
|
|
60
|
+
"@jxsuite/ai": "^0.36.4",
|
|
61
|
+
"@jxsuite/collab": "^0.8.3",
|
|
62
|
+
"@jxsuite/create": "^1.3.7",
|
|
63
|
+
"@jxsuite/formulas": "^0.0.14",
|
|
64
|
+
"@jxsuite/markup": "^0.4.7",
|
|
65
|
+
"@jxsuite/protocol": "^2.0.0",
|
|
66
|
+
"@jxsuite/runtime": "^2.1.0",
|
|
67
|
+
"@jxsuite/schema": "^1.9.0",
|
|
68
68
|
"@spectrum-web-components/accordion": "^1.12.2",
|
|
69
69
|
"@spectrum-web-components/action-bar": "1.12.2",
|
|
70
70
|
"@spectrum-web-components/action-button": "^1.12.2",
|
|
@@ -19,7 +19,7 @@ import { html, nothing } from "lit-html";
|
|
|
19
19
|
import { repeat } from "lit-html/directives/repeat.js";
|
|
20
20
|
import { ref } from "lit-html/directives/ref.js";
|
|
21
21
|
import { isImage } from "../files/media-upload";
|
|
22
|
-
import {
|
|
22
|
+
import { previewFileSrc } from "../files/media-paths";
|
|
23
23
|
import { localeLabel } from "@jxsuite/schema/locale";
|
|
24
24
|
import { groupByCategory, groupByDate } from "./library-model";
|
|
25
25
|
import type { LibraryFile, LibraryLayout } from "./library-model";
|
|
@@ -154,7 +154,7 @@ function previewTpl(file: LibraryFile, ctx: LayoutContext): TemplateResult {
|
|
|
154
154
|
return html`<img
|
|
155
155
|
class="library-thumb"
|
|
156
156
|
loading="lazy"
|
|
157
|
-
src=${
|
|
157
|
+
src=${previewFileSrc(file.path)}
|
|
158
158
|
alt=""
|
|
159
159
|
/>`;
|
|
160
160
|
}
|
|
@@ -268,7 +268,7 @@ export function mediaTpl(files: readonly LibraryFile[], ctx: LayoutContext): Tem
|
|
|
268
268
|
? html`<img
|
|
269
269
|
class="library-thumb"
|
|
270
270
|
loading="lazy"
|
|
271
|
-
src=${
|
|
271
|
+
src=${previewFileSrc(file.path)}
|
|
272
272
|
alt=""
|
|
273
273
|
/>`
|
|
274
274
|
: html`<sp-icon-document size="l"></sp-icon-document>`
|
|
@@ -43,7 +43,7 @@ import { confirmFileDelete, renamePromptMessage } from "../files/file-ops";
|
|
|
43
43
|
import { createFileIn, openFileInTab } from "../files/files";
|
|
44
44
|
import { createEntry } from "../content/entry-commands";
|
|
45
45
|
import { entryCollections } from "../content/entry-model";
|
|
46
|
-
import {
|
|
46
|
+
import { uploadAccept, uploadAssets } from "../files/media-upload";
|
|
47
47
|
import { localeLabel } from "@jxsuite/schema/locale";
|
|
48
48
|
import {
|
|
49
49
|
LIBRARY_CATEGORIES,
|
|
@@ -760,7 +760,7 @@ function toolbarTpl(panel: ActiveLibraryPane, files: readonly LibraryFile[]) {
|
|
|
760
760
|
<input
|
|
761
761
|
type="file"
|
|
762
762
|
multiple
|
|
763
|
-
accept=${
|
|
763
|
+
accept=${uploadAccept()}
|
|
764
764
|
class="library-upload-input"
|
|
765
765
|
@change=${(e: Event) => {
|
|
766
766
|
const input = e.target as HTMLInputElement;
|
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
* already knows a preview is in flight without this module declaring an idle source of its own.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
buildScope,
|
|
24
|
+
renderNode,
|
|
25
|
+
setCanvasAssetResolver,
|
|
26
|
+
setSkipServerFunctions,
|
|
27
|
+
} from "@jxsuite/runtime";
|
|
28
|
+
import { assetContextFor, hostAssetDeclarations, resolveAssetRef } from "../canvas/asset-refs";
|
|
23
29
|
import { componentRegistry } from "../files/components";
|
|
24
30
|
import { getPlatform } from "../platform";
|
|
25
31
|
import { loadFormats, formatForPath } from "../format/format-host";
|
|
@@ -115,6 +121,7 @@ export async function renderDocPreview(filePath: string): Promise<HTMLElement |
|
|
|
115
121
|
try {
|
|
116
122
|
const content = await getPlatform().readFile(filePath);
|
|
117
123
|
setSkipServerFunctions(true);
|
|
124
|
+
const assets = assetContextFor(filePath, hostAssetDeclarations());
|
|
118
125
|
await loadFormats();
|
|
119
126
|
let document_: JxDocument;
|
|
120
127
|
if (formatForPath(filePath)) {
|
|
@@ -124,8 +131,17 @@ export async function renderDocPreview(filePath: string): Promise<HTMLElement |
|
|
|
124
131
|
document_ = JSON.parse(content) as JxDocument;
|
|
125
132
|
}
|
|
126
133
|
const scope = await buildScope(document_, {}, location.href);
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
/* This render lands in the PARENT document, so an `<img src="/hero.jpg">` inside it resolves
|
|
135
|
+
against `index.html` — which is the shell, not the site. Same hook the canvas installs, same
|
|
136
|
+
context, restored afterwards because the next preview is a different document and a stale
|
|
137
|
+
`documentDir` would resolve its relative refs against the wrong entry. */
|
|
138
|
+
setCanvasAssetResolver((value) => resolveAssetRef(value, assets));
|
|
139
|
+
try {
|
|
140
|
+
const element = renderNode(document_, scope);
|
|
141
|
+
return element instanceof HTMLElement ? element : null;
|
|
142
|
+
} finally {
|
|
143
|
+
setCanvasAssetResolver(null);
|
|
144
|
+
}
|
|
129
145
|
} catch {
|
|
130
146
|
return null;
|
|
131
147
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset references, resolved for the canvas — the half that asks the running editor.
|
|
3
|
+
*
|
|
4
|
+
* The resolution math itself is {@link file://./asset-resolve.ts}, which is pure and which the
|
|
5
|
+
* canvas iframe imports. This module is everything that has to look something up: the open
|
|
6
|
+
* project's content sections and locales, the active tab, the registered platform's declarations.
|
|
7
|
+
*
|
|
8
|
+
* The split is by REALM. The iframe bundle is deliberately dependency-light, and reaching
|
|
9
|
+
* `projectState` or `getPlatform()` from it drags the whole editor shell in behind it — which is
|
|
10
|
+
* what `check-studio-dist` caught the first time these lived in one file.
|
|
11
|
+
*
|
|
12
|
+
* @docs studio/projects/media
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { resolveI18n } from "@jxsuite/schema/locale";
|
|
16
|
+
import { BUILD_LANES, dirOfPath } from "@jxsuite/schema/asset-paths";
|
|
17
|
+
import { contentMountFor, resolveAssetRef } from "./asset-resolve";
|
|
18
|
+
import { documentBase, loopbackAssetSrc } from "./canvas-origin";
|
|
19
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
20
|
+
import { projectState } from "../store";
|
|
21
|
+
import { activeTab } from "../workspace/workspace";
|
|
22
|
+
import type { AssetContext } from "./asset-resolve";
|
|
23
|
+
import type { ContentSectionEntry } from "../types";
|
|
24
|
+
|
|
25
|
+
/* Re-exported so callers that want both halves import one module. The pure ones are defined next
|
|
26
|
+
door precisely so the iframe can reach them WITHOUT reaching any of the above. */
|
|
27
|
+
export { contentMountFor, mountedRefFor, resolveAssetRef } from "./asset-resolve";
|
|
28
|
+
export type { AssetContext } from "./asset-resolve";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The open project's canonical locale tags, or `[]` when it declares none.
|
|
32
|
+
*
|
|
33
|
+
* Through `resolveI18n` rather than off the raw config, so `EN-us` means the same thing here as it
|
|
34
|
+
* does in the compiler and the content loader — the whole reason that resolver is in
|
|
35
|
+
* `@jxsuite/schema`.
|
|
36
|
+
*/
|
|
37
|
+
export function projectLocales(): string[] {
|
|
38
|
+
return resolveI18n(projectState?.projectConfig ?? {}).i18n?.locales ?? [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The context for a document in the project currently open, or null when nothing applies.
|
|
43
|
+
*
|
|
44
|
+
* @param {string | null | undefined} documentPath - Project-relative path of the open document
|
|
45
|
+
* @param {object} [over] - Host declarations: the asset space and the project-file base URL
|
|
46
|
+
* @returns {AssetContext | null} A context, or null when no resolution is needed
|
|
47
|
+
*/
|
|
48
|
+
export function assetContextFor(
|
|
49
|
+
documentPath: string | null | undefined,
|
|
50
|
+
over?: { space?: "site" | "repo" | undefined; fileBaseUrl?: string | undefined },
|
|
51
|
+
): AssetContext | null {
|
|
52
|
+
const space = over?.space ?? "site";
|
|
53
|
+
const mount = contentMountFor(
|
|
54
|
+
documentPath,
|
|
55
|
+
projectState?.projectConfig?.content as Record<string, ContentSectionEntry> | undefined,
|
|
56
|
+
projectLocales(),
|
|
57
|
+
);
|
|
58
|
+
/* In site space the mount is the ONLY thing that needs doing, so no mount means no work. In repo
|
|
59
|
+
space every reference needs rebasing, mount or not. */
|
|
60
|
+
if (space === "site" && !mount) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
documentDir: dirOfPath(documentPath ?? ""),
|
|
65
|
+
...(over?.fileBaseUrl === undefined ? {} : { fileBaseUrl: over.fileBaseUrl }),
|
|
66
|
+
lanes: BUILD_LANES,
|
|
67
|
+
mounts: mount ? [mount] : [],
|
|
68
|
+
space,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The src a PARENT-REALM preview should load for an AUTHORED value — a media-picker thumbnail, the
|
|
74
|
+
* social card in the SEO modal.
|
|
75
|
+
*
|
|
76
|
+
* The canvas resolves its own references; panel chrome renders in the parent document, where a
|
|
77
|
+
* content-relative value would resolve against `index.html` and 404 exactly as it did in the
|
|
78
|
+
* canvas. Same mapping, same rule — resolved against the ACTIVE tab, which is the entry whose field
|
|
79
|
+
* is being edited.
|
|
80
|
+
*
|
|
81
|
+
* Takes what the author WROTE. For a project FILE PATH — a library thumbnail, a media-browser tile
|
|
82
|
+
* — use `previewFileSrc` in `files/media-paths`; the two are separate because a file path is not an
|
|
83
|
+
* authored reference and confusing them is silent.
|
|
84
|
+
*
|
|
85
|
+
* The loopback absolutization is folded in rather than left to the caller: every one of the four
|
|
86
|
+
* call sites needs both steps, and one that composed them by hand would work everywhere except the
|
|
87
|
+
* desktop shell, where a relative src resolves against `views://`.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} value - The reference as the author wrote it
|
|
90
|
+
* @returns {string} A src the parent realm can load
|
|
91
|
+
*/
|
|
92
|
+
export function previewAssetSrc(value: string): string {
|
|
93
|
+
if (!value) {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
const ctx = assetContextFor(activeTab.value?.documentPath, hostAssetDeclarations());
|
|
97
|
+
return loopbackAssetSrc(resolveAssetRef(value, ctx) ?? value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* What the registered host says about media — the platform's `assetSpace`, and the base project
|
|
102
|
+
* files are served under.
|
|
103
|
+
*
|
|
104
|
+
* Read through the platform rather than passed in, because every caller is panel chrome with no
|
|
105
|
+
* access to the render pipeline and because the answer is a property of the HOST, not of the call.
|
|
106
|
+
* The canvas pipeline builds the same two values from the same two places; the difference is only
|
|
107
|
+
* that it already has the document in hand.
|
|
108
|
+
*
|
|
109
|
+
* @returns {{ space?: "site" | "repo"; fileBaseUrl: string }} The host's declarations
|
|
110
|
+
*/
|
|
111
|
+
export function hostAssetDeclarations(): {
|
|
112
|
+
space?: "site" | "repo" | undefined;
|
|
113
|
+
fileBaseUrl: string;
|
|
114
|
+
} {
|
|
115
|
+
return {
|
|
116
|
+
fileBaseUrl: documentBase(projectState?.projectRoot),
|
|
117
|
+
...(hasPlatform() ? { space: getPlatform().assetSpace } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset references, resolved for the canvas — the PURE half.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `asset-refs.ts` for one reason, and it is a hard one: this module runs **inside the
|
|
5
|
+
* canvas iframe**, which is a deliberately dependency-light bundle. `asset-refs.ts` reaches the
|
|
6
|
+
* open project, the active tab and the registered platform, and importing any of that from the
|
|
7
|
+
* iframe drags the entire editor shell in behind it. The build gate caught exactly that.
|
|
8
|
+
*
|
|
9
|
+
* So the split is by REALM, not by taste: everything here is a pure function of its arguments, and
|
|
10
|
+
* everything that has to ask the running editor a question lives next door.
|
|
11
|
+
*
|
|
12
|
+
* A document names media in one of two shapes, and neither is a URL the canvas can use as written.
|
|
13
|
+
*
|
|
14
|
+
* A content entry references media relative to ITSELF — `` — so the file
|
|
15
|
+
* reads correctly in a plain markdown editor and so collections sourced outside the project root
|
|
16
|
+
* still reach their media (specs/site-architecture.md §9.3). On the built site the collection
|
|
17
|
+
* loader rewrites those refs onto the content type's asset mount,
|
|
18
|
+
* `/content/<type>/images/hero.png`. Studio opens an entry as a STANDALONE document, so that loader
|
|
19
|
+
* never runs.
|
|
20
|
+
*
|
|
21
|
+
* A page references media by SITE URL — `/hero.jpg` — which is a file the project tree calls
|
|
22
|
+
* `public/hero.jpg`. That works wherever the canvas is hosted by something that serves the site's
|
|
23
|
+
* own URL space, which is what the dev server and the desktop loopback both are.
|
|
24
|
+
*
|
|
25
|
+
* {@link AssetContext} is the host's answer to "which of those two do you serve", and
|
|
26
|
+
* {@link resolveAssetRef} is the whole of the resolution:
|
|
27
|
+
*
|
|
28
|
+
* - `"site"` — the origin already answers site URLs, so only the content-mount mapping is missing.
|
|
29
|
+
* This is what desktop and `jx dev` have always done, and the default when a host says nothing.
|
|
30
|
+
* - `"repo"` — nothing answers a site URL, and the host serves PROJECT PATHS under a base instead.
|
|
31
|
+
* Every reference resolves to the file it names and is rebased onto that base. The mount detour
|
|
32
|
+
* disappears: a content entry's `./images/hero.png` is already `content/posts/images/hero.png`, a
|
|
33
|
+
* real path the host can serve.
|
|
34
|
+
*
|
|
35
|
+
* The SOURCE document is never touched in either case: the authored ref is what gets serialized
|
|
36
|
+
* back to disk, and the properties panel shows the author what they wrote.
|
|
37
|
+
*
|
|
38
|
+
* One deliberate divergence from the content loader: it declines to rewrite a ref whose target does
|
|
39
|
+
* not exist (`existsSync`), leaving an unresolvable path alone rather than changing its meaning.
|
|
40
|
+
* The browser has no filesystem, so this resolves optimistically. A missing file is broken either
|
|
41
|
+
* way; pointing at the URL the built site would have attempted is the more informative failure.
|
|
42
|
+
*
|
|
43
|
+
* @docs studio/projects/media
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
assetUrlFor,
|
|
48
|
+
encodeProjectPath,
|
|
49
|
+
isNonRelativeRef,
|
|
50
|
+
joinProjectPath,
|
|
51
|
+
projectPathForRef,
|
|
52
|
+
splitRefSuffix,
|
|
53
|
+
} from "@jxsuite/schema/asset-paths";
|
|
54
|
+
import type { AssetLane, AssetMount } from "@jxsuite/schema/asset-paths";
|
|
55
|
+
import type { ContentSectionEntry } from "../types";
|
|
56
|
+
|
|
57
|
+
/** Content type names safe in a URL path segment. Mirrors `SAFE_TYPE_NAME` in the content loader. */
|
|
58
|
+
const SAFE_TYPE_NAME = /^[\w.~-]+$/;
|
|
59
|
+
|
|
60
|
+
/** The project.json section key content types live under; also their URL namespace. */
|
|
61
|
+
const SECTION_KEY = "content";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The placeholder a content type's `source` may carry to say "one directory per locale".
|
|
65
|
+
*
|
|
66
|
+
* Held here rather than imported out of the parser extension's content loader ({@link
|
|
67
|
+
* file://../../../../extensions/parser/src/content-loader.ts}): that module reads the filesystem
|
|
68
|
+
* and this one runs in a browser. It is also an extension, and core may not import one.
|
|
69
|
+
*/
|
|
70
|
+
const LOCALE_PLACEHOLDER = "{locale}";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Everything the canvas needs to resolve one reference, as PLAIN DATA.
|
|
74
|
+
*
|
|
75
|
+
* Plain data because it has to cross a realm. The resolver itself is a function and cannot be
|
|
76
|
+
* posted into the canvas iframe, so the parent posts this beside the render document and the iframe
|
|
77
|
+
* builds the resolver from it locally.
|
|
78
|
+
*/
|
|
79
|
+
export interface AssetContext {
|
|
80
|
+
/** What the canvas ORIGIN answers for a site URL — the platform's `assetSpace`, defaulted. */
|
|
81
|
+
space: "site" | "repo";
|
|
82
|
+
/**
|
|
83
|
+
* Base URL under which the host serves PROJECT PATHS. Required by `"repo"` and inert without it:
|
|
84
|
+
* a host that declares repo space and no base has told us its site URLs are wrong without telling
|
|
85
|
+
* us what is right, and inventing an answer there would be worse than leaving the ref alone.
|
|
86
|
+
*/
|
|
87
|
+
fileBaseUrl?: string | undefined;
|
|
88
|
+
/** Project-relative directory of the document being rendered (`""` at the project root). */
|
|
89
|
+
documentDir: string;
|
|
90
|
+
/** Project-relative asset mounts that apply to this document. */
|
|
91
|
+
mounts: AssetMount[];
|
|
92
|
+
/**
|
|
93
|
+
* The resolution order for a site-absolute URL.
|
|
94
|
+
*
|
|
95
|
+
* `BUILD_LANES` by default, and that is the deliberate answer rather than an omission: with no
|
|
96
|
+
* filesystem to probe, a lane list has to collapse to ONE candidate, and the one that makes the
|
|
97
|
+
* preview agree with the deployed site is the build's. See `DEV_SERVER_LANES` for the order the
|
|
98
|
+
* editing servers actually use, and why the two differ.
|
|
99
|
+
*/
|
|
100
|
+
lanes: readonly AssetLane[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The asset mount for the content type that owns `documentPath`, or null when the document is not a
|
|
105
|
+
* directory-backed content entry.
|
|
106
|
+
*
|
|
107
|
+
* Only in-project sources can match: an entry Studio can address project-relatively is by
|
|
108
|
+
* definition inside the project, and a source pointing elsewhere (`../../docs`) names files the
|
|
109
|
+
* file tree cannot reach in the first place.
|
|
110
|
+
*
|
|
111
|
+
* @param {string | null | undefined} documentPath - Project-relative path of the open document
|
|
112
|
+
* @param {Record<string, ContentSectionEntry> | null | undefined} content - Project.json `content`
|
|
113
|
+
* @param {readonly string[]} [locales] - Canonical locale tags, for `{locale}` sources
|
|
114
|
+
* @returns {AssetMount | null} The mount, project-relative
|
|
115
|
+
*/
|
|
116
|
+
export function contentMountFor(
|
|
117
|
+
documentPath: string | null | undefined,
|
|
118
|
+
content: Record<string, ContentSectionEntry> | null | undefined,
|
|
119
|
+
locales: readonly string[] = [],
|
|
120
|
+
): AssetMount | null {
|
|
121
|
+
if (!documentPath || !content) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const path = documentPath.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
125
|
+
let best: AssetMount | null = null;
|
|
126
|
+
const consider = (dir: string, urlPrefix: string): void => {
|
|
127
|
+
if (path.startsWith(`${dir}/`) && (!best || dir.length > best.dir.length)) {
|
|
128
|
+
best = { dir, urlPrefix };
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
for (const [name, def] of Object.entries(content)) {
|
|
132
|
+
if (!def?.source || !SAFE_TYPE_NAME.test(name)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const dir = normalizeSource(def.source);
|
|
136
|
+
// A single-file source is not a collection directory — its siblings are not its assets.
|
|
137
|
+
if (dir === "" || dir.startsWith("..")) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (dir.includes(LOCALE_PLACEHOLDER)) {
|
|
141
|
+
const mount = localeMountFor(path, dir, name, locales);
|
|
142
|
+
if (mount) {
|
|
143
|
+
consider(mount.dir, mount.urlPrefix);
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (/\.[a-z0-9]+$/i.test(dir)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
consider(dir, `/${SECTION_KEY}/${name}`);
|
|
151
|
+
}
|
|
152
|
+
return best;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The mount for ONE locale of a `{locale}` source — the locale being whichever one `path` is in.
|
|
157
|
+
*
|
|
158
|
+
* A localized content type is N directories, not N content types, and each publishes separately at
|
|
159
|
+
* `/content/<type>/<locale>` so a French post's `./hero.png` and its English translation's cannot
|
|
160
|
+
* collide at one URL. The content loader picks the directory by probing the disk (`localeSource`);
|
|
161
|
+
* Studio has no disk, so it reads the locale out of the document path it was given and checks that
|
|
162
|
+
* against the project's declared tags. That is not a weaker test — the path IS the file, so the
|
|
163
|
+
* only question left is whether its directory is a locale the project declares.
|
|
164
|
+
*
|
|
165
|
+
* Matched case-insensitively, for the same reason the loader does: two writers name that directory
|
|
166
|
+
* and they differ. A project that declared `fr-CA` most likely typed `fr-CA/`, while Studio creates
|
|
167
|
+
* `fr-ca/` because a page directory becomes a URL and the site's URLs are lowercase. The mount's
|
|
168
|
+
* `dir` is the spelling on disk and its `urlPrefix` is the CANONICAL tag, exactly as the loader
|
|
169
|
+
* publishes it.
|
|
170
|
+
*/
|
|
171
|
+
function localeMountFor(
|
|
172
|
+
path: string,
|
|
173
|
+
source: string,
|
|
174
|
+
name: string,
|
|
175
|
+
locales: readonly string[],
|
|
176
|
+
): AssetMount | null {
|
|
177
|
+
const cut = source.indexOf(LOCALE_PLACEHOLDER);
|
|
178
|
+
const head = source.slice(0, cut);
|
|
179
|
+
const tail = source.slice(cut + LOCALE_PLACEHOLDER.length);
|
|
180
|
+
if (!path.startsWith(head)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const segment = path.slice(head.length).split("/")[0] ?? "";
|
|
184
|
+
if (segment === "" || !SAFE_TYPE_NAME.test(segment)) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const canonical = locales.find((tag) => tag.toLowerCase() === segment.toLowerCase());
|
|
188
|
+
if (canonical === undefined || !SAFE_TYPE_NAME.test(canonical)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const dir = `${head}${segment}${tail}`.replace(/\/+$/, "");
|
|
192
|
+
return { dir, urlPrefix: `/${SECTION_KEY}/${name}/${canonical}` };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Strip `./` and any trailing slash from a configured `source`, the way collectionDirs does. */
|
|
196
|
+
function normalizeSource(source: string): string {
|
|
197
|
+
return source.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The URL the canvas should load for one authored reference, or null to leave it as written.
|
|
202
|
+
*
|
|
203
|
+
* Null is not a failure — it is most of the answers. An absolute URL, a `data:` URI, a `${…}`
|
|
204
|
+
* template that did not resolve to a project file, a `../` escape out of the collection: each one
|
|
205
|
+
* already means what it says, and changing it would be the bug.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} value - The reference as the author wrote it
|
|
208
|
+
* @param {AssetContext | null} ctx - The host's asset context, or null for no resolution at all
|
|
209
|
+
* @returns {string | null} The URL to load, or null to leave `value` untouched
|
|
210
|
+
*/
|
|
211
|
+
export function resolveAssetRef(value: string, ctx: AssetContext | null): string | null {
|
|
212
|
+
if (!ctx || value === "") {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (ctx.space === "repo") {
|
|
216
|
+
if (!ctx.fileBaseUrl) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const path = projectPathForRef(value, ctx.documentDir, ctx.mounts, ctx.lanes);
|
|
220
|
+
if (path === null) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const { suffix } = splitRefSuffix(value);
|
|
224
|
+
return `${ctx.fileBaseUrl}${encodeProjectPath(path)}${suffix}`;
|
|
225
|
+
}
|
|
226
|
+
const [mount] = ctx.mounts;
|
|
227
|
+
return mount ? mountedRefFor(value, ctx.documentDir, mount) : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The mounted URL for one content-relative reference, or null to leave it as written. A ref is
|
|
232
|
+
* rewritten only when it is relative and lands inside the collection directory — anything else (an
|
|
233
|
+
* absolute URL, a project-root path, a bound template, a `../` escape out of the collection) keeps
|
|
234
|
+
* the meaning it already had.
|
|
235
|
+
*/
|
|
236
|
+
export function mountedRefFor(value: string, entryDir: string, mount: AssetMount): string | null {
|
|
237
|
+
if (isNonRelativeRef(value)) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const { path: bare, suffix } = splitRefSuffix(value);
|
|
241
|
+
let decoded: string;
|
|
242
|
+
try {
|
|
243
|
+
decoded = decodeURIComponent(bare);
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const target = joinProjectPath(entryDir, decoded);
|
|
248
|
+
if (target === null) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const url = assetUrlFor([mount], target);
|
|
252
|
+
if (!url) {
|
|
253
|
+
return null; // Outside the collection — not the mount's to publish.
|
|
254
|
+
}
|
|
255
|
+
return `${url}${suffix}`;
|
|
256
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
resolveLayoutDoc,
|
|
21
21
|
} from "../site-context";
|
|
22
22
|
import { documentBase } from "./canvas-origin";
|
|
23
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
23
24
|
import { componentRegistry, computeRelativePath } from "../files/components";
|
|
24
25
|
import { prepareForEditMode } from "../utils/edit-display";
|
|
25
26
|
import {
|
|
@@ -28,11 +29,11 @@ import {
|
|
|
28
29
|
substitutePreviewParams,
|
|
29
30
|
} from "../page-params";
|
|
30
31
|
import { isComponentDoc, substitutePreviewProps } from "../component-props";
|
|
31
|
-
import {
|
|
32
|
+
import { assetContextFor } from "./asset-refs";
|
|
32
33
|
|
|
34
|
+
import type { AssetContext } from "./asset-refs";
|
|
33
35
|
import type { JxElement, JxMutableNode, JxPath } from "@jxsuite/schema/types";
|
|
34
36
|
import type { ComponentEntry } from "../files/components.js";
|
|
35
|
-
import type { ContentSectionEntry } from "../types";
|
|
36
37
|
import type { LayoutMarker } from "./path-mapping";
|
|
37
38
|
import type { Tab } from "../tabs/tab";
|
|
38
39
|
import type { WireMapperCtx } from "./iframe-protocol";
|
|
@@ -138,6 +139,8 @@ export async function resolveCanvasDocument(
|
|
|
138
139
|
): Promise<{
|
|
139
140
|
renderDoc: JxMutableNode;
|
|
140
141
|
docBase: string | undefined;
|
|
142
|
+
/** How the canvas must address project media, or null when its origin serves the site URL space. */
|
|
143
|
+
assets: AssetContext | null;
|
|
141
144
|
mapperCtx: WireMapperCtx;
|
|
142
145
|
siteStyle: Record<string, unknown> | null;
|
|
143
146
|
}> {
|
|
@@ -179,9 +182,23 @@ export async function resolveCanvasDocument(
|
|
|
179
182
|
}
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
const fileBase = documentBase(projectState?.projectRoot);
|
|
186
|
+
const docBase = S.documentPath ? new URL(S.documentPath, fileBase).href : undefined;
|
|
187
|
+
/*
|
|
188
|
+
* How this render addresses media, as PLAIN DATA — the resolver itself is a function and cannot
|
|
189
|
+
* cross into the canvas realm, so the iframe rebuilds it from this.
|
|
190
|
+
*
|
|
191
|
+
* It replaces a walk over the render document that used to sit further down this function. A walk
|
|
192
|
+
* can only ever see LITERAL values: `applyAttributes` resolves a `{"$ref": …}` or `"${…}"` src
|
|
193
|
+
* inside a reactive effect, so at walk time a bound image src is not a string at all. That is why
|
|
194
|
+
* a collection listing of thirty bound card images broke all at once in preview and nowhere else
|
|
195
|
+
* — design and edit swap every bound src for a transparent pixel, so they never reached the
|
|
196
|
+
* network to fail.
|
|
197
|
+
*/
|
|
198
|
+
const assets = assetContextFor(S.documentPath, {
|
|
199
|
+
fileBaseUrl: fileBase,
|
|
200
|
+
...(hasPlatform() ? { space: getPlatform().assetSpace } : {}),
|
|
201
|
+
});
|
|
185
202
|
|
|
186
203
|
// Substitute chosen dynamic route params ({$ref: "#/$params/x"} → literal), inject state.$page,
|
|
187
204
|
// And bake the substituted class-prototype state entries via the backend resolver — in every
|
|
@@ -210,16 +227,6 @@ export async function resolveCanvasDocument(
|
|
|
210
227
|
}
|
|
211
228
|
}
|
|
212
229
|
|
|
213
|
-
// A content entry references its media relative to ITSELF; the built site serves those files from
|
|
214
|
-
// The content type's asset mount. Studio opens the entry standalone, so the collection loader that
|
|
215
|
-
// Normally performs that mapping never runs — do it here, on the RENDER doc only, so the canvas
|
|
216
|
-
// Previews the URL production serves while the source doc keeps the authored relative ref.
|
|
217
|
-
renderDoc = rewriteContentAssets(
|
|
218
|
-
renderDoc,
|
|
219
|
-
S.documentPath,
|
|
220
|
-
projectState?.projectConfig?.content as Record<string, ContentSectionEntry> | undefined,
|
|
221
|
-
);
|
|
222
|
-
|
|
223
230
|
const arrayPaths = new Set<string>();
|
|
224
231
|
if (canvasMode === "design" || canvasMode === "edit") {
|
|
225
232
|
(function findArrayPaths(node: JxMutableNode, path: (string | number)[]) {
|
|
@@ -321,6 +328,7 @@ export async function resolveCanvasDocument(
|
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
return {
|
|
331
|
+
assets,
|
|
324
332
|
docBase,
|
|
325
333
|
mapperCtx: {
|
|
326
334
|
arrayPaths: [...arrayPaths],
|
|
@@ -2775,6 +2775,7 @@ function preparePassRender(
|
|
|
2775
2775
|
const formatElements = formatByName(renderTab?.doc.sourceFormat)?.studio?.elements;
|
|
2776
2776
|
const editableTags = formatElements ? formatEditableVerdicts(formatElements) : undefined;
|
|
2777
2777
|
return {
|
|
2778
|
+
assets: resolved.assets,
|
|
2778
2779
|
doc: cloneableDoc,
|
|
2779
2780
|
docBase: resolved.docBase ?? `${canvasBaseOrigin()}/`,
|
|
2780
2781
|
mapperCtx: resolved.mapperCtx,
|
|
@@ -2898,6 +2899,9 @@ export function mountStylebookCanvas(
|
|
|
2898
2899
|
const message: ParentToIframe = {
|
|
2899
2900
|
// The specimen carries no tab identity, but the STAGE it is mounted into does: a stylebook is
|
|
2900
2901
|
// Opened as a tab like anything else, and its `previewColorScheme` is that tab's.
|
|
2902
|
+
/* A stylebook specimen is generated, not a project document, so it references no project
|
|
2903
|
+
media and needs no resolution. */
|
|
2904
|
+
assets: null,
|
|
2901
2905
|
colorScheme: schemeWireFor(tabOfContainer(canvasEl)),
|
|
2902
2906
|
doc: cloneableDoc,
|
|
2903
2907
|
docBase: `${canvasBaseOrigin()}/`,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* the cross-frame analog of the legacy renderer's `renderGeneration` staleness guard.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { AssetContext } from "./asset-resolve";
|
|
12
13
|
import type { JxDocOp } from "../tabs/patch-ops";
|
|
13
14
|
import type { JxContentResult, SlashCommand } from "../editor/inline-edit";
|
|
14
15
|
import type { JxExpressionNode, JxMutableNode } from "@jxsuite/schema/types";
|
|
@@ -88,6 +89,14 @@ export type ParentToIframe =
|
|
|
88
89
|
*/
|
|
89
90
|
allowAutoRequests?: boolean;
|
|
90
91
|
docBase: string;
|
|
92
|
+
/**
|
|
93
|
+
* How this host addresses project media, or null/absent when the canvas origin serves the
|
|
94
|
+
* site's own URL space (desktop, `jx dev`) and no reference needs resolving.
|
|
95
|
+
*
|
|
96
|
+
* Plain data because the resolver is a FUNCTION and functions do not cross a realm: the
|
|
97
|
+
* iframe rebuilds the resolver from this and installs it on the runtime.
|
|
98
|
+
*/
|
|
99
|
+
assets?: AssetContext | null;
|
|
91
100
|
mapperCtx: WireMapperCtx;
|
|
92
101
|
siteStyle: Record<string, unknown> | null;
|
|
93
102
|
// Forced color-scheme preview (spec §9.5): "light"/"dark" sets data-color-scheme on the
|