@valbuild/shared 0.102.0 → 0.103.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.
@@ -15,3 +15,4 @@ export * from "./routeValidation.js";
15
15
  export * from "./resolveSchemaSourceFixes.js";
16
16
  export * from "./getErrorMessageFromUnknownJson.js";
17
17
  export * from "./zod/SerializedSchema.js";
18
+ export * from "./valCanvasProtocol.js";
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The protocol between the studio's canvas and the page inside it.
3
+ *
4
+ * The canvas shows the running site in a frame. That frame is a different
5
+ * document — the customer's app, rendered by their components — so the studio
6
+ * cannot reach into it, and it cannot reach out. Everything they say to each
7
+ * other goes through `postMessage`, and this is the vocabulary.
8
+ *
9
+ * It lives in `shared` because both halves have to agree on it: the page half
10
+ * ships in `@valbuild/next`, the studio half in `@valbuild/ui`, and a message
11
+ * shape defined twice is a message shape that will differ once.
12
+ *
13
+ * ## Why a query parameter turns it on
14
+ *
15
+ * The frame has to render the page as Val sees it — draft content, with
16
+ * `data-val-path` on every element Val tracks — while *not* showing Val's own
17
+ * overlay, because the studio is already the UI. Nothing about the cookies can
18
+ * express that: they are per-origin, and the studio and the page share an
19
+ * origin, so a cookie would change the customer's own tabs too. A parameter on
20
+ * the frame's URL is scoped to exactly the one document that needs it.
21
+ */
22
+ import type { ModuleFilePath, SourcePath } from "@valbuild/core";
23
+ /** Marks a page load as the studio's canvas frame. */
24
+ export declare const VAL_CANVAS_PARAM = "val_canvas";
25
+ /**
26
+ * Every message is tagged with this, and the tag is checked on receipt.
27
+ *
28
+ * A window receives messages from anything that has a handle on it —
29
+ * extensions, embeds, other frames — and `event.data` is whatever they chose to
30
+ * send. Checking a discriminator before reading anything else is what keeps a
31
+ * stray message from being read as a canvas instruction.
32
+ */
33
+ export declare const VAL_CANVAS_MESSAGE = "val-canvas";
34
+ /** One element on the page that Val has a path for. */
35
+ export type ValCanvasElement = {
36
+ /**
37
+ * The paths on the element, already split.
38
+ *
39
+ * `data-val-path` holds a comma-separated list, because one element can
40
+ * render more than one piece of content — a heading built from two fields is
41
+ * one element with two paths.
42
+ *
43
+ * Typed as `SourcePath` rather than `string` because that is what they are:
44
+ * the page splits the attribute with Val's own splitter, so the values arrive
45
+ * already being paths. Saying so here is what keeps every reader of them from
46
+ * having to assert it again.
47
+ */
48
+ paths: SourcePath[];
49
+ /** Where it is, in the page's own coordinates (unscaled, page-relative). */
50
+ rect: {
51
+ top: number;
52
+ left: number;
53
+ width: number;
54
+ height: number;
55
+ };
56
+ };
57
+ /** What the page tells the studio. */
58
+ export type ValCanvasPageMessage = {
59
+ val: typeof VAL_CANVAS_MESSAGE;
60
+ type: "ready";
61
+ /**
62
+ * Whether the page is actually rendering draft content.
63
+ *
64
+ * The studio asks the server the same question, but the server answers
65
+ * for the *next* request; this answers for the document on screen, which
66
+ * is the one the person is looking at.
67
+ */
68
+ draftMode: boolean;
69
+ /** The URL the frame ended up on, which a redirect may have changed. */
70
+ url: string;
71
+ } | {
72
+ val: typeof VAL_CANVAS_MESSAGE;
73
+ type: "elements";
74
+ elements: ValCanvasElement[];
75
+ /** The page's full layout size, for mapping a click back to an element. */
76
+ pageSize: {
77
+ width: number;
78
+ height: number;
79
+ };
80
+ } | {
81
+ val: typeof VAL_CANVAS_MESSAGE;
82
+ type: "clicked";
83
+ /** The paths on the element that was clicked. */
84
+ paths: SourcePath[];
85
+ } | {
86
+ val: typeof VAL_CANVAS_MESSAGE;
87
+ type: "refreshing";
88
+ /**
89
+ * Whether the page is re-rendering because of an edit.
90
+ *
91
+ * An edit reaches the page in two steps of very different cost: the
92
+ * client store updates at once, and anything rendered on the SERVER waits
93
+ * for a `router.refresh()` and its round trip — which in development is
94
+ * long enough to look like nothing happened. The studio cannot see that
95
+ * from outside, so the page says so and the canvas can show it is working.
96
+ */
97
+ pending: boolean;
98
+ };
99
+ /** What the studio tells the page. */
100
+ export type ValCanvasStudioMessage = {
101
+ val: typeof VAL_CANVAS_MESSAGE;
102
+ type: "rescan";
103
+ } | {
104
+ val: typeof VAL_CANVAS_MESSAGE;
105
+ type: "highlight";
106
+ /** The path to outline, or null to clear the outline. */
107
+ path: SourcePath | null;
108
+ /** Bring it into view as well as outlining it. */
109
+ scrollIntoView?: boolean;
110
+ } | {
111
+ /**
112
+ * A module's source moved, so the page should re-render with it.
113
+ *
114
+ * The page already knows how to do this — it is what makes an inline edit
115
+ * visible behind the overlay — but it learns about it from a CustomEvent
116
+ * on its own window, and the studio is in a different window. So the
117
+ * update is relayed and the bridge re-dispatches it where the page is
118
+ * listening.
119
+ *
120
+ * Without it the canvas is only correct immediately after a load: a
121
+ * server component re-reads content when the page is requested, so
122
+ * everything typed since then is invisible until a reload.
123
+ */
124
+ val: typeof VAL_CANVAS_MESSAGE;
125
+ type: "sourceUpdate";
126
+ moduleFilePath: ModuleFilePath;
127
+ /**
128
+ * The whole module source. `unknown` because the page's own types are the
129
+ * ones that give it meaning; nothing in between should be interpreting it.
130
+ */
131
+ source: unknown;
132
+ } | {
133
+ /**
134
+ * Everything the studio holds has been sent.
135
+ *
136
+ * The page waits for draft sources before it renders, and it cannot know
137
+ * how many to expect. The studio only sends modules it has PATCHES for —
138
+ * an unedited module has no draft, so there is nothing to send — and the
139
+ * page cannot tell "not sent yet" from "nothing to send". Left to a
140
+ * timeout, every page reading an unedited module stalled ten seconds and
141
+ * then rendered committed source; the module that mattered, a router with
142
+ * a brand new page in it, was the one whose absence looked identical.
143
+ *
144
+ * So the studio says when it has finished. After this, a module the page
145
+ * has not been given has no draft, and committed source IS the draft.
146
+ */
147
+ val: typeof VAL_CANVAS_MESSAGE;
148
+ type: "sourcesSynced";
149
+ } | {
150
+ val: typeof VAL_CANVAS_MESSAGE;
151
+ type: "setPicking";
152
+ /**
153
+ * Whether a click picks an element instead of doing what the page would
154
+ * do with it.
155
+ *
156
+ * Off by default: in the normal view the canvas is the page, and a click
157
+ * on a link should follow the link.
158
+ */
159
+ picking: boolean;
160
+ };
161
+ /** Narrow an unknown `event.data` to a message from the page. */
162
+ export declare function isValCanvasPageMessage(data: unknown): data is ValCanvasPageMessage;
163
+ /** Narrow an unknown `event.data` to a message from the studio. */
164
+ export declare function isValCanvasStudioMessage(data: unknown): data is ValCanvasStudioMessage;
165
+ /**
166
+ * Whether this document was loaded as the studio's canvas frame.
167
+ *
168
+ * Reads the location directly rather than taking it as an argument, because
169
+ * every caller wants the answer for the document it is running in, and passing
170
+ * it around is how one of them ends up asking about the wrong one.
171
+ */
172
+ export declare function isValCanvasFrame(search: string): boolean;
173
+ /** Add the canvas marker to a page URL. */
174
+ export declare function withValCanvasParam(url: string): string;
@@ -351,22 +351,21 @@ function convertImageNode(path, node, files, configDirectory, remoteOptions) {
351
351
  }
