@valbuild/shared 0.102.0 → 0.105.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.
@@ -430,6 +430,10 @@ export declare const Api: {
430
430
  schemaSha: z.ZodString;
431
431
  sourcesSha: z.ZodString;
432
432
  patches: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
433
+ removed: z.ZodOptional<z.ZodArray<z.ZodObject<{
434
+ patchId: z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>;
435
+ reason: z.ZodString;
436
+ }, z.core.$strip>>>;
433
437
  config: z.ZodObject<{
434
438
  project: z.ZodOptional<z.ZodString>;
435
439
  root: z.ZodOptional<z.ZodString>;
@@ -813,7 +817,7 @@ export declare const Api: {
813
817
  schemaSha: z.ZodString;
814
818
  sourcesSha: z.ZodString;
815
819
  modules: z.ZodRecord<z.ZodString & z.ZodType<ModuleFilePath, string, z.core.$ZodTypeInternals<ModuleFilePath, string>>, z.ZodObject<{
816
- render: z.ZodOptional<z.ZodAny>;
820
+ preview: z.ZodOptional<z.ZodAny>;
817
821
  source: z.ZodOptional<z.ZodAny>;
818
822
  baseSource: z.ZodOptional<z.ZodAny>;
819
823
  patches: z.ZodOptional<z.ZodObject<{
@@ -914,7 +918,13 @@ export declare const Api: {
914
918
  }, z.core.$strip>;
915
919
  }, z.core.$strip>, z.ZodObject<{
916
920
  status: z.ZodLiteral<200>;
917
- json: z.ZodObject<{}, z.core.$strip>;
921
+ json: z.ZodObject<{
922
+ removed: z.ZodOptional<z.ZodArray<z.ZodObject<{
923
+ patchId: z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>;
924
+ moduleFilePath: z.ZodString & z.ZodType<ModuleFilePath, string, z.core.$ZodTypeInternals<ModuleFilePath, string>>;
925
+ message: z.ZodString;
926
+ }, z.core.$strip>>>;
927
+ }, z.core.$strip>;
918
928
  }, z.core.$strip>, z.ZodObject<{
919
929
  status: z.ZodLiteral<409>;
920
930
  json: z.ZodObject<{
@@ -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
  }
@@ -1014,6 +1001,14 @@ var SourcePath = zod.z.string().transform(function (path) {
1014
1001
 
1015
1002
  var SerializedStringSchema = zod.z.object({
1016
1003
  type: zod.z.literal("string"),
1004
+ // A render is static config, so unlike a `preview` it travels WHOLE — this
1005
+ // is the field the editor reads the layout from. See `core/src/render.ts`.
1006
+ render: zod.z.union([zod.z.object({
1007
+ as: zod.z.literal("textarea")
1008
+ }), zod.z.object({
1009
+ as: zod.z.literal("code"),
1010
+ language: zod.z["enum"](core.CODE_LANGUAGES)
1011
+ })]).optional(),
1017
1012
  options: zod.z.object({
1018
1013
  maxLength: zod.z.number().optional(),
1019
1014
  minLength: zod.z.number().optional(),
@@ -1064,6 +1059,8 @@ var SerializedArraySchema = zod.z.lazy(function () {
1064
1059
  type: zod.z.literal("array"),
1065
1060
  item: SerializedSchema,
1066
1061
  opt: zod.z["boolean"](),
1062
+ // Only whether a preview EXISTS: the closure itself cannot be serialized.
1063
+ preview: zod.z.literal(true).optional(),
1067
1064
  readonly: zod.z["boolean"]().optional(),
1068
1065
  hidden: zod.z["boolean"]().optional()
1069
1066
  });
@@ -1133,6 +1130,8 @@ var SerializedRecordSchema = zod.z.lazy(function () {
1133
1130
  type: zod.z.literal("record"),
1134
1131
  item: SerializedSchema,
1135
1132
  opt: zod.z["boolean"](),
1133
+ // Only whether a preview EXISTS: the closure itself cannot be serialized.
1134
+ preview: zod.z.literal(true).optional(),
1136
1135
  // Optional gallery marker for files/images
1137
1136
  mediaType: zod.z.union([zod.z.literal("files"), zod.z.literal("images")]).optional(),
1138
1137
  // Optional legacy gallery metadata
@@ -1661,6 +1660,21 @@ var Api = {
1661
1660
  schemaSha: zod.z.string(),
1662
1661
  sourcesSha: zod.z.string(),
1663
1662
  patches: zod.z.array(PatchId),
1663
+ /**
1664
+ * Unpublished changes the store threw away because it could not
1665
+ * read them.
1666
+ *
1667
+ * On stat rather than on `GET /patches` because stat is the
1668
+ * channel that always flows: the case worth reporting is a repair
1669
+ * that removed EVERYTHING, and then there is nothing left for the
1670
+ * studio to fetch, so a notice riding on the fetch is never
1671
+ * collected. Said once - the server drains it when it hands it
1672
+ * over.
1673
+ */
1674
+ removed: zod.z.array(zod.z.object({
1675
+ patchId: PatchId,
1676
+ reason: zod.z.string()
1677
+ })).optional(),
1664
1678
  config: ValConfig,
1665
1679
  profileId: zod.z.string().nullable(),
1666
1680
  mode: zod.z.union([zod.z.literal("http"), zod.z.literal("fs")]),
@@ -1907,7 +1921,7 @@ var Api = {
1907
1921
  schemaSha: zod.z.string(),
1908
1922
  sourcesSha: zod.z.string(),
1909
1923
  modules: zod.z.record(ModuleFilePath, zod.z.object({
1910
- render: zod.z.any().optional(),
1924
+ preview: zod.z.any().optional(),
1911
1925
  // TODO: improve this type
1912
1926
  source: zod.z.any().optional(),
1913
1927
  //.optional(), // TODO: Json zod type
@@ -1987,7 +2001,23 @@ var Api = {
1987
2001
  },
1988
2002
  res: zod.z.union([unauthorizedResponse, zod.z.object({
1989
2003
  status: zod.z.literal(200),
1990
- json: zod.z.object({}) // TODO:
2004
+ json: zod.z.object({
2005
+ /**
2006
+ * Unpublished changes the save threw away because they could not be
2007
+ * applied.
2008
+ *
2009
+ * `fs` mode only, and the reason the save is a 200 rather than the
2010
+ * 400 it used to be: with auto-save on, refusing the whole commit
2011
+ * for one bad patch means nothing is ever written again. The rest is
2012
+ * written, these are removed, and the studio drops them locally and
2013
+ * says so.
2014
+ */
2015
+ removed: zod.z.array(zod.z.object({
2016
+ patchId: PatchId,
2017
+ moduleFilePath: ModuleFilePath,
2018
+ message: zod.z.string()
2019
+ })).optional()
2020
+ })
1991
2021
  }), zod.z.object({
1992
2022
  status: zod.z.literal(409),
1993
2023
  json: zod.z.object({
@@ -3297,6 +3327,84 @@ function resolveSchemaSourceFixes(validationErrors, snapshot) {
3297
3327
  return result;
3298
3328
  }
3299
3329
 
3330
+ /**
3331
+ * The protocol between the studio's canvas and the page inside it.
3332
+ *
3333
+ * The canvas shows the running site in a frame. That frame is a different
3334
+ * document — the customer's app, rendered by their components — so the studio
3335
+ * cannot reach into it, and it cannot reach out. Everything they say to each
3336
+ * other goes through `postMessage`, and this is the vocabulary.
3337
+ *
3338
+ * It lives in `shared` because both halves have to agree on it: the page half
3339
+ * ships in `@valbuild/next`, the studio half in `@valbuild/ui`, and a message
3340
+ * shape defined twice is a message shape that will differ once.
3341
+ *
3342
+ * ## Why a query parameter turns it on
3343
+ *
3344
+ * The frame has to render the page as Val sees it — draft content, with
3345
+ * `data-val-path` on every element Val tracks — while *not* showing Val's own
3346
+ * overlay, because the studio is already the UI. Nothing about the cookies can
3347
+ * express that: they are per-origin, and the studio and the page share an
3348
+ * origin, so a cookie would change the customer's own tabs too. A parameter on
3349
+ * the frame's URL is scoped to exactly the one document that needs it.
3350
+ */
3351
+
3352
+ /** Marks a page load as the studio's canvas frame. */
3353
+ var VAL_CANVAS_PARAM = "val_canvas";
3354
+
3355
+ /**
3356
+ * Every message is tagged with this, and the tag is checked on receipt.
3357
+ *
3358
+ * A window receives messages from anything that has a handle on it —
3359
+ * extensions, embeds, other frames — and `event.data` is whatever they chose to
3360
+ * send. Checking a discriminator before reading anything else is what keeps a
3361
+ * stray message from being read as a canvas instruction.
3362
+ */
3363
+ var VAL_CANVAS_MESSAGE = "val-canvas";
3364
+
3365
+ /** One element on the page that Val has a path for. */
3366
+
3367
+ /** What the page tells the studio. */
3368
+
3369
+ /** What the studio tells the page. */
3370
+
3371
+ /** Narrow an unknown `event.data` to a message from the page. */
3372
+ function isValCanvasPageMessage(data) {
3373
+ if (_typeof(data) !== "object" || data === null) return false;
3374
+ var message = data;
3375
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3376
+ return message.type === "ready" || message.type === "elements" || message.type === "clicked" || message.type === "refreshing";
3377
+ }
3378
+
3379
+ /** Narrow an unknown `event.data` to a message from the studio. */
3380
+ function isValCanvasStudioMessage(data) {
3381
+ if (_typeof(data) !== "object" || data === null) return false;
3382
+ var message = data;
3383
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3384
+ return message.type === "rescan" || message.type === "highlight" || message.type === "setPicking" || message.type === "sourceUpdate" || message.type === "sourcesSynced";
3385
+ }
3386
+
3387
+ /**
3388
+ * Whether this document was loaded as the studio's canvas frame.
3389
+ *
3390
+ * Reads the location directly rather than taking it as an argument, because
3391
+ * every caller wants the answer for the document it is running in, and passing
3392
+ * it around is how one of them ends up asking about the wrong one.
3393
+ */
3394
+ function isValCanvasFrame(search) {
3395
+ try {
3396
+ return new URLSearchParams(search).get(VAL_CANVAS_PARAM) !== null;
3397
+ } catch (_unused) {
3398
+ return false;
3399
+ }
3400
+ }
3401
+
3402
+ /** Add the canvas marker to a page URL. */
3403
+ function withValCanvasParam(url) {
3404
+ var separator = url.includes("?") ? "&" : "?";
3405
+ return "".concat(url).concat(separator).concat(VAL_CANVAS_PARAM, "=1");
3406
+ }
3407
+
3300
3408
  exports.Api = Api;
3301
3409
  exports.ColorOptions = ColorOptions;
3302
3410
  exports.DEFAULT_ERROR_MESSAGE = DEFAULT_ERROR_MESSAGE;
@@ -3340,6 +3448,8 @@ exports.SerializedStringSchema = SerializedStringSchema;
3340
3448
  exports.SerializedUnionSchema = SerializedUnionSchema;
3341
3449
  exports.SharedValConfig = SharedValConfig;
3342
3450
  exports.VAL_AI_SESSION_STORAGE_KEY = VAL_AI_SESSION_STORAGE_KEY;
3451
+ exports.VAL_CANVAS_MESSAGE = VAL_CANVAS_MESSAGE;
3452
+ exports.VAL_CANVAS_PARAM = VAL_CANVAS_PARAM;
3343
3453
  exports.VAL_CONFIG_SESSION_STORAGE_KEY = VAL_CONFIG_SESSION_STORAGE_KEY;
3344
3454
  exports.VAL_ENABLE_COOKIE_NAME = VAL_ENABLE_COOKIE_NAME;
3345
3455
  exports.VAL_SESSION_COOKIE = VAL_SESSION_COOKIE;
@@ -3357,6 +3467,9 @@ exports.getNextAppRouterSourceFolder = getNextAppRouterSourceFolder;
3357
3467
  exports.getPatternFromModuleFilePath = getPatternFromModuleFilePath;
3358
3468
  exports.getRoutesOf = getRoutesOf;
3359
3469
  exports.getRoutesWithModulePaths = getRoutesWithModulePaths;
3470
+ exports.isValCanvasFrame = isValCanvasFrame;
3471
+ exports.isValCanvasPageMessage = isValCanvasPageMessage;
3472
+ exports.isValCanvasStudioMessage = isValCanvasStudioMessage;
3360
3473
  exports.levenshtein = levenshtein;
3361
3474
  exports.parseRoutePattern = parseRoutePattern;
3362
3475
  exports.remirrorToRichTextSource = remirrorToRichTextSource;
@@ -3365,3 +3478,4 @@ exports.resolveSchemaSourceFixes = resolveSchemaSourceFixes;
3365
3478
  exports.richTextToRemirror = richTextToRemirror;
3366
3479
  exports.urlOf = urlOf;
3367
3480
  exports.validateRoutePatterns = validateRoutePatterns;
3481
+ 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
  }
@@ -1014,6 +1001,14 @@ var SourcePath = zod.z.string().transform(function (path) {
1014
1001
 
1015
1002
  var SerializedStringSchema = zod.z.object({
1016
1003
  type: zod.z.literal("string"),
1004
+ // A render is static config, so unlike a `preview` it travels WHOLE — this
1005
+ // is the field the editor reads the layout from. See `core/src/render.ts`.
1006
+ render: zod.z.union([zod.z.object({
1007
+ as: zod.z.literal("textarea")
1008
+ }), zod.z.object({
1009
+ as: zod.z.literal("code"),
1010
+ language: zod.z["enum"](core.CODE_LANGUAGES)
1011
+ })]).optional(),
1017
1012
  options: zod.z.object({
1018
1013
  maxLength: zod.z.number().optional(),
1019
1014
  minLength: zod.z.number().optional(),
@@ -1064,6 +1059,8 @@ var SerializedArraySchema = zod.z.lazy(function () {
1064
1059
  type: zod.z.literal("array"),
1065
1060
  item: SerializedSchema,
1066
1061
  opt: zod.z["boolean"](),
1062
+ // Only whether a preview EXISTS: the closure itself cannot be serialized.
1063
+ preview: zod.z.literal(true).optional(),
1067
1064
  readonly: zod.z["boolean"]().optional(),
1068
1065
  hidden: zod.z["boolean"]().optional()
1069
1066
  });
@@ -1133,6 +1130,8 @@ var SerializedRecordSchema = zod.z.lazy(function () {
1133
1130
  type: zod.z.literal("record"),
1134
1131
  item: SerializedSchema,
1135
1132
  opt: zod.z["boolean"](),
1133
+ // Only whether a preview EXISTS: the closure itself cannot be serialized.
1134
+ preview: zod.z.literal(true).optional(),
1136
1135
  // Optional gallery marker for files/images
1137
1136
  mediaType: zod.z.union([zod.z.literal("files"), zod.z.literal("images")]).optional(),
1138
1137
  // Optional legacy gallery metadata
@@ -1661,6 +1660,21 @@ var Api = {
1661
1660
  schemaSha: zod.z.string(),
1662
1661
  sourcesSha: zod.z.string(),
1663
1662
  patches: zod.z.array(PatchId),
1663
+ /**
1664
+ * Unpublished changes the store threw away because it could not
1665
+ * read them.
1666
+ *
1667
+ * On stat rather than on `GET /patches` because stat is the
1668
+ * channel that always flows: the case worth reporting is a repair
1669
+ * that removed EVERYTHING, and then there is nothing left for the
1670
+ * studio to fetch, so a notice riding on the fetch is never
1671
+ * collected. Said once - the server drains it when it hands it
1672
+ * over.
1673
+ */
1674
+ removed: zod.z.array(zod.z.object({
1675
+ patchId: PatchId,
1676
+ reason: zod.z.string()
1677
+ })).optional(),
1664
1678
  config: ValConfig,
1665
1679
  profileId: zod.z.string().nullable(),
1666
1680
  mode: zod.z.union([zod.z.literal("http"), zod.z.literal("fs")]),
@@ -1907,7 +1921,7 @@ var Api = {
1907
1921
  schemaSha: zod.z.string(),
1908
1922
  sourcesSha: zod.z.string(),
1909
1923
  modules: zod.z.record(ModuleFilePath, zod.z.object({
1910
- render: zod.z.any().optional(),
1924
+ preview: zod.z.any().optional(),
1911
1925
  // TODO: improve this type
1912
1926
  source: zod.z.any().optional(),
1913
1927
  //.optional(), // TODO: Json zod type
@@ -1987,7 +2001,23 @@ var Api = {
1987
2001
  },
1988
2002
  res: zod.z.union([unauthorizedResponse, zod.z.object({
1989
2003
  status: zod.z.literal(200),
1990
- json: zod.z.object({}) // TODO:
2004
+ json: zod.z.object({
2005
+ /**
2006
+ * Unpublished changes the save threw away because they could not be
2007
+ * applied.
2008
+ *
2009
+ * `fs` mode only, and the reason the save is a 200 rather than the
2010
+ * 400 it used to be: with auto-save on, refusing the whole commit
2011
+ * for one bad patch means nothing is ever written again. The rest is
2012
+ * written, these are removed, and the studio drops them locally and
2013
+ * says so.
2014
+ */
2015
+ removed: zod.z.array(zod.z.object({
2016
+ patchId: PatchId,
2017
+ moduleFilePath: ModuleFilePath,
2018
+ message: zod.z.string()
2019
+ })).optional()
2020
+ })
1991
2021
  }), zod.z.object({
1992
2022
  status: zod.z.literal(409),
1993
2023
  json: zod.z.object({
@@ -3297,6 +3327,84 @@ function resolveSchemaSourceFixes(validationErrors, snapshot) {
3297
3327
  return result;
3298
3328
  }
3299
3329
 
3330
+ /**
3331
+ * The protocol between the studio's canvas and the page inside it.
3332
+ *
3333
+ * The canvas shows the running site in a frame. That frame is a different
3334
+ * document — the customer's app, rendered by their components — so the studio
3335
+ * cannot reach into it, and it cannot reach out. Everything they say to each
3336
+ * other goes through `postMessage`, and this is the vocabulary.
3337
+ *
3338
+ * It lives in `shared` because both halves have to agree on it: the page half
3339
+ * ships in `@valbuild/next`, the studio half in `@valbuild/ui`, and a message
3340
+ * shape defined twice is a message shape that will differ once.
3341
+ *
3342
+ * ## Why a query parameter turns it on
3343
+ *
3344
+ * The frame has to render the page as Val sees it — draft content, with
3345
+ * `data-val-path` on every element Val tracks — while *not* showing Val's own
3346
+ * overlay, because the studio is already the UI. Nothing about the cookies can
3347
+ * express that: they are per-origin, and the studio and the page share an
3348
+ * origin, so a cookie would change the customer's own tabs too. A parameter on
3349
+ * the frame's URL is scoped to exactly the one document that needs it.
3350
+ */
3351
+
3352
+ /** Marks a page load as the studio's canvas frame. */
3353
+ var VAL_CANVAS_PARAM = "val_canvas";
3354
+
3355
+ /**
3356
+ * Every message is tagged with this, and the tag is checked on receipt.
3357
+ *
3358
+ * A window receives messages from anything that has a handle on it —
3359
+ * extensions, embeds, other frames — and `event.data` is whatever they chose to
3360
+ * send. Checking a discriminator before reading anything else is what keeps a
3361
+ * stray message from being read as a canvas instruction.
3362
+ */
3363
+ var VAL_CANVAS_MESSAGE = "val-canvas";
3364
+
3365
+ /** One element on the page that Val has a path for. */
3366
+
3367
+ /** What the page tells the studio. */
3368
+
3369
+ /** What the studio tells the page. */
3370
+
3371
+ /** Narrow an unknown `event.data` to a message from the page. */
3372
+ function isValCanvasPageMessage(data) {
3373
+ if (_typeof(data) !== "object" || data === null) return false;
3374
+ var message = data;
3375
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3376
+ return message.type === "ready" || message.type === "elements" || message.type === "clicked" || message.type === "refreshing";
3377
+ }
3378
+
3379
+ /** Narrow an unknown `event.data` to a message from the studio. */
3380
+ function isValCanvasStudioMessage(data) {
3381
+ if (_typeof(data) !== "object" || data === null) return false;
3382
+ var message = data;
3383
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3384
+ return message.type === "rescan" || message.type === "highlight" || message.type === "setPicking" || message.type === "sourceUpdate" || message.type === "sourcesSynced";
3385
+ }
3386
+
3387
+ /**
3388
+ * Whether this document was loaded as the studio's canvas frame.
3389
+ *
3390
+ * Reads the location directly rather than taking it as an argument, because
3391
+ * every caller wants the answer for the document it is running in, and passing
3392
+ * it around is how one of them ends up asking about the wrong one.
3393
+ */
3394
+ function isValCanvasFrame(search) {
3395
+ try {
3396
+ return new URLSearchParams(search).get(VAL_CANVAS_PARAM) !== null;
3397
+ } catch (_unused) {
3398
+ return false;
3399
+ }
3400
+ }
3401
+
3402
+ /** Add the canvas marker to a page URL. */
3403
+ function withValCanvasParam(url) {
3404
+ var separator = url.includes("?") ? "&" : "?";
3405
+ return "".concat(url).concat(separator).concat(VAL_CANVAS_PARAM, "=1");
3406
+ }
3407
+
3300
3408
  exports.Api = Api;
3301
3409
  exports.ColorOptions = ColorOptions;
3302
3410
  exports.DEFAULT_ERROR_MESSAGE = DEFAULT_ERROR_MESSAGE;
@@ -3340,6 +3448,8 @@ exports.SerializedStringSchema = SerializedStringSchema;
3340
3448
  exports.SerializedUnionSchema = SerializedUnionSchema;
3341
3449
  exports.SharedValConfig = SharedValConfig;
3342
3450
  exports.VAL_AI_SESSION_STORAGE_KEY = VAL_AI_SESSION_STORAGE_KEY;
3451
+ exports.VAL_CANVAS_MESSAGE = VAL_CANVAS_MESSAGE;
3452
+ exports.VAL_CANVAS_PARAM = VAL_CANVAS_PARAM;
3343
3453
  exports.VAL_CONFIG_SESSION_STORAGE_KEY = VAL_CONFIG_SESSION_STORAGE_KEY;
3344
3454
  exports.VAL_ENABLE_COOKIE_NAME = VAL_ENABLE_COOKIE_NAME;
3345
3455
  exports.VAL_SESSION_COOKIE = VAL_SESSION_COOKIE;
@@ -3357,6 +3467,9 @@ exports.getNextAppRouterSourceFolder = getNextAppRouterSourceFolder;
3357
3467
  exports.getPatternFromModuleFilePath = getPatternFromModuleFilePath;
3358
3468
  exports.getRoutesOf = getRoutesOf;
3359
3469
  exports.getRoutesWithModulePaths = getRoutesWithModulePaths;
3470
+ exports.isValCanvasFrame = isValCanvasFrame;
3471
+ exports.isValCanvasPageMessage = isValCanvasPageMessage;
3472
+ exports.isValCanvasStudioMessage = isValCanvasStudioMessage;
3360
3473
  exports.levenshtein = levenshtein;
3361
3474
  exports.parseRoutePattern = parseRoutePattern;
3362
3475
  exports.remirrorToRichTextSource = remirrorToRichTextSource;
@@ -3365,3 +3478,4 @@ exports.resolveSchemaSourceFixes = resolveSchemaSourceFixes;
3365
3478
  exports.richTextToRemirror = richTextToRemirror;
3366
3479
  exports.urlOf = urlOf;
3367
3480
  exports.validateRoutePatterns = validateRoutePatterns;
3481
+ 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, CODE_LANGUAGES } 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
  }
@@ -1010,6 +997,14 @@ var SourcePath = z.string().transform(function (path) {
1010
997
 
1011
998
  var SerializedStringSchema = z.object({
1012
999
  type: z.literal("string"),
1000
+ // A render is static config, so unlike a `preview` it travels WHOLE — this
1001
+ // is the field the editor reads the layout from. See `core/src/render.ts`.
1002
+ render: z.union([z.object({
1003
+ as: z.literal("textarea")
1004
+ }), z.object({
1005
+ as: z.literal("code"),
1006
+ language: z["enum"](CODE_LANGUAGES)
1007
+ })]).optional(),
1013
1008
  options: z.object({
1014
1009
  maxLength: z.number().optional(),
1015
1010
  minLength: z.number().optional(),
@@ -1060,6 +1055,8 @@ var SerializedArraySchema = z.lazy(function () {
1060
1055
  type: z.literal("array"),
1061
1056
  item: SerializedSchema,
1062
1057
  opt: z["boolean"](),
1058
+ // Only whether a preview EXISTS: the closure itself cannot be serialized.
1059
+ preview: z.literal(true).optional(),
1063
1060
  readonly: z["boolean"]().optional(),
1064
1061
  hidden: z["boolean"]().optional()
1065
1062
  });
@@ -1129,6 +1126,8 @@ var SerializedRecordSchema = z.lazy(function () {
1129
1126
  type: z.literal("record"),
1130
1127
  item: SerializedSchema,
1131
1128
  opt: z["boolean"](),
1129
+ // Only whether a preview EXISTS: the closure itself cannot be serialized.
1130
+ preview: z.literal(true).optional(),
1132
1131
  // Optional gallery marker for files/images
1133
1132
  mediaType: z.union([z.literal("files"), z.literal("images")]).optional(),
1134
1133
  // Optional legacy gallery metadata
@@ -1657,6 +1656,21 @@ var Api = {
1657
1656
  schemaSha: z.string(),
1658
1657
  sourcesSha: z.string(),
1659
1658
  patches: z.array(PatchId),
1659
+ /**
1660
+ * Unpublished changes the store threw away because it could not
1661
+ * read them.
1662
+ *
1663
+ * On stat rather than on `GET /patches` because stat is the
1664
+ * channel that always flows: the case worth reporting is a repair
1665
+ * that removed EVERYTHING, and then there is nothing left for the
1666
+ * studio to fetch, so a notice riding on the fetch is never
1667
+ * collected. Said once - the server drains it when it hands it
1668
+ * over.
1669
+ */
1670
+ removed: z.array(z.object({
1671
+ patchId: PatchId,
1672
+ reason: z.string()
1673
+ })).optional(),
1660
1674
  config: ValConfig,
1661
1675
  profileId: z.string().nullable(),
1662
1676
  mode: z.union([z.literal("http"), z.literal("fs")]),
@@ -1903,7 +1917,7 @@ var Api = {
1903
1917
  schemaSha: z.string(),
1904
1918
  sourcesSha: z.string(),
1905
1919
  modules: z.record(ModuleFilePath, z.object({
1906
- render: z.any().optional(),
1920
+ preview: z.any().optional(),
1907
1921
  // TODO: improve this type
1908
1922
  source: z.any().optional(),
1909
1923
  //.optional(), // TODO: Json zod type
@@ -1983,7 +1997,23 @@ var Api = {
1983
1997
  },
1984
1998
  res: z.union([unauthorizedResponse, z.object({
1985
1999
  status: z.literal(200),
1986
- json: z.object({}) // TODO:
2000
+ json: z.object({
2001
+ /**
2002
+ * Unpublished changes the save threw away because they could not be
2003
+ * applied.
2004
+ *
2005
+ * `fs` mode only, and the reason the save is a 200 rather than the
2006
+ * 400 it used to be: with auto-save on, refusing the whole commit
2007
+ * for one bad patch means nothing is ever written again. The rest is
2008
+ * written, these are removed, and the studio drops them locally and
2009
+ * says so.
2010
+ */
2011
+ removed: z.array(z.object({
2012
+ patchId: PatchId,
2013
+ moduleFilePath: ModuleFilePath,
2014
+ message: z.string()
2015
+ })).optional()
2016
+ })
1987
2017
  }), z.object({
1988
2018
  status: z.literal(409),
1989
2019
  json: z.object({
@@ -3293,4 +3323,82 @@ function resolveSchemaSourceFixes(validationErrors, snapshot) {
3293
3323
  return result;
3294
3324
  }
3295
3325
 
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 };
3326
+ /**
3327
+ * The protocol between the studio's canvas and the page inside it.
3328
+ *
3329
+ * The canvas shows the running site in a frame. That frame is a different
3330
+ * document — the customer's app, rendered by their components — so the studio
3331
+ * cannot reach into it, and it cannot reach out. Everything they say to each
3332
+ * other goes through `postMessage`, and this is the vocabulary.
3333
+ *
3334
+ * It lives in `shared` because both halves have to agree on it: the page half
3335
+ * ships in `@valbuild/next`, the studio half in `@valbuild/ui`, and a message
3336
+ * shape defined twice is a message shape that will differ once.
3337
+ *
3338
+ * ## Why a query parameter turns it on
3339
+ *
3340
+ * The frame has to render the page as Val sees it — draft content, with
3341
+ * `data-val-path` on every element Val tracks — while *not* showing Val's own
3342
+ * overlay, because the studio is already the UI. Nothing about the cookies can
3343
+ * express that: they are per-origin, and the studio and the page share an
3344
+ * origin, so a cookie would change the customer's own tabs too. A parameter on
3345
+ * the frame's URL is scoped to exactly the one document that needs it.
3346
+ */
3347
+
3348
+ /** Marks a page load as the studio's canvas frame. */
3349
+ var VAL_CANVAS_PARAM = "val_canvas";
3350
+
3351
+ /**
3352
+ * Every message is tagged with this, and the tag is checked on receipt.
3353
+ *
3354
+ * A window receives messages from anything that has a handle on it —
3355
+ * extensions, embeds, other frames — and `event.data` is whatever they chose to
3356
+ * send. Checking a discriminator before reading anything else is what keeps a
3357
+ * stray message from being read as a canvas instruction.
3358
+ */
3359
+ var VAL_CANVAS_MESSAGE = "val-canvas";
3360
+
3361
+ /** One element on the page that Val has a path for. */
3362
+
3363
+ /** What the page tells the studio. */
3364
+
3365
+ /** What the studio tells the page. */
3366
+
3367
+ /** Narrow an unknown `event.data` to a message from the page. */
3368
+ function isValCanvasPageMessage(data) {
3369
+ if (_typeof(data) !== "object" || data === null) return false;
3370
+ var message = data;
3371
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3372
+ return message.type === "ready" || message.type === "elements" || message.type === "clicked" || message.type === "refreshing";
3373
+ }
3374
+
3375
+ /** Narrow an unknown `event.data` to a message from the studio. */
3376
+ function isValCanvasStudioMessage(data) {
3377
+ if (_typeof(data) !== "object" || data === null) return false;
3378
+ var message = data;
3379
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
3380
+ return message.type === "rescan" || message.type === "highlight" || message.type === "setPicking" || message.type === "sourceUpdate" || message.type === "sourcesSynced";
3381
+ }
3382
+
3383
+ /**
3384
+ * Whether this document was loaded as the studio's canvas frame.
3385
+ *
3386
+ * Reads the location directly rather than taking it as an argument, because
3387
+ * every caller wants the answer for the document it is running in, and passing
3388
+ * it around is how one of them ends up asking about the wrong one.
3389
+ */
3390
+ function isValCanvasFrame(search) {
3391
+ try {
3392
+ return new URLSearchParams(search).get(VAL_CANVAS_PARAM) !== null;
3393
+ } catch (_unused) {
3394
+ return false;
3395
+ }
3396
+ }
3397
+
3398
+ /** Add the canvas marker to a page URL. */
3399
+ function withValCanvasParam(url) {
3400
+ var separator = url.includes("?") ? "&" : "?";
3401
+ return "".concat(url).concat(separator).concat(VAL_CANVAS_PARAM, "=1");
3402
+ }
3403
+
3404
+ 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.105.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.105.0"
35
35
  },
36
36
  "files": [
37
37
  "dist",