352
352
  return {
353
353
  tag: "img",
354
- src: _objectSpread2(_objectSpread2(_defineProperty({}, core.FILE_REF_PROP, ref), remoteOptions ? _defineProperty({}, core.VAL_EXTENSION, "remote") : _defineProperty(_defineProperty({}, core.VAL_EXTENSION, "file"), core.FILE_REF_SUBTYPE_TAG, "image")), {}, {
355
- metadata: {
356
- width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
357
- height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
358
- mimeType: mimeType
359
- }
360
- })
354
+ src: {
355
+ path: ref,
356
+ width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
357
+ height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
358
+ mimeType: mimeType
359
+ }
361
360
  };
362
361
  } else if (node.attrs) {
363
362
  var url = node.attrs.src;
364
363
  var patchId = getParam("patch_id", url);
364
+ // Recover the path the URL was built from. Local and remote produce the
365
+ // same source shape now, so this is only about undoing the URL rule.
365
366
  var noParamsUrl = url.split("?")[0];
366
- var remote = false;
367
367
  if (patchId) {
368
- remote = getParam("remote", url) === "true";
369
- if (remote) {
368
+ if (getParam("remote", url) === "true") {
370
369
  var remoteRef = getParam("ref", url);
371
370
  if (remoteRef) {
372
371
  noParamsUrl = decodeURIComponent(remoteRef);
@@ -378,23 +377,18 @@ function convertImageNode(path, node, files, configDirectory, remoteOptions) {
378
377
  } else {
379
378
  console.error("Patched image URL does not start with /api/val/files: " + url);
380
379
  }
380
+ } else if (remoteOptions && noParamsUrl.startsWith(remoteOptions.remoteHost)) ; else if (!noParamsUrl.startsWith("/public")) {
381
+ noParamsUrl = "/public".concat(noParamsUrl);
381
382
  } else {
382
- if (remoteOptions && noParamsUrl.startsWith(remoteOptions.remoteHost)) {
383
- remote = true;
384
- } else if (!noParamsUrl.startsWith("/public")) {
385
- noParamsUrl = "/public".concat(noParamsUrl);
386
- } else {
387
- console.error("Unpatched image URL starts with /public: " + url);
388
- }
383
+ console.error("Unpatched image URL starts with /public: " + url);
389
384
  }
390
385
  var tag = {
391
386
  tag: "img",
392
- src: _objectSpread2(_objectSpread2({}, remote ? _defineProperty(_defineProperty({}, core.VAL_EXTENSION, "remote"), core.FILE_REF_PROP, noParamsUrl) : _defineProperty(_defineProperty(_defineProperty({}, core.VAL_EXTENSION, "file"), core.FILE_REF_SUBTYPE_TAG, "image"), core.FILE_REF_PROP, noParamsUrl)), {}, {
393
- metadata: {
394
- width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
395
- height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
396
- mimeType: noParamsUrl && core.Internal.filenameToMimeType(noParamsUrl) || ""
397
- }
387
+ src: _objectSpread2({
388
+ path: noParamsUrl,
389
+ width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
390
+ height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
391
+ mimeType: noParamsUrl && core.Internal.filenameToMimeType(noParamsUrl) || ""
398
392
  }, patchId ? {
399
393
  patch_id: patchId
400
394
  } : {})
@@ -675,24 +669,17 @@ function convertLinkNodeToRemirror(linkNode) {
675
669
  });
676
670
  }
677
671
  function convertImageNodeToRemirror(imageNode) {
678
- var _fileSource$metadata, _fileSource$metadata2, _fileSource$metadata3;
679
- var fileSource = imageNode.src;
680
- if (!(core.VAL_EXTENSION in fileSource) || fileSource[core.VAL_EXTENSION] !== "file" && fileSource[core.VAL_EXTENSION] !== "remote") {
681
- throw Error("Expected file source in image node");
682
- }
683
- var fileVal;
684
- if (fileSource[core.VAL_EXTENSION] === "remote") {
685
- fileVal = core.Internal.convertRemoteSource(fileSource);
686
- } else {
687
- fileVal = core.Internal.convertFileSource(fileSource);
672
+ var imageSource = imageNode.src;
673
+ if (typeof (imageSource === null || imageSource === void 0 ? void 0 : imageSource.path) !== "string") {
674
+ throw Error("Expected image source in image node");
688
675
  }
689
676
  return {
690
677
  type: "image",
691
678
  attrs: {
692
- height: (_fileSource$metadata = fileSource.metadata) === null || _fileSource$metadata === void 0 ? void 0 : _fileSource$metadata.height,
693
- width: (_fileSource$metadata2 = fileSource.metadata) === null || _fileSource$metadata2 === void 0 ? void 0 : _fileSource$metadata2.width,
694
- alt: (_fileSource$metadata3 = fileSource.metadata) === null || _fileSource$metadata3 === void 0 ? void 0 : _fileSource$metadata3.alt,
695
- src: fileVal.url
679
+ height: imageSource.height,
680
+ width: imageSource.width,
681
+ alt: imageSource.alt,
682
+ src: core.Internal.mediaUrl(imageSource)
696
683
  }
697
684
  };
698
685
  }
@@ -3297,6 +3284,84 @@ function resolveSchemaSourceFixes(validationErrors, snapshot) {
3297
3284
  return result;
3298
3285
  }
3299
3286
 
3287
+ /**
3288
+ * The protocol between the studio's canvas and the page inside it.
3289
+ *
3290
+ * The canvas shows the running site in a frame. That frame is a different
3291
+ * document — the customer's app, rendered by their components — so the studio
3292
+ * cannot reach into it, and it cannot reach out. Everything they say to each
3293
+ * other goes through `postMessage`, and this is the vocabulary.
3294
+ *
3295
+ * It lives in `shared` because both halves have to agree on it: the page half
3296
+ * ships in `@valbuild/next`, the studio half in `@valbuild/ui`, and a message
3297
+ * shape defined twice is a message shape that will differ once.
3298
+ *
3299
+ * ## Why a query parameter turns it on
3300
+ *
3301
+ * The frame has to render the page as Val sees it — draft content, with
3302
+ * `data-val-path` on every element Val tracks — while *not* showing Val's own
3303
+ * overlay, because the studio is already the UI. Nothing about the cookies can
3304
+ * express that: they are per-origin, and the studio and the page share an
3305
+ * origin, so a cookie would change the customer's own tabs too. A parameter on
3306
+ * the frame's URL is scoped to exactly the one document that needs it.
3307
+ */
3308
+
3309
+ /** Marks a page load as the studio's canvas frame. */
3310
+ var VAL_CANVAS_PARAM = "val_canvas";
3311
+
3312
+ /**
3313
+ * Every message is tagged with this, and the tag is checked on receipt.
3314
+ *
3315
+ * A window receives messages from anything that has a handle on it —
3316
+ * extensions, embeds, other frames — and `event.data` is whatever they chose to
3317
+ * send. Checking a discriminator before reading anything else is what keeps a
3318
+ * stray message from being read as a canvas instruction.
3319
+ */
3320
+ var VAL_CANVAS_MESSAGE = "val-canvas";
3321
+
3322
+ /** One element on the page that Val has a path for. */
3323
+
3324
+ /** What the page tells the studio. */
3325
+
3326
+ /** What the studio tells the page. */
3327
+
3328
+ /** Narrow an unknown `event.data` to a message from the page. */
3329
+ function isValCanvasPageMessage(data) {
3330
+ if (_typeof(data) !== "object" || data === null) return false;
3331
+ var message = data;
3332
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3333
+ return message.type === "ready" || message.type === "elements" || message.type === "clicked" || message.type === "refreshing";
3334
+ }
3335
+
3336
+ /** Narrow an unknown `event.data` to a message from the studio. */
3337
+ function isValCanvasStudioMessage(data) {
3338
+ if (_typeof(data) !== "object" || data === null) return false;
3339
+ var message = data;
3340
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3341
+ return message.type === "rescan" || message.type === "highlight" || message.type === "setPicking" || message.type === "sourceUpdate" || message.type === "sourcesSynced";
3342
+ }
3343
+
3344
+ /**
3345
+ * Whether this document was loaded as the studio's canvas frame.
3346
+ *
3347
+ * Reads the location directly rather than taking it as an argument, because
3348
+ * every caller wants the answer for the document it is running in, and passing
3349
+ * it around is how one of them ends up asking about the wrong one.
3350
+ */
3351
+ function isValCanvasFrame(search) {
3352
+ try {
3353
+ return new URLSearchParams(search).get(VAL_CANVAS_PARAM) !== null;
3354
+ } catch (_unused) {
3355
+ return false;
3356
+ }
3357
+ }
3358
+
3359
+ /** Add the canvas marker to a page URL. */
3360
+ function withValCanvasParam(url) {
3361
+ var separator = url.includes("?") ? "&" : "?";
3362
+ return "".concat(url).concat(separator).concat(VAL_CANVAS_PARAM, "=1");
3363
+ }
3364
+
3300
3365
  exports.Api = Api;
3301
3366
  exports.ColorOptions = ColorOptions;
3302
3367
  exports.DEFAULT_ERROR_MESSAGE = DEFAULT_ERROR_MESSAGE;
@@ -3340,6 +3405,8 @@ exports.SerializedStringSchema = SerializedStringSchema;
3340
3405
  exports.SerializedUnionSchema = SerializedUnionSchema;
3341
3406
  exports.SharedValConfig = SharedValConfig;
3342
3407
  exports.VAL_AI_SESSION_STORAGE_KEY = VAL_AI_SESSION_STORAGE_KEY;
3408
+ exports.VAL_CANVAS_MESSAGE = VAL_CANVAS_MESSAGE;
3409
+ exports.VAL_CANVAS_PARAM = VAL_CANVAS_PARAM;
3343
3410
  exports.VAL_CONFIG_SESSION_STORAGE_KEY = VAL_CONFIG_SESSION_STORAGE_KEY;
3344
3411
  exports.VAL_ENABLE_COOKIE_NAME = VAL_ENABLE_COOKIE_NAME;
3345
3412
  exports.VAL_SESSION_COOKIE = VAL_SESSION_COOKIE;
@@ -3357,6 +3424,9 @@ exports.getNextAppRouterSourceFolder = getNextAppRouterSourceFolder;
3357
3424
  exports.getPatternFromModuleFilePath = getPatternFromModuleFilePath;
3358
3425
  exports.getRoutesOf = getRoutesOf;
3359
3426
  exports.getRoutesWithModulePaths = getRoutesWithModulePaths;
3427
+ exports.isValCanvasFrame = isValCanvasFrame;
3428
+ exports.isValCanvasPageMessage = isValCanvasPageMessage;
3429
+ exports.isValCanvasStudioMessage = isValCanvasStudioMessage;
3360
3430
  exports.levenshtein = levenshtein;
3361
3431
  exports.parseRoutePattern = parseRoutePattern;
3362
3432
  exports.remirrorToRichTextSource = remirrorToRichTextSource;
@@ -3365,3 +3435,4 @@ exports.resolveSchemaSourceFixes = resolveSchemaSourceFixes;
3365
3435
  exports.richTextToRemirror = richTextToRemirror;
3366
3436
  exports.urlOf = urlOf;
3367
3437
  exports.validateRoutePatterns = validateRoutePatterns;
3438
+ exports.withValCanvasParam = withValCanvasParam;
@@ -351,22 +351,21 @@ function convertImageNode(path, node, files, configDirectory, remoteOptions) {
351
351
  }
352
352
  return {
353
353
  tag: "img",
354
- src: _objectSpread2(_objectSpread2(_defineProperty({}, core.FILE_REF_PROP, ref), remoteOptions ? _defineProperty({}, core.VAL_EXTENSION, "remote") : _defineProperty(_defineProperty({}, core.VAL_EXTENSION, "file"), core.FILE_REF_SUBTYPE_TAG, "image")), {}, {
355
- metadata: {
356
- width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
357
- height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
358
- mimeType: mimeType
359
- }
360
- })
354
+ src: {
355
+ path: ref,
356
+ width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
357
+ height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
358
+ mimeType: mimeType
359
+ }
361
360
  };
362
361
  } else if (node.attrs) {
363
362
  var url = node.attrs.src;
364
363
  var patchId = getParam("patch_id", url);
364
+ // Recover the path the URL was built from. Local and remote produce the
365
+ // same source shape now, so this is only about undoing the URL rule.
365
366
  var noParamsUrl = url.split("?")[0];
366
- var remote = false;
367
367
  if (patchId) {
368
- remote = getParam("remote", url) === "true";
369
- if (remote) {
368
+ if (getParam("remote", url) === "true") {
370
369
  var remoteRef = getParam("ref", url);
371
370
  if (remoteRef) {
372
371
  noParamsUrl = decodeURIComponent(remoteRef);
@@ -378,23 +377,18 @@ function convertImageNode(path, node, files, configDirectory, remoteOptions) {
378
377
  } else {
379
378
  console.error("Patched image URL does not start with /api/val/files: " + url);
380
379
  }
380
+ } else if (remoteOptions && noParamsUrl.startsWith(remoteOptions.remoteHost)) ; else if (!noParamsUrl.startsWith("/public")) {
381
+ noParamsUrl = "/public".concat(noParamsUrl);
381
382
  } else {
382
- if (remoteOptions && noParamsUrl.startsWith(remoteOptions.remoteHost)) {
383
- remote = true;
384
- } else if (!noParamsUrl.startsWith("/public")) {
385
- noParamsUrl = "/public".concat(noParamsUrl);
386
- } else {
387
- console.error("Unpatched image URL starts with /public: " + url);
388
- }
383
+ console.error("Unpatched image URL starts with /public: " + url);
389
384
  }
390
385
  var tag = {
391
386
  tag: "img",
392
- src: _objectSpread2(_objectSpread2({}, remote ? _defineProperty(_defineProperty({}, core.VAL_EXTENSION, "remote"), core.FILE_REF_PROP, noParamsUrl) : _defineProperty(_defineProperty(_defineProperty({}, core.VAL_EXTENSION, "file"), core.FILE_REF_SUBTYPE_TAG, "image"), core.FILE_REF_PROP, noParamsUrl)), {}, {
393
- metadata: {
394
- width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
395
- height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
396
- mimeType: noParamsUrl && core.Internal.filenameToMimeType(noParamsUrl) || ""
397
- }
387
+ src: _objectSpread2({
388
+ path: noParamsUrl,
389
+ width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
390
+ height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
391
+ mimeType: noParamsUrl && core.Internal.filenameToMimeType(noParamsUrl) || ""
398
392
  }, patchId ? {
399
393
  patch_id: patchId
400
394
  } : {})
@@ -675,24 +669,17 @@ function convertLinkNodeToRemirror(linkNode) {
675
669
  });
676
670
  }
677
671
  function convertImageNodeToRemirror(imageNode) {
678
- var _fileSource$metadata, _fileSource$metadata2, _fileSource$metadata3;
679
- var fileSource = imageNode.src;
680
- if (!(core.VAL_EXTENSION in fileSource) || fileSource[core.VAL_EXTENSION] !== "file" && fileSource[core.VAL_EXTENSION] !== "remote") {
681
- throw Error("Expected file source in image node");
682
- }
683
- var fileVal;
684
- if (fileSource[core.VAL_EXTENSION] === "remote") {
685
- fileVal = core.Internal.convertRemoteSource(fileSource);
686
- } else {
687
- fileVal = core.Internal.convertFileSource(fileSource);
672
+ var imageSource = imageNode.src;
673
+ if (typeof (imageSource === null || imageSource === void 0 ? void 0 : imageSource.path) !== "string") {
674
+ throw Error("Expected image source in image node");
688
675
  }
689
676
  return {
690
677
  type: "image",
691
678
  attrs: {
692
- height: (_fileSource$metadata = fileSource.metadata) === null || _fileSource$metadata === void 0 ? void 0 : _fileSource$metadata.height,
693
- width: (_fileSource$metadata2 = fileSource.metadata) === null || _fileSource$metadata2 === void 0 ? void 0 : _fileSource$metadata2.width,
694
- alt: (_fileSource$metadata3 = fileSource.metadata) === null || _fileSource$metadata3 === void 0 ? void 0 : _fileSource$metadata3.alt,
695
- src: fileVal.url
679
+ height: imageSource.height,
680
+ width: imageSource.width,
681
+ alt: imageSource.alt,
682
+ src: core.Internal.mediaUrl(imageSource)
696
683
  }
697
684
  };
698
685
  }
@@ -3297,6 +3284,84 @@ function resolveSchemaSourceFixes(validationErrors, snapshot) {
3297
3284
  return result;
3298
3285
  }
3299
3286
 
3287
+ /**
3288
+ * The protocol between the studio's canvas and the page inside it.
3289
+ *
3290
+ * The canvas shows the running site in a frame. That frame is a different
3291
+ * document — the customer's app, rendered by their components — so the studio
3292
+ * cannot reach into it, and it cannot reach out. Everything they say to each
3293
+ * other goes through `postMessage`, and this is the vocabulary.
3294
+ *
3295
+ * It lives in `shared` because both halves have to agree on it: the page half
3296
+ * ships in `@valbuild/next`, the studio half in `@valbuild/ui`, and a message
3297
+ * shape defined twice is a message shape that will differ once.
3298
+ *
3299
+ * ## Why a query parameter turns it on
3300
+ *
3301
+ * The frame has to render the page as Val sees it — draft content, with
3302
+ * `data-val-path` on every element Val tracks — while *not* showing Val's own
3303
+ * overlay, because the studio is already the UI. Nothing about the cookies can
3304
+ * express that: they are per-origin, and the studio and the page share an
3305
+ * origin, so a cookie would change the customer's own tabs too. A parameter on
3306
+ * the frame's URL is scoped to exactly the one document that needs it.
3307
+ */
3308
+
3309
+ /** Marks a page load as the studio's canvas frame. */
3310
+ var VAL_CANVAS_PARAM = "val_canvas";
3311
+
3312
+ /**
3313
+ * Every message is tagged with this, and the tag is checked on receipt.
3314
+ *
3315
+ * A window receives messages from anything that has a handle on it —
3316
+ * extensions, embeds, other frames — and `event.data` is whatever they chose to
3317
+ * send. Checking a discriminator before reading anything else is what keeps a
3318
+ * stray message from being read as a canvas instruction.
3319
+ */
3320
+ var VAL_CANVAS_MESSAGE = "val-canvas";
3321
+
3322
+ /** One element on the page that Val has a path for. */
3323
+
3324
+ /** What the page tells the studio. */
3325
+
3326
+ /** What the studio tells the page. */
3327
+
3328
+ /** Narrow an unknown `event.data` to a message from the page. */
3329
+ function isValCanvasPageMessage(data) {
3330
+ if (_typeof(data) !== "object" || data === null) return false;
3331
+ var message = data;
3332
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3333
+ return message.type === "ready" || message.type === "elements" || message.type === "clicked" || message.type === "refreshing";
3334
+ }
3335
+
3336
+ /** Narrow an unknown `event.data` to a message from the studio. */
3337
+ function isValCanvasStudioMessage(data) {
3338
+ if (_typeof(data) !== "object" || data === null) return false;
3339
+ var message = data;
3340
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3341
+ return message.type === "rescan" || message.type === "highlight" || message.type === "setPicking" || message.type === "sourceUpdate" || message.type === "sourcesSynced";
3342
+ }
3343
+
3344
+ /**
3345
+ * Whether this document was loaded as the studio's canvas frame.
3346
+ *
3347
+ * Reads the location directly rather than taking it as an argument, because
3348
+ * every caller wants the answer for the document it is running in, and passing
3349
+ * it around is how one of them ends up asking about the wrong one.
3350
+ */
3351
+ function isValCanvasFrame(search) {
3352
+ try {
3353
+ return new URLSearchParams(search).get(VAL_CANVAS_PARAM) !== null;
3354
+ } catch (_unused) {
3355
+ return false;
3356
+ }
3357
+ }
3358
+
3359
+ /** Add the canvas marker to a page URL. */
3360
+ function withValCanvasParam(url) {
3361
+ var separator = url.includes("?") ? "&" : "?";
3362
+ return "".concat(url).concat(separator).concat(VAL_CANVAS_PARAM, "=1");
3363
+ }
3364
+
3300
3365
  exports.Api = Api;
3301
3366
  exports.ColorOptions = ColorOptions;
3302
3367
  exports.DEFAULT_ERROR_MESSAGE = DEFAULT_ERROR_MESSAGE;
@@ -3340,6 +3405,8 @@ exports.SerializedStringSchema = SerializedStringSchema;
3340
3405
  exports.SerializedUnionSchema = SerializedUnionSchema;
3341
3406
  exports.SharedValConfig = SharedValConfig;
3342
3407
  exports.VAL_AI_SESSION_STORAGE_KEY = VAL_AI_SESSION_STORAGE_KEY;
3408
+ exports.VAL_CANVAS_MESSAGE = VAL_CANVAS_MESSAGE;
3409
+ exports.VAL_CANVAS_PARAM = VAL_CANVAS_PARAM;
3343
3410
  exports.VAL_CONFIG_SESSION_STORAGE_KEY = VAL_CONFIG_SESSION_STORAGE_KEY;
3344
3411
  exports.VAL_ENABLE_COOKIE_NAME = VAL_ENABLE_COOKIE_NAME;
3345
3412
  exports.VAL_SESSION_COOKIE = VAL_SESSION_COOKIE;
@@ -3357,6 +3424,9 @@ exports.getNextAppRouterSourceFolder = getNextAppRouterSourceFolder;
3357
3424
  exports.getPatternFromModuleFilePath = getPatternFromModuleFilePath;
3358
3425
  exports.getRoutesOf = getRoutesOf;
3359
3426
  exports.getRoutesWithModulePaths = getRoutesWithModulePaths;
3427
+ exports.isValCanvasFrame = isValCanvasFrame;
3428
+ exports.isValCanvasPageMessage = isValCanvasPageMessage;
3429
+ exports.isValCanvasStudioMessage = isValCanvasStudioMessage;
3360
3430
  exports.levenshtein = levenshtein;
3361
3431
  exports.parseRoutePattern = parseRoutePattern;
3362
3432
  exports.remirrorToRichTextSource = remirrorToRichTextSource;
@@ -3365,3 +3435,4 @@ exports.resolveSchemaSourceFixes = resolveSchemaSourceFixes;
3365
3435
  exports.richTextToRemirror = richTextToRemirror;
3366
3436
  exports.urlOf = urlOf;
3367
3437
  exports.validateRoutePatterns = validateRoutePatterns;
3438
+ exports.withValCanvasParam = withValCanvasParam;
@@ -1,4 +1,4 @@
1
- import { Internal, FILE_REF_PROP, VAL_EXTENSION, FILE_REF_SUBTYPE_TAG } from '@valbuild/core';
1
+ import { Internal } from '@valbuild/core';
2
2
  import { b as base64DataUrlToUint8Array } from '../../dist/utils-00f3e1df.esm.js';
3
3
  import { z } from 'zod';
4
4
  import { fromError } from 'zod-validation-error';
@@ -347,22 +347,21 @@ function convertImageNode(path, node, files, configDirectory, remoteOptions) {
347
347
  }
348
348
  return {
349
349
  tag: "img",
350
- src: _objectSpread2(_objectSpread2(_defineProperty({}, FILE_REF_PROP, ref), remoteOptions ? _defineProperty({}, VAL_EXTENSION, "remote") : _defineProperty(_defineProperty({}, VAL_EXTENSION, "file"), FILE_REF_SUBTYPE_TAG, "image")), {}, {
351
- metadata: {
352
- width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
353
- height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
354
- mimeType: mimeType
355
- }
356
- })
350
+ src: {
351
+ path: ref,
352
+ width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
353
+ height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
354
+ mimeType: mimeType
355
+ }
357
356
  };
358
357
  } else if (node.attrs) {
359
358
  var url = node.attrs.src;
360
359
  var patchId = getParam("patch_id", url);
360
+ // Recover the path the URL was built from. Local and remote produce the
361
+ // same source shape now, so this is only about undoing the URL rule.
361
362
  var noParamsUrl = url.split("?")[0];
362
- var remote = false;
363
363
  if (patchId) {
364
- remote = getParam("remote", url) === "true";
365
- if (remote) {
364
+ if (getParam("remote", url) === "true") {
366
365
  var remoteRef = getParam("ref", url);
367
366
  if (remoteRef) {
368
367
  noParamsUrl = decodeURIComponent(remoteRef);
@@ -374,23 +373,18 @@ function convertImageNode(path, node, files, configDirectory, remoteOptions) {
374
373
  } else {
375
374
  console.error("Patched image URL does not start with /api/val/files: " + url);
376
375
  }
376
+ } else if (remoteOptions && noParamsUrl.startsWith(remoteOptions.remoteHost)) ; else if (!noParamsUrl.startsWith("/public")) {
377
+ noParamsUrl = "/public".concat(noParamsUrl);
377
378
  } else {
378
- if (remoteOptions && noParamsUrl.startsWith(remoteOptions.remoteHost)) {
379
- remote = true;
380
- } else if (!noParamsUrl.startsWith("/public")) {
381
- noParamsUrl = "/public".concat(noParamsUrl);
382
- } else {
383
- console.error("Unpatched image URL starts with /public: " + url);
384
- }
379
+ console.error("Unpatched image URL starts with /public: " + url);
385
380
  }
386
381
  var tag = {
387
382
  tag: "img",
388
- src: _objectSpread2(_objectSpread2({}, remote ? _defineProperty(_defineProperty({}, VAL_EXTENSION, "remote"), FILE_REF_PROP, noParamsUrl) : _defineProperty(_defineProperty(_defineProperty({}, VAL_EXTENSION, "file"), FILE_REF_SUBTYPE_TAG, "image"), FILE_REF_PROP, noParamsUrl)), {}, {
389
- metadata: {
390
- width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
391
- height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
392
- mimeType: noParamsUrl && Internal.filenameToMimeType(noParamsUrl) || ""
393
- }
383
+ src: _objectSpread2({
384
+ path: noParamsUrl,
385
+ width: typeof node.attrs.width === "number" ? node.attrs.width : 0,
386
+ height: typeof node.attrs.height === "number" ? node.attrs.height : 0,
387
+ mimeType: noParamsUrl && Internal.filenameToMimeType(noParamsUrl) || ""
394
388
  }, patchId ? {
395
389
  patch_id: patchId
396
390
  } : {})
@@ -671,24 +665,17 @@ function convertLinkNodeToRemirror(linkNode) {
671
665
  });
672
666
  }
673
667
  function convertImageNodeToRemirror(imageNode) {
674
- var _fileSource$metadata, _fileSource$metadata2, _fileSource$metadata3;
675
- var fileSource = imageNode.src;
676
- if (!(VAL_EXTENSION in fileSource) || fileSource[VAL_EXTENSION] !== "file" && fileSource[VAL_EXTENSION] !== "remote") {
677
- throw Error("Expected file source in image node");
678
- }
679
- var fileVal;
680
- if (fileSource[VAL_EXTENSION] === "remote") {
681
- fileVal = Internal.convertRemoteSource(fileSource);
682
- } else {
683
- fileVal = Internal.convertFileSource(fileSource);
668
+ var imageSource = imageNode.src;
669
+ if (typeof (imageSource === null || imageSource === void 0 ? void 0 : imageSource.path) !== "string") {
670
+ throw Error("Expected image source in image node");
684
671
  }
685
672
  return {
686
673
  type: "image",
687
674
  attrs: {
688
- height: (_fileSource$metadata = fileSource.metadata) === null || _fileSource$metadata === void 0 ? void 0 : _fileSource$metadata.height,
689
- width: (_fileSource$metadata2 = fileSource.metadata) === null || _fileSource$metadata2 === void 0 ? void 0 : _fileSource$metadata2.width,
690
- alt: (_fileSource$metadata3 = fileSource.metadata) === null || _fileSource$metadata3 === void 0 ? void 0 : _fileSource$metadata3.alt,
691
- src: fileVal.url
675
+ height: imageSource.height,
676
+ width: imageSource.width,
677
+ alt: imageSource.alt,
678
+ src: Internal.mediaUrl(imageSource)
692
679
  }
693
680
  };
694
681
  }
@@ -3293,4 +3280,82 @@ function resolveSchemaSourceFixes(validationErrors, snapshot) {
3293
3280
  return result;
3294
3281
  }
3295
3282
 
3296
- export { Api, ColorOptions, DEFAULT_ERROR_MESSAGE, DateOptions, FileOptions, ImageOptions, JSON_ENTRIES_BATCH_MAX, ParentRef$1 as ParentRef, Patch, PatchBlock, PatchId, PatchJSON, RemirrorBr, RemirrorBulletList, RemirrorHeading, RemirrorImage, RemirrorJSON, RemirrorLinkMark, RemirrorListItem, RemirrorOrderedList, RemirrorParagraph, RemirrorText, RemirrorTextMark, RichTextOptions, SerializedArraySchema, SerializedBooleanSchema, SerializedColorSchema, SerializedDateSchema, SerializedDateTimeSchema, SerializedFileSchema, SerializedImageSchema, SerializedKeyOfSchema, SerializedLiteralSchema, SerializedNumberSchema, SerializedObjectSchema, SerializedRecordSchema, SerializedRichTextSchema, SerializedRouteSchema, SerializedSchema, SerializedStringSchema, SerializedUnionSchema, SharedValConfig, VAL_AI_SESSION_STORAGE_KEY, VAL_CONFIG_SESSION_STORAGE_KEY, VAL_ENABLE_COOKIE_NAME, VAL_SESSION_COOKIE, VAL_STATE_COOKIE, VAL_THEME_SESSION_STORAGE_KEY, ValCommit, ValDeployment, createRegExpFromPattern, createValClient, filterRoutesByPatterns, findSimilar, getErrorMessageFromUnknownJson, getNextAppRouterSitemapTree, getNextAppRouterSourceFolder, getPatternFromModuleFilePath, getRoutesOf, getRoutesWithModulePaths, levenshtein, parseRoutePattern, remirrorToRichTextSource, resolveSchemaSourceFixForError, resolveSchemaSourceFixes, richTextToRemirror, urlOf, validateRoutePatterns };
3283
+ /**
3284
+ * The protocol between the studio's canvas and the page inside it.
3285
+ *
3286
+ * The canvas shows the running site in a frame. That frame is a different
3287
+ * document — the customer's app, rendered by their components — so the studio
3288
+ * cannot reach into it, and it cannot reach out. Everything they say to each
3289
+ * other goes through `postMessage`, and this is the vocabulary.
3290
+ *
3291
+ * It lives in `shared` because both halves have to agree on it: the page half
3292
+ * ships in `@valbuild/next`, the studio half in `@valbuild/ui`, and a message
3293
+ * shape defined twice is a message shape that will differ once.
3294
+ *
3295
+ * ## Why a query parameter turns it on
3296
+ *
3297
+ * The frame has to render the page as Val sees it — draft content, with
3298
+ * `data-val-path` on every element Val tracks — while *not* showing Val's own
3299
+ * overlay, because the studio is already the UI. Nothing about the cookies can
3300
+ * express that: they are per-origin, and the studio and the page share an
3301
+ * origin, so a cookie would change the customer's own tabs too. A parameter on
3302
+ * the frame's URL is scoped to exactly the one document that needs it.
3303
+ */
3304
+
3305
+ /** Marks a page load as the studio's canvas frame. */
3306
+ var VAL_CANVAS_PARAM = "val_canvas";
3307
+
3308
+ /**
3309
+ * Every message is tagged with this, and the tag is checked on receipt.
3310
+ *
3311
+ * A window receives messages from anything that has a handle on it —
3312
+ * extensions, embeds, other frames — and `event.data` is whatever they chose to
3313
+ * send. Checking a discriminator before reading anything else is what keeps a
3314
+ * stray message from being read as a canvas instruction.
3315
+ */
3316
+ var VAL_CANVAS_MESSAGE = "val-canvas";
3317
+
3318
+ /** One element on the page that Val has a path for. */
3319
+
3320
+ /** What the page tells the studio. */
3321
+
3322
+ /** What the studio tells the page. */
3323
+
3324
+ /** Narrow an unknown `event.data` to a message from the page. */
3325
+ function isValCanvasPageMessage(data) {
3326
+ if (_typeof(data) !== "object" || data === null) return false;
3327
+ var message = data;
3328
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3329
+ return message.type === "ready" || message.type === "elements" || message.type === "clicked" || message.type === "refreshing";
3330
+ }
3331
+
3332
+ /** Narrow an unknown `event.data` to a message from the studio. */
3333
+ function isValCanvasStudioMessage(data) {
3334
+ if (_typeof(data) !== "object" || data === null) return false;
3335
+ var message = data;
3336
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3337
+ return message.type === "rescan" || message.type === "highlight" || message.type === "setPicking" || message.type === "sourceUpdate" || message.type === "sourcesSynced";
3338
+ }
3339
+
3340
+ /**
3341
+ * Whether this document was loaded as the studio's canvas frame.
3342
+ *
3343
+ * Reads the location directly rather than taking it as an argument, because
3344
+ * every caller wants the answer for the document it is running in, and passing
3345
+ * it around is how one of them ends up asking about the wrong one.
3346
+ */
3347
+ function isValCanvasFrame(search) {
3348
+ try {
3349
+ return new URLSearchParams(search).get(VAL_CANVAS_PARAM) !== null;
3350
+ } catch (_unused) {
3351
+ return false;
3352
+ }
3353
+ }
3354
+
3355
+ /** Add the canvas marker to a page URL. */
3356
+ function withValCanvasParam(url) {
3357
+ var separator = url.includes("?") ? "&" : "?";
3358
+ return "".concat(url).concat(separator).concat(VAL_CANVAS_PARAM, "=1");
3359
+ }
3360
+
3361
+ export { Api, ColorOptions, DEFAULT_ERROR_MESSAGE, DateOptions, FileOptions, ImageOptions, JSON_ENTRIES_BATCH_MAX, ParentRef$1 as ParentRef, Patch, PatchBlock, PatchId, PatchJSON, RemirrorBr, RemirrorBulletList, RemirrorHeading, RemirrorImage, RemirrorJSON, RemirrorLinkMark, RemirrorListItem, RemirrorOrderedList, RemirrorParagraph, RemirrorText, RemirrorTextMark, RichTextOptions, SerializedArraySchema, SerializedBooleanSchema, SerializedColorSchema, SerializedDateSchema, SerializedDateTimeSchema, SerializedFileSchema, SerializedImageSchema, SerializedKeyOfSchema, SerializedLiteralSchema, SerializedNumberSchema, SerializedObjectSchema, SerializedRecordSchema, SerializedRichTextSchema, SerializedRouteSchema, SerializedSchema, SerializedStringSchema, SerializedUnionSchema, SharedValConfig, VAL_AI_SESSION_STORAGE_KEY, VAL_CANVAS_MESSAGE, VAL_CANVAS_PARAM, VAL_CONFIG_SESSION_STORAGE_KEY, VAL_ENABLE_COOKIE_NAME, VAL_SESSION_COOKIE, VAL_STATE_COOKIE, VAL_THEME_SESSION_STORAGE_KEY, ValCommit, ValDeployment, createRegExpFromPattern, createValClient, filterRoutesByPatterns, findSimilar, getErrorMessageFromUnknownJson, getNextAppRouterSitemapTree, getNextAppRouterSourceFolder, getPatternFromModuleFilePath, getRoutesOf, getRoutesWithModulePaths, isValCanvasFrame, isValCanvasPageMessage, isValCanvasStudioMessage, levenshtein, parseRoutePattern, remirrorToRichTextSource, resolveSchemaSourceFixForError, resolveSchemaSourceFixes, richTextToRemirror, urlOf, validateRoutePatterns, withValCanvasParam };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valbuild/shared",
3
- "version": "0.102.0",
3
+ "version": "0.103.0",
4
4
  "private": false,
5
5
  "description": "Val shared types and utilities",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "dependencies": {
32
32
  "zod": "^4.3.5",
33
33
  "zod-validation-error": "^5.0.0",
34
- "@valbuild/core": "0.102.0"
34
+ "@valbuild/core": "0.103.0"
35
35
  },
36
36
  "files": [
37
37
  "dist",