@pantheon-systems/p1-media 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { ReactElement, ImgHTMLAttributes, ReactNode } from 'react';
1
+ import { ImgHTMLAttributes, ReactElement, ReactNode } from 'react';
2
2
  import { ComponentConfig } from '@puckeditor/core';
3
3
 
4
4
  interface ImageTransformParams {
@@ -92,7 +92,7 @@ declare function getMediaProps(value: MediaFieldValue | null | undefined, option
92
92
  * (a broken foreign-origin image is never emitted). `alt` may be overridden;
93
93
  * otherwise it comes from the value.
94
94
  */
95
- declare function MediaImage({ image, mediaBaseUrl, transform, alt, ...rest }: {
95
+ declare function MediaImage({ image, mediaBaseUrl, transform, alt, loading, decoding, ...rest }: {
96
96
  image: MediaFieldValue | null | undefined;
97
97
  mediaBaseUrl?: string;
98
98
  transform?: ImageTransformParams;
@@ -102,17 +102,21 @@ declare function MediaImage({ image, mediaBaseUrl, transform, alt, ...rest }: {
102
102
  * ESCAPED text (React default escaping — never dangerouslySetInnerHTML, req.
103
103
  * R6). Renders nothing when the src is rejected/empty.
104
104
  */
105
- declare function MediaFigure({ image, schema, mediaBaseUrl, transform, className, captionClassName, }: {
105
+ declare function MediaFigure({ image, schema, mediaBaseUrl, transform, className, captionClassName, loading, decoding, }: {
106
106
  image: MediaFieldValue | null | undefined;
107
107
  schema?: MetadataFieldDef[];
108
108
  mediaBaseUrl?: string;
109
109
  transform?: ImageTransformParams;
110
110
  className?: string;
111
111
  captionClassName?: string;
112
+ /** Defaults to "lazy"; pass "eager" for an above-the-fold figure. */
113
+ loading?: ImgHTMLAttributes<HTMLImageElement>["loading"];
114
+ decoding?: ImgHTMLAttributes<HTMLImageElement>["decoding"];
112
115
  }): ReactElement | null;
113
116
 
114
117
  interface MediaFigureBlockProps {
115
118
  photo: MediaFieldValue | null;
119
+ loading?: "lazy" | "eager";
116
120
  }
117
121
  interface MediaFigureBlockOptions {
118
122
  /**
@@ -137,6 +141,12 @@ interface MediaFigureBlockOptions {
137
141
  captionClassName?: string;
138
142
  /** Rendered when no photo is chosen (or its URL fails origin validation). */
139
143
  placeholder?: ReactNode;
144
+ /**
145
+ * Default for the block's "Loading" field. "lazy" (the default) keeps
146
+ * off-screen images off the wire on image-heavy pages; use "eager" when the
147
+ * block is reliably above the fold.
148
+ */
149
+ defaultLoading?: "lazy" | "eager";
140
150
  }
141
151
  /**
142
152
  * Builds a registerable Puck component around the rich `p1-media` field and
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ReactElement, ImgHTMLAttributes, ReactNode } from 'react';
1
+ import { ImgHTMLAttributes, ReactElement, ReactNode } from 'react';
2
2
  import { ComponentConfig } from '@puckeditor/core';
3
3
 
4
4
  interface ImageTransformParams {
@@ -92,7 +92,7 @@ declare function getMediaProps(value: MediaFieldValue | null | undefined, option
92
92
  * (a broken foreign-origin image is never emitted). `alt` may be overridden;
93
93
  * otherwise it comes from the value.
94
94
  */
95
- declare function MediaImage({ image, mediaBaseUrl, transform, alt, ...rest }: {
95
+ declare function MediaImage({ image, mediaBaseUrl, transform, alt, loading, decoding, ...rest }: {
96
96
  image: MediaFieldValue | null | undefined;
97
97
  mediaBaseUrl?: string;
98
98
  transform?: ImageTransformParams;
@@ -102,17 +102,21 @@ declare function MediaImage({ image, mediaBaseUrl, transform, alt, ...rest }: {
102
102
  * ESCAPED text (React default escaping — never dangerouslySetInnerHTML, req.
103
103
  * R6). Renders nothing when the src is rejected/empty.
104
104
  */
105
- declare function MediaFigure({ image, schema, mediaBaseUrl, transform, className, captionClassName, }: {
105
+ declare function MediaFigure({ image, schema, mediaBaseUrl, transform, className, captionClassName, loading, decoding, }: {
106
106
  image: MediaFieldValue | null | undefined;
107
107
  schema?: MetadataFieldDef[];
108
108
  mediaBaseUrl?: string;
109
109
  transform?: ImageTransformParams;
110
110
  className?: string;
111
111
  captionClassName?: string;
112
+ /** Defaults to "lazy"; pass "eager" for an above-the-fold figure. */
113
+ loading?: ImgHTMLAttributes<HTMLImageElement>["loading"];
114
+ decoding?: ImgHTMLAttributes<HTMLImageElement>["decoding"];
112
115
  }): ReactElement | null;
113
116
 
114
117
  interface MediaFigureBlockProps {
115
118
  photo: MediaFieldValue | null;
119
+ loading?: "lazy" | "eager";
116
120
  }
117
121
  interface MediaFigureBlockOptions {
118
122
  /**
@@ -137,6 +141,12 @@ interface MediaFigureBlockOptions {
137
141
  captionClassName?: string;
138
142
  /** Rendered when no photo is chosen (or its URL fails origin validation). */
139
143
  placeholder?: ReactNode;
144
+ /**
145
+ * Default for the block's "Loading" field. "lazy" (the default) keeps
146
+ * off-screen images off the wire on image-heavy pages; use "eager" when the
147
+ * block is reliably above the fold.
148
+ */
149
+ defaultLoading?: "lazy" | "eager";
140
150
  }
141
151
  /**
142
152
  * Builds a registerable Puck component around the rich `p1-media` field and
package/dist/server.js CHANGED
@@ -119,6 +119,8 @@ function MediaImage({
119
119
  mediaBaseUrl,
120
120
  transform,
121
121
  alt,
122
+ loading = "lazy",
123
+ decoding = "async",
122
124
  ...rest
123
125
  }) {
124
126
  const props = getMediaProps(image, { mediaBaseUrl, transform });
@@ -128,6 +130,8 @@ function MediaImage({
128
130
  {
129
131
  src: props.src,
130
132
  alt: alt ?? props.alt,
133
+ loading,
134
+ decoding,
131
135
  ...props.width !== void 0 ? { width: props.width } : {},
132
136
  ...props.height !== void 0 ? { height: props.height } : {},
133
137
  ...rest
@@ -160,7 +164,9 @@ function MediaFigure({
160
164
  mediaBaseUrl,
161
165
  transform,
162
166
  className,
163
- captionClassName
167
+ captionClassName,
168
+ loading = "lazy",
169
+ decoding = "async"
164
170
  }) {
165
171
  const props = getMediaProps(image, { mediaBaseUrl, transform });
166
172
  if (!props.src) return null;
@@ -171,6 +177,8 @@ function MediaFigure({
171
177
  {
172
178
  src: props.src,
173
179
  alt: props.alt,
180
+ loading,
181
+ decoding,
174
182
  ...props.width !== void 0 ? { width: props.width } : {},
175
183
  ...props.height !== void 0 ? { height: props.height } : {}
176
184
  }
@@ -202,19 +210,29 @@ function createMediaFigureBlock(options) {
202
210
  schema,
203
211
  className,
204
212
  captionClassName,
205
- placeholder = "Choose a photo from the media library"
213
+ placeholder = "Choose a photo from the media library",
214
+ defaultLoading = "lazy"
206
215
  } = options;
207
216
  return {
208
217
  label,
209
218
  fields: {
210
219
  // `p1-media` is registered by createMediaPlugin via overrides.fieldTypes,
211
220
  // so it is not part of Puck's built-in Field union.
212
- photo: { type: "p1-media", label: fieldLabel }
221
+ photo: { type: "p1-media", label: fieldLabel },
222
+ loading: {
223
+ type: "radio",
224
+ label: "Loading",
225
+ options: [
226
+ { label: "Lazy", value: "lazy" },
227
+ { label: "Eager", value: "eager" }
228
+ ]
229
+ }
213
230
  },
214
231
  defaultProps: {
215
- photo: null
232
+ photo: null,
233
+ loading: defaultLoading
216
234
  },
217
- render: ({ photo }) => {
235
+ render: ({ photo, loading }) => {
218
236
  const { src } = getMediaProps(photo ?? null, { mediaBaseUrl });
219
237
  if (!src) {
220
238
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: placeholderStyle, children: placeholder });
@@ -227,7 +245,8 @@ function createMediaFigureBlock(options) {
227
245
  transform,
228
246
  schema,
229
247
  className,
230
- captionClassName
248
+ captionClassName,
249
+ loading: loading === "eager" ? "eager" : "lazy"
231
250
  }
232
251
  );
233
252
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/utils.ts","../src/media-value.ts","../src/render.tsx","../src/media-figure-block.tsx"],"sourcesContent":["// Server-safe entry point — no React context, safe for RSC.\n// Next.js resolves this via the \"react-server\" exports condition.\nexport { buildImageUrl } from \"./utils\";\nexport type { ImageTransformParams } from \"./utils\";\nexport { getMediaProps, MediaImage, MediaFigure } from \"./render\";\nexport type { GetMediaPropsOptions } from \"./render\";\nexport { createMediaFigureBlock } from \"./media-figure-block\";\nexport type { MediaFigureBlockOptions, MediaFigureBlockProps } from \"./media-figure-block\";\nexport { makeMediaValue, isMediaValue } from \"./media-value\";\nexport type {\n MediaValue,\n MediaFieldValue,\n MediaProps,\n MetadataFieldDef,\n} from \"./types\";\n","export interface ImageTransformParams {\n width?: number;\n height?: number;\n /** `auto` negotiates avif → webp → jpeg from the request's Accept header. */\n format?: \"auto\" | \"webp\" | \"jpeg\" | \"png\" | \"gif\" | \"avif\";\n quality?: number;\n}\n\n/**\n * Merges Cloudflare Images transform params onto a CDN image URL.\n * Preserves any existing params in the URL — e.g. the editor's crop intent\n * (`fit=cover&gravity=auto` for smart crop, `trim.*` for a manual crop).\n *\n * @example\n * buildImageUrl(data.heroImage, { width: 1200, height: 630, format: \"webp\" })\n */\nexport function buildImageUrl(url: string, params: ImageTransformParams): string {\n if (!url) return url;\n const parsed = new URL(url);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return url;\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined) {\n parsed.searchParams.set(k, String(v));\n }\n }\n return parsed.toString();\n}\n","import type { MediaFieldValue, MediaValue } from \"./types\";\nimport {\n buildValueWithCrop,\n buildValueWithTrim,\n getBaseUrl,\n type CropMode,\n type TrimRect,\n} from \"./crop\";\n\n/**\n * Structural keys on a MediaValue that are not schema-driven metadata. Used to\n * avoid clobbering identity fields when copying metadata defaults into a value.\n */\nexport const STRUCTURAL_MEDIA_KEYS = new Set([\n \"assetId\",\n \"versionId\",\n \"url\",\n \"metaSchemaVersion\",\n]);\n\n/** Narrows a MediaFieldValue to the rich object form. */\nexport function isMediaValue(\n value: MediaFieldValue | null | undefined,\n): value is MediaValue {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Builds the value the `p1-media` field writes on asset selection.\n *\n * R10 invariant: never synthesize a MediaValue unless BOTH `assetId` and\n * `versionId` are present — otherwise fall back to a bare URL string (basic\n * mode). A value with undefined identity is unfindable by \"update usages\" and\n * can render `src=undefined`, so this fallback is load-bearing, not cosmetic.\n * It also means picking from a pre-upgrade Worker (bare-array `GET /media`, no\n * assetId) yields a string, so the plugin degrades cleanly before the cutover.\n */\nexport function makeMediaValue(input: {\n assetId?: string | null;\n versionId?: string | null;\n url: string;\n metaSchemaVersion?: number;\n metadata?: Record<string, string | number | undefined>;\n}): MediaFieldValue {\n if (!input.assetId || !input.versionId) {\n return input.url; // R10 fallback: string value\n }\n const value: MediaValue = {\n assetId: input.assetId,\n versionId: input.versionId,\n url: input.url,\n };\n if (typeof input.metaSchemaVersion === \"number\") {\n value.metaSchemaVersion = input.metaSchemaVersion;\n }\n if (input.metadata) {\n for (const [k, v] of Object.entries(input.metadata)) {\n if (v === undefined || v === \"\") continue;\n if (STRUCTURAL_MEDIA_KEYS.has(k)) continue; // never let metadata clobber identity\n value[k] = v;\n }\n }\n return value;\n}\n\n/** The asset fields the field editor needs to build a value on selection. */\nexport interface SelectableAsset {\n assetId?: string | null;\n versionId?: string | null;\n url: string;\n width?: number;\n height?: number;\n metadata?: Record<string, string>;\n metaSchemaVersion?: number;\n}\n\n/**\n * Builds the value written when an asset is picked from the library, applying\n * the current crop and copying metadata (flat, incl. alt) plus captured\n * dimensions. Delegates to makeMediaValue, so R10 still holds (a pre-cutover\n * asset with no identity yields a string).\n */\nexport function buildValueFromAsset(\n asset: SelectableAsset,\n crop: CropMode,\n): MediaFieldValue {\n const metadata: Record<string, string | number | undefined> = { ...(asset.metadata ?? {}) };\n if (typeof asset.width === \"number\") metadata.width = asset.width;\n if (typeof asset.height === \"number\") metadata.height = asset.height;\n return makeMediaValue({\n assetId: asset.assetId,\n versionId: asset.versionId,\n url: buildValueWithCrop(getBaseUrl(asset.url), crop),\n metaSchemaVersion: asset.metaSchemaVersion,\n metadata,\n });\n}\n\n/**\n * Applies a crop mode to a field value. R10: a string stays a string (basic\n * mode) — crop never promotes a value to a partial-identity object.\n */\nexport function applyCropToValue(\n value: MediaFieldValue,\n crop: CropMode,\n): MediaFieldValue {\n const currentUrl = isMediaValue(value) ? value.url : value;\n const baseUrl = getBaseUrl(typeof currentUrl === \"string\" ? currentUrl : \"\");\n if (!baseUrl) return value;\n const url = buildValueWithCrop(baseUrl, crop);\n return isMediaValue(value) ? { ...value, url } : url;\n}\n\n/**\n * Applies a manual crop region (the interactive cropper's result) to a field\n * value. Same R10 shape as applyCropToValue: a string stays a string, an\n * object keeps its identity and only swaps `url`.\n */\nexport function applyTrimToValue(\n value: MediaFieldValue,\n rect: TrimRect,\n): MediaFieldValue {\n const currentUrl = isMediaValue(value) ? value.url : value;\n const baseUrl = getBaseUrl(typeof currentUrl === \"string\" ? currentUrl : \"\");\n if (!baseUrl) return value;\n const url = buildValueWithTrim(baseUrl, rect);\n return isMediaValue(value) ? { ...value, url } : url;\n}\n\n/**\n * Sets one metadata field on a value. R10: refuses to operate on a string\n * (returns it unchanged — metadata can't be attached until an asset is picked)\n * and never lets a structural key clobber the pinned identity.\n */\nexport function setMetaOnValue(\n value: MediaFieldValue,\n fieldName: string,\n fieldValue: string,\n): MediaFieldValue {\n if (!isMediaValue(value)) return value;\n if (STRUCTURAL_MEDIA_KEYS.has(fieldName)) return value;\n return { ...value, [fieldName]: fieldValue };\n}\n","// Render-side helpers. Server-safe (no React context, no client-only hooks) so\n// they can be imported from RSC via the package's \"react-server\" export.\nimport type { ImgHTMLAttributes, ReactElement } from \"react\";\nimport type {\n MediaFieldValue,\n MediaProps,\n MetadataFieldDef,\n} from \"./types\";\nimport { isMediaValue, STRUCTURAL_MEDIA_KEYS } from \"./media-value\";\nimport { buildImageUrl, type ImageTransformParams } from \"./utils\";\n\nexport interface GetMediaPropsOptions {\n /**\n * The CDN image origin that serves media, e.g.\n * \"https://staging.media.p1.pantheon.io\". This is the public image host —\n * NOT the Worker API URL (`…workers.dev`), whose origin would reject every\n * real image URL.\n *\n * Security (required): a value's `url` is untrusted document content — anyone\n * who can edit a document, or call the CCR `/edits` API, controls it. Without\n * an origin check a crafted `https://evil.example/beacon.png` turns every\n * published render into a visitor-IP exfil beacon (and an SSRF under\n * server-fetching `next/image`). getMediaProps therefore rejects any url that\n * is not `https` on this exact origin. If `mediaBaseUrl` is omitted it\n * **fails closed** (empty src) rather than degrading to an insecure pass-through.\n *\n * Local dev exception: when the configured base is itself `http` on a\n * loopback host (`localhost`, `127.0.0.1`, `[::1]` — a local `wrangler dev`\n * worker), same-origin `http` urls are allowed so rich values render locally.\n */\n mediaBaseUrl?: string;\n /** Transform params merged onto the validated URL (width, height, format, quality). */\n transform?: ImageTransformParams;\n}\n\n// Hosts where an http mediaBaseUrl is honored (local `wrangler dev`). Exact\n// hostnames only — no *.localhost subdomains.\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n\n/**\n * Returns the url only if it is on the configured CDN origin AND https —\n * except that an http origin is honored when the configured mediaBaseUrl is\n * itself http on a loopback host (local dev against `wrangler dev`, which\n * serves plain http). Production is unaffected: real CDN bases are https, so\n * every http url still fails the origin check. Otherwise \"\". Fails closed\n * when no origin is configured (see GetMediaPropsOptions).\n */\nfunction validateSrc(url: string, mediaBaseUrl?: string): string {\n if (!url || !mediaBaseUrl) return \"\";\n let parsed: URL;\n let base: URL;\n try {\n parsed = new URL(url);\n base = new URL(mediaBaseUrl);\n } catch {\n return \"\";\n }\n if (parsed.origin !== base.origin) return \"\";\n if (parsed.protocol !== \"https:\") {\n // Non-https is allowed only as plain http against an explicitly-configured\n // local-dev loopback base. Checking parsed.protocol too keeps schemes whose\n // origin serializes to the base origin (blob:http://…) out of the carve-out.\n const isLocalDevBase = base.protocol === \"http:\" && LOOPBACK_HOSTS.has(base.hostname);\n if (!isLocalDevBase || parsed.protocol !== \"http:\") return \"\";\n }\n return url;\n}\n\n/**\n * Normalizes a `string | MediaValue | null` into `{ src, alt, width?, height? }`.\n * A string (basic mode) yields `alt: \"\"`. `src` is validated against the CDN\n * origin (see GetMediaPropsOptions) and empty on rejection.\n */\nexport function getMediaProps(\n value: MediaFieldValue | null | undefined,\n options?: GetMediaPropsOptions,\n): MediaProps {\n const mediaBaseUrl = options?.mediaBaseUrl;\n const transform = options?.transform;\n\n const finalize = (rawUrl: string): string => {\n const validated = validateSrc(rawUrl, mediaBaseUrl);\n return validated && transform ? buildImageUrl(validated, transform) : validated;\n };\n\n if (value == null) return { src: \"\", alt: \"\" };\n\n if (typeof value === \"string\") {\n return { src: finalize(value), alt: \"\" };\n }\n\n const props: MediaProps = {\n src: finalize(typeof value.url === \"string\" ? value.url : \"\"),\n alt: typeof value.alt === \"string\" ? value.alt : \"\",\n };\n if (typeof value.width === \"number\") props.width = value.width;\n if (typeof value.height === \"number\") props.height = value.height;\n return props;\n}\n\n/**\n * Common-case `<img>` wrapper. Renders nothing when the src is rejected/empty\n * (a broken foreign-origin image is never emitted). `alt` may be overridden;\n * otherwise it comes from the value.\n */\nexport function MediaImage({\n image,\n mediaBaseUrl,\n transform,\n alt,\n ...rest\n}: {\n image: MediaFieldValue | null | undefined;\n mediaBaseUrl?: string;\n transform?: ImageTransformParams;\n} & Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\">): ReactElement | null {\n const props = getMediaProps(image, { mediaBaseUrl, transform });\n if (!props.src) return null;\n return (\n <img\n src={props.src}\n alt={alt ?? props.alt}\n {...(props.width !== undefined ? { width: props.width } : {})}\n {...(props.height !== undefined ? { height: props.height } : {})}\n {...rest}\n />\n );\n}\n\n/**\n * Collects the metadata fields a figure should render, generically (req. R14):\n * with a schema, iterate its entries (canonical order/labels); without one,\n * iterate the value's own string metadata keys. Either way a new backend text\n * field appears with no plugin release. `alt` (on the `<img>`) and non-text\n * values (width/height/metaSchemaVersion) are excluded from the caption.\n */\nfunction collectFigureFields(\n image: MediaFieldValue | null | undefined,\n schema?: MetadataFieldDef[],\n): Array<{ name: string; label?: string; value: string }> {\n if (!isMediaValue(image)) return [];\n const out: Array<{ name: string; label?: string; value: string }> = [];\n if (schema && schema.length > 0) {\n for (const entry of schema) {\n if (entry.name === \"alt\") continue;\n const raw = image[entry.name];\n if (raw === undefined || raw === \"\") continue;\n out.push({ name: entry.name, label: entry.label, value: String(raw) });\n }\n } else {\n for (const [k, v] of Object.entries(image)) {\n if (k === \"alt\") continue;\n if (STRUCTURAL_MEDIA_KEYS.has(k)) continue;\n if (typeof v !== \"string\" || v === \"\") continue;\n out.push({ name: k, value: v });\n }\n }\n return out;\n}\n\n/**\n * `<figure>` that renders the image plus the schema-advertised text fields as\n * ESCAPED text (React default escaping — never dangerouslySetInnerHTML, req.\n * R6). Renders nothing when the src is rejected/empty.\n */\nexport function MediaFigure({\n image,\n schema,\n mediaBaseUrl,\n transform,\n className,\n captionClassName,\n}: {\n image: MediaFieldValue | null | undefined;\n schema?: MetadataFieldDef[];\n mediaBaseUrl?: string;\n transform?: ImageTransformParams;\n className?: string;\n captionClassName?: string;\n}): ReactElement | null {\n const props = getMediaProps(image, { mediaBaseUrl, transform });\n if (!props.src) return null;\n\n const fields = collectFigureFields(image, schema);\n\n return (\n <figure className={className}>\n <img\n src={props.src}\n alt={props.alt}\n {...(props.width !== undefined ? { width: props.width } : {})}\n {...(props.height !== undefined ? { height: props.height } : {})}\n />\n {fields.length > 0 && (\n <figcaption className={captionClassName}>\n {fields.map((f) => (\n <span key={f.name} data-field={f.name}>\n {f.value}\n </span>\n ))}\n </figcaption>\n )}\n </figure>\n );\n}\n","// Ready-made Puck component for the rich `p1-media` field. Server-safe (no\n// hooks, no context) so it can be registered in configs that render via RSC.\nimport type { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport type { ComponentConfig, Field } from \"@puckeditor/core\";\nimport { getMediaProps, MediaFigure } from \"./render\";\nimport type { ImageTransformParams } from \"./utils\";\nimport type { MediaFieldValue, MetadataFieldDef } from \"./types\";\n\nexport interface MediaFigureBlockProps {\n photo: MediaFieldValue | null;\n}\n\nexport interface MediaFigureBlockOptions {\n /**\n * The CDN image origin used to validate value URLs — NOT the Worker API\n * URL. See GetMediaPropsOptions. Defaults to the production origin\n * (\"https://media.p1.pantheon.io\"); override for sandbox/staging/local dev.\n */\n mediaBaseUrl?: string;\n /**\n * Render-time transform. Include BOTH width and height — the editor's crop\n * intent only changes the output when there is a target aspect ratio.\n * Defaults to `{ width: 1200, height: 630, format: \"auto\" }`.\n */\n transform?: ImageTransformParams;\n /** Component label in the Puck sidebar. Defaults to \"Media Figure\". */\n label?: string;\n /** Label of the media field. Defaults to \"Photo\". */\n fieldLabel?: string;\n /** Metadata schema passed to MediaFigure — pins figcaption field order/labels. */\n schema?: MetadataFieldDef[];\n className?: string;\n captionClassName?: string;\n /** Rendered when no photo is chosen (or its URL fails origin validation). */\n placeholder?: ReactNode;\n}\n\nconst DEFAULT_TRANSFORM: ImageTransformParams = { width: 1200, height: 630, format: \"auto\" };\n\n/** Same production default as createMediaPlugin's workerUrl — same host serves both. */\nconst DEFAULT_MEDIA_BASE_URL = \"https://media.p1.pantheon.io\";\n\nconst placeholderStyle: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"200px\",\n borderRadius: \"8px\",\n backgroundColor: \"#e5e5e5\",\n color: \"#525252\",\n fontSize: \"14px\",\n};\n\n/**\n * Builds a registerable Puck component around the rich `p1-media` field and\n * `MediaFigure`. Spares consumers the custom-field-type cast and the\n * width+height transform pitfall:\n *\n * ```tsx\n * const config = {\n * components: {\n * MediaFigureBlock: createMediaFigureBlock({ mediaBaseUrl: MEDIA_BASE }),\n * },\n * };\n * ```\n */\nexport function createMediaFigureBlock(\n options: MediaFigureBlockOptions,\n): ComponentConfig<MediaFigureBlockProps> {\n const {\n mediaBaseUrl = DEFAULT_MEDIA_BASE_URL,\n transform = DEFAULT_TRANSFORM,\n label = \"Media Figure\",\n fieldLabel = \"Photo\",\n schema,\n className,\n captionClassName,\n placeholder = \"Choose a photo from the media library\",\n } = options;\n\n return {\n label,\n fields: {\n // `p1-media` is registered by createMediaPlugin via overrides.fieldTypes,\n // so it is not part of Puck's built-in Field union.\n photo: { type: \"p1-media\", label: fieldLabel } as unknown as Field,\n },\n defaultProps: {\n photo: null,\n },\n render: ({ photo }: MediaFigureBlockProps): ReactElement => {\n const { src } = getMediaProps(photo ?? null, { mediaBaseUrl });\n if (!src) {\n return <div style={placeholderStyle}>{placeholder}</div>;\n }\n return (\n <MediaFigure\n image={photo ?? null}\n mediaBaseUrl={mediaBaseUrl}\n transform={transform}\n schema={schema}\n className={className}\n captionClassName={captionClassName}\n />\n );\n },\n };\n}\n"],"mappings":"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","names":["import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/server.ts","../src/utils.ts","../src/media-value.ts","../src/render.tsx","../src/media-figure-block.tsx"],"sourcesContent":["// Server-safe entry point — no React context, safe for RSC.\n// Next.js resolves this via the \"react-server\" exports condition.\nexport { buildImageUrl } from \"./utils\";\nexport type { ImageTransformParams } from \"./utils\";\nexport { getMediaProps, MediaImage, MediaFigure } from \"./render\";\nexport type { GetMediaPropsOptions } from \"./render\";\nexport { createMediaFigureBlock } from \"./media-figure-block\";\nexport type { MediaFigureBlockOptions, MediaFigureBlockProps } from \"./media-figure-block\";\nexport { makeMediaValue, isMediaValue } from \"./media-value\";\nexport type {\n MediaValue,\n MediaFieldValue,\n MediaProps,\n MetadataFieldDef,\n} from \"./types\";\n","export interface ImageTransformParams {\n width?: number;\n height?: number;\n /** `auto` negotiates avif → webp → jpeg from the request's Accept header. */\n format?: \"auto\" | \"webp\" | \"jpeg\" | \"png\" | \"gif\" | \"avif\";\n quality?: number;\n}\n\n/**\n * Merges Cloudflare Images transform params onto a CDN image URL.\n * Preserves any existing params in the URL — e.g. the editor's crop intent\n * (`fit=cover&gravity=auto` for smart crop, `trim.*` for a manual crop).\n *\n * @example\n * buildImageUrl(data.heroImage, { width: 1200, height: 630, format: \"webp\" })\n */\nexport function buildImageUrl(url: string, params: ImageTransformParams): string {\n if (!url) return url;\n const parsed = new URL(url);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return url;\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined) {\n parsed.searchParams.set(k, String(v));\n }\n }\n return parsed.toString();\n}\n","import type { MediaFieldValue, MediaValue } from \"./types\";\nimport {\n buildValueWithCrop,\n buildValueWithTrim,\n getBaseUrl,\n type CropMode,\n type TrimRect,\n} from \"./crop\";\n\n/**\n * Structural keys on a MediaValue that are not schema-driven metadata. Used to\n * avoid clobbering identity fields when copying metadata defaults into a value.\n */\nexport const STRUCTURAL_MEDIA_KEYS = new Set([\n \"assetId\",\n \"versionId\",\n \"url\",\n \"metaSchemaVersion\",\n]);\n\n/** Narrows a MediaFieldValue to the rich object form. */\nexport function isMediaValue(\n value: MediaFieldValue | null | undefined,\n): value is MediaValue {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Builds the value the `p1-media` field writes on asset selection.\n *\n * R10 invariant: never synthesize a MediaValue unless BOTH `assetId` and\n * `versionId` are present — otherwise fall back to a bare URL string (basic\n * mode). A value with undefined identity is unfindable by \"update usages\" and\n * can render `src=undefined`, so this fallback is load-bearing, not cosmetic.\n * It also means picking from a pre-upgrade Worker (bare-array `GET /media`, no\n * assetId) yields a string, so the plugin degrades cleanly before the cutover.\n */\nexport function makeMediaValue(input: {\n assetId?: string | null;\n versionId?: string | null;\n url: string;\n metaSchemaVersion?: number;\n metadata?: Record<string, string | number | undefined>;\n}): MediaFieldValue {\n if (!input.assetId || !input.versionId) {\n return input.url; // R10 fallback: string value\n }\n const value: MediaValue = {\n assetId: input.assetId,\n versionId: input.versionId,\n url: input.url,\n };\n if (typeof input.metaSchemaVersion === \"number\") {\n value.metaSchemaVersion = input.metaSchemaVersion;\n }\n if (input.metadata) {\n for (const [k, v] of Object.entries(input.metadata)) {\n if (v === undefined || v === \"\") continue;\n if (STRUCTURAL_MEDIA_KEYS.has(k)) continue; // never let metadata clobber identity\n value[k] = v;\n }\n }\n return value;\n}\n\n/** The asset fields the field editor needs to build a value on selection. */\nexport interface SelectableAsset {\n assetId?: string | null;\n versionId?: string | null;\n url: string;\n width?: number;\n height?: number;\n metadata?: Record<string, string>;\n metaSchemaVersion?: number;\n}\n\n/**\n * Builds the value written when an asset is picked from the library, applying\n * the current crop and copying metadata (flat, incl. alt) plus captured\n * dimensions. Delegates to makeMediaValue, so R10 still holds (a pre-cutover\n * asset with no identity yields a string).\n */\nexport function buildValueFromAsset(\n asset: SelectableAsset,\n crop: CropMode,\n): MediaFieldValue {\n const metadata: Record<string, string | number | undefined> = { ...(asset.metadata ?? {}) };\n if (typeof asset.width === \"number\") metadata.width = asset.width;\n if (typeof asset.height === \"number\") metadata.height = asset.height;\n return makeMediaValue({\n assetId: asset.assetId,\n versionId: asset.versionId,\n url: buildValueWithCrop(getBaseUrl(asset.url), crop),\n metaSchemaVersion: asset.metaSchemaVersion,\n metadata,\n });\n}\n\n/**\n * Applies a crop mode to a field value. R10: a string stays a string (basic\n * mode) — crop never promotes a value to a partial-identity object.\n */\nexport function applyCropToValue(\n value: MediaFieldValue,\n crop: CropMode,\n): MediaFieldValue {\n const currentUrl = isMediaValue(value) ? value.url : value;\n const baseUrl = getBaseUrl(typeof currentUrl === \"string\" ? currentUrl : \"\");\n if (!baseUrl) return value;\n const url = buildValueWithCrop(baseUrl, crop);\n return isMediaValue(value) ? { ...value, url } : url;\n}\n\n/**\n * Applies a manual crop region (the interactive cropper's result) to a field\n * value. Same R10 shape as applyCropToValue: a string stays a string, an\n * object keeps its identity and only swaps `url`.\n */\nexport function applyTrimToValue(\n value: MediaFieldValue,\n rect: TrimRect,\n): MediaFieldValue {\n const currentUrl = isMediaValue(value) ? value.url : value;\n const baseUrl = getBaseUrl(typeof currentUrl === \"string\" ? currentUrl : \"\");\n if (!baseUrl) return value;\n const url = buildValueWithTrim(baseUrl, rect);\n return isMediaValue(value) ? { ...value, url } : url;\n}\n\n/**\n * Sets one metadata field on a value. R10: refuses to operate on a string\n * (returns it unchanged — metadata can't be attached until an asset is picked)\n * and never lets a structural key clobber the pinned identity.\n */\nexport function setMetaOnValue(\n value: MediaFieldValue,\n fieldName: string,\n fieldValue: string,\n): MediaFieldValue {\n if (!isMediaValue(value)) return value;\n if (STRUCTURAL_MEDIA_KEYS.has(fieldName)) return value;\n return { ...value, [fieldName]: fieldValue };\n}\n","// Render-side helpers. Server-safe (no React context, no client-only hooks) so\n// they can be imported from RSC via the package's \"react-server\" export.\nimport type { ImgHTMLAttributes, ReactElement } from \"react\";\nimport type {\n MediaFieldValue,\n MediaProps,\n MetadataFieldDef,\n} from \"./types\";\nimport { isMediaValue, STRUCTURAL_MEDIA_KEYS } from \"./media-value\";\nimport { buildImageUrl, type ImageTransformParams } from \"./utils\";\n\nexport interface GetMediaPropsOptions {\n /**\n * The CDN image origin that serves media, e.g.\n * \"https://staging.media.p1.pantheon.io\". This is the public image host —\n * NOT the Worker API URL (`…workers.dev`), whose origin would reject every\n * real image URL.\n *\n * Security (required): a value's `url` is untrusted document content — anyone\n * who can edit a document, or call the CCR `/edits` API, controls it. Without\n * an origin check a crafted `https://evil.example/beacon.png` turns every\n * published render into a visitor-IP exfil beacon (and an SSRF under\n * server-fetching `next/image`). getMediaProps therefore rejects any url that\n * is not `https` on this exact origin. If `mediaBaseUrl` is omitted it\n * **fails closed** (empty src) rather than degrading to an insecure pass-through.\n *\n * Local dev exception: when the configured base is itself `http` on a\n * loopback host (`localhost`, `127.0.0.1`, `[::1]` — a local `wrangler dev`\n * worker), same-origin `http` urls are allowed so rich values render locally.\n */\n mediaBaseUrl?: string;\n /** Transform params merged onto the validated URL (width, height, format, quality). */\n transform?: ImageTransformParams;\n}\n\n// Hosts where an http mediaBaseUrl is honored (local `wrangler dev`). Exact\n// hostnames only — no *.localhost subdomains.\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n\n/**\n * Returns the url only if it is on the configured CDN origin AND https —\n * except that an http origin is honored when the configured mediaBaseUrl is\n * itself http on a loopback host (local dev against `wrangler dev`, which\n * serves plain http). Production is unaffected: real CDN bases are https, so\n * every http url still fails the origin check. Otherwise \"\". Fails closed\n * when no origin is configured (see GetMediaPropsOptions).\n */\nfunction validateSrc(url: string, mediaBaseUrl?: string): string {\n if (!url || !mediaBaseUrl) return \"\";\n let parsed: URL;\n let base: URL;\n try {\n parsed = new URL(url);\n base = new URL(mediaBaseUrl);\n } catch {\n return \"\";\n }\n if (parsed.origin !== base.origin) return \"\";\n if (parsed.protocol !== \"https:\") {\n // Non-https is allowed only as plain http against an explicitly-configured\n // local-dev loopback base. Checking parsed.protocol too keeps schemes whose\n // origin serializes to the base origin (blob:http://…) out of the carve-out.\n const isLocalDevBase = base.protocol === \"http:\" && LOOPBACK_HOSTS.has(base.hostname);\n if (!isLocalDevBase || parsed.protocol !== \"http:\") return \"\";\n }\n return url;\n}\n\n/**\n * Normalizes a `string | MediaValue | null` into `{ src, alt, width?, height? }`.\n * A string (basic mode) yields `alt: \"\"`. `src` is validated against the CDN\n * origin (see GetMediaPropsOptions) and empty on rejection.\n */\nexport function getMediaProps(\n value: MediaFieldValue | null | undefined,\n options?: GetMediaPropsOptions,\n): MediaProps {\n const mediaBaseUrl = options?.mediaBaseUrl;\n const transform = options?.transform;\n\n const finalize = (rawUrl: string): string => {\n const validated = validateSrc(rawUrl, mediaBaseUrl);\n return validated && transform ? buildImageUrl(validated, transform) : validated;\n };\n\n if (value == null) return { src: \"\", alt: \"\" };\n\n if (typeof value === \"string\") {\n return { src: finalize(value), alt: \"\" };\n }\n\n const props: MediaProps = {\n src: finalize(typeof value.url === \"string\" ? value.url : \"\"),\n alt: typeof value.alt === \"string\" ? value.alt : \"\",\n };\n if (typeof value.width === \"number\") props.width = value.width;\n if (typeof value.height === \"number\") props.height = value.height;\n return props;\n}\n\n/**\n * Common-case `<img>` wrapper. Renders nothing when the src is rejected/empty\n * (a broken foreign-origin image is never emitted). `alt` may be overridden;\n * otherwise it comes from the value.\n */\nexport function MediaImage({\n image,\n mediaBaseUrl,\n transform,\n alt,\n loading = \"lazy\",\n decoding = \"async\",\n ...rest\n}: {\n image: MediaFieldValue | null | undefined;\n mediaBaseUrl?: string;\n transform?: ImageTransformParams;\n} & Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\">): ReactElement | null {\n const props = getMediaProps(image, { mediaBaseUrl, transform });\n if (!props.src) return null;\n return (\n <img\n src={props.src}\n alt={alt ?? props.alt}\n loading={loading}\n decoding={decoding}\n {...(props.width !== undefined ? { width: props.width } : {})}\n {...(props.height !== undefined ? { height: props.height } : {})}\n {...rest}\n />\n );\n}\n\n/**\n * Collects the metadata fields a figure should render, generically (req. R14):\n * with a schema, iterate its entries (canonical order/labels); without one,\n * iterate the value's own string metadata keys. Either way a new backend text\n * field appears with no plugin release. `alt` (on the `<img>`) and non-text\n * values (width/height/metaSchemaVersion) are excluded from the caption.\n */\nfunction collectFigureFields(\n image: MediaFieldValue | null | undefined,\n schema?: MetadataFieldDef[],\n): { name: string; label?: string; value: string }[] {\n if (!isMediaValue(image)) return [];\n const out: { name: string; label?: string; value: string }[] = [];\n if (schema && schema.length > 0) {\n for (const entry of schema) {\n if (entry.name === \"alt\") continue;\n const raw = image[entry.name];\n if (raw === undefined || raw === \"\") continue;\n out.push({ name: entry.name, label: entry.label, value: String(raw) });\n }\n } else {\n for (const [k, v] of Object.entries(image)) {\n if (k === \"alt\") continue;\n if (STRUCTURAL_MEDIA_KEYS.has(k)) continue;\n if (typeof v !== \"string\" || v === \"\") continue;\n out.push({ name: k, value: v });\n }\n }\n return out;\n}\n\n/**\n * `<figure>` that renders the image plus the schema-advertised text fields as\n * ESCAPED text (React default escaping — never dangerouslySetInnerHTML, req.\n * R6). Renders nothing when the src is rejected/empty.\n */\nexport function MediaFigure({\n image,\n schema,\n mediaBaseUrl,\n transform,\n className,\n captionClassName,\n loading = \"lazy\",\n decoding = \"async\",\n}: {\n image: MediaFieldValue | null | undefined;\n schema?: MetadataFieldDef[];\n mediaBaseUrl?: string;\n transform?: ImageTransformParams;\n className?: string;\n captionClassName?: string;\n /** Defaults to \"lazy\"; pass \"eager\" for an above-the-fold figure. */\n loading?: ImgHTMLAttributes<HTMLImageElement>[\"loading\"];\n decoding?: ImgHTMLAttributes<HTMLImageElement>[\"decoding\"];\n}): ReactElement | null {\n const props = getMediaProps(image, { mediaBaseUrl, transform });\n if (!props.src) return null;\n\n const fields = collectFigureFields(image, schema);\n\n return (\n <figure className={className}>\n <img\n src={props.src}\n alt={props.alt}\n loading={loading}\n decoding={decoding}\n {...(props.width !== undefined ? { width: props.width } : {})}\n {...(props.height !== undefined ? { height: props.height } : {})}\n />\n {fields.length > 0 && (\n <figcaption className={captionClassName}>\n {fields.map((f) => (\n <span key={f.name} data-field={f.name}>\n {f.value}\n </span>\n ))}\n </figcaption>\n )}\n </figure>\n );\n}\n","// Ready-made Puck component for the rich `p1-media` field. Server-safe (no\n// hooks, no context) so it can be registered in configs that render via RSC.\nimport type { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport type { ComponentConfig, Field } from \"@puckeditor/core\";\nimport { getMediaProps, MediaFigure } from \"./render\";\nimport type { ImageTransformParams } from \"./utils\";\nimport type { MediaFieldValue, MetadataFieldDef } from \"./types\";\n\nexport interface MediaFigureBlockProps {\n photo: MediaFieldValue | null;\n loading?: \"lazy\" | \"eager\";\n}\n\nexport interface MediaFigureBlockOptions {\n /**\n * The CDN image origin used to validate value URLs — NOT the Worker API\n * URL. See GetMediaPropsOptions. Defaults to the production origin\n * (\"https://media.p1.pantheon.io\"); override for sandbox/staging/local dev.\n */\n mediaBaseUrl?: string;\n /**\n * Render-time transform. Include BOTH width and height — the editor's crop\n * intent only changes the output when there is a target aspect ratio.\n * Defaults to `{ width: 1200, height: 630, format: \"auto\" }`.\n */\n transform?: ImageTransformParams;\n /** Component label in the Puck sidebar. Defaults to \"Media Figure\". */\n label?: string;\n /** Label of the media field. Defaults to \"Photo\". */\n fieldLabel?: string;\n /** Metadata schema passed to MediaFigure — pins figcaption field order/labels. */\n schema?: MetadataFieldDef[];\n className?: string;\n captionClassName?: string;\n /** Rendered when no photo is chosen (or its URL fails origin validation). */\n placeholder?: ReactNode;\n /**\n * Default for the block's \"Loading\" field. \"lazy\" (the default) keeps\n * off-screen images off the wire on image-heavy pages; use \"eager\" when the\n * block is reliably above the fold.\n */\n defaultLoading?: \"lazy\" | \"eager\";\n}\n\nconst DEFAULT_TRANSFORM: ImageTransformParams = { width: 1200, height: 630, format: \"auto\" };\n\n/** Same production default as createMediaPlugin's workerUrl — same host serves both. */\nconst DEFAULT_MEDIA_BASE_URL = \"https://media.p1.pantheon.io\";\n\nconst placeholderStyle: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"200px\",\n borderRadius: \"8px\",\n backgroundColor: \"#e5e5e5\",\n color: \"#525252\",\n fontSize: \"14px\",\n};\n\n/**\n * Builds a registerable Puck component around the rich `p1-media` field and\n * `MediaFigure`. Spares consumers the custom-field-type cast and the\n * width+height transform pitfall:\n *\n * ```tsx\n * const config = {\n * components: {\n * MediaFigureBlock: createMediaFigureBlock({ mediaBaseUrl: MEDIA_BASE }),\n * },\n * };\n * ```\n */\nexport function createMediaFigureBlock(\n options: MediaFigureBlockOptions,\n): ComponentConfig<MediaFigureBlockProps> {\n const {\n mediaBaseUrl = DEFAULT_MEDIA_BASE_URL,\n transform = DEFAULT_TRANSFORM,\n label = \"Media Figure\",\n fieldLabel = \"Photo\",\n schema,\n className,\n captionClassName,\n placeholder = \"Choose a photo from the media library\",\n defaultLoading = \"lazy\",\n } = options;\n\n return {\n label,\n fields: {\n // `p1-media` is registered by createMediaPlugin via overrides.fieldTypes,\n // so it is not part of Puck's built-in Field union.\n photo: { type: \"p1-media\", label: fieldLabel } as unknown as Field,\n loading: {\n type: \"radio\",\n label: \"Loading\",\n options: [\n { label: \"Lazy\", value: \"lazy\" },\n { label: \"Eager\", value: \"eager\" },\n ],\n },\n },\n defaultProps: {\n photo: null,\n loading: defaultLoading,\n },\n render: ({ photo, loading }: MediaFigureBlockProps): ReactElement => {\n const { src } = getMediaProps(photo ?? null, { mediaBaseUrl });\n if (!src) {\n return <div style={placeholderStyle}>{placeholder}</div>;\n }\n return (\n <MediaFigure\n image={photo ?? null}\n mediaBaseUrl={mediaBaseUrl}\n transform={transform}\n schema={schema}\n className={className}\n captionClassName={captionClassName}\n loading={loading === \"eager\" ? \"eager\" : \"lazy\"}\n />\n );\n },\n };\n}\n"],"mappings":"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","names":["import_jsx_runtime"]}
package/dist/server.mjs CHANGED
@@ -87,6 +87,8 @@ function MediaImage({
87
87
  mediaBaseUrl,
88
88
  transform,
89
89
  alt,
90
+ loading = "lazy",
91
+ decoding = "async",
90
92
  ...rest
91
93
  }) {
92
94
  const props = getMediaProps(image, { mediaBaseUrl, transform });
@@ -96,6 +98,8 @@ function MediaImage({
96
98
  {
97
99
  src: props.src,
98
100
  alt: alt ?? props.alt,
101
+ loading,
102
+ decoding,
99
103
  ...props.width !== void 0 ? { width: props.width } : {},
100
104
  ...props.height !== void 0 ? { height: props.height } : {},
101
105
  ...rest
@@ -128,7 +132,9 @@ function MediaFigure({
128
132
  mediaBaseUrl,
129
133
  transform,
130
134
  className,
131
- captionClassName
135
+ captionClassName,
136
+ loading = "lazy",
137
+ decoding = "async"
132
138
  }) {
133
139
  const props = getMediaProps(image, { mediaBaseUrl, transform });
134
140
  if (!props.src) return null;
@@ -139,6 +145,8 @@ function MediaFigure({
139
145
  {
140
146
  src: props.src,
141
147
  alt: props.alt,
148
+ loading,
149
+ decoding,
142
150
  ...props.width !== void 0 ? { width: props.width } : {},
143
151
  ...props.height !== void 0 ? { height: props.height } : {}
144
152
  }
@@ -170,19 +178,29 @@ function createMediaFigureBlock(options) {
170
178
  schema,
171
179
  className,
172
180
  captionClassName,
173
- placeholder = "Choose a photo from the media library"
181
+ placeholder = "Choose a photo from the media library",
182
+ defaultLoading = "lazy"
174
183
  } = options;
175
184
  return {
176
185
  label,
177
186
  fields: {
178
187
  // `p1-media` is registered by createMediaPlugin via overrides.fieldTypes,
179
188
  // so it is not part of Puck's built-in Field union.
180
- photo: { type: "p1-media", label: fieldLabel }
189
+ photo: { type: "p1-media", label: fieldLabel },
190
+ loading: {
191
+ type: "radio",
192
+ label: "Loading",
193
+ options: [
194
+ { label: "Lazy", value: "lazy" },
195
+ { label: "Eager", value: "eager" }
196
+ ]
197
+ }
181
198
  },
182
199
  defaultProps: {
183
- photo: null
200
+ photo: null,
201
+ loading: defaultLoading
184
202
  },
185
- render: ({ photo }) => {
203
+ render: ({ photo, loading }) => {
186
204
  const { src } = getMediaProps(photo ?? null, { mediaBaseUrl });
187
205
  if (!src) {
188
206
  return /* @__PURE__ */ jsx2("div", { style: placeholderStyle, children: placeholder });
@@ -195,7 +213,8 @@ function createMediaFigureBlock(options) {
195
213
  transform,
196
214
  schema,
197
215
  className,
198
- captionClassName
216
+ captionClassName,
217
+ loading: loading === "eager" ? "eager" : "lazy"
199
218
  }
200
219
  );
201
220
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils.ts","../src/media-value.ts","../src/render.tsx","../src/media-figure-block.tsx"],"sourcesContent":["export interface ImageTransformParams {\n width?: number;\n height?: number;\n /** `auto` negotiates avif → webp → jpeg from the request's Accept header. */\n format?: \"auto\" | \"webp\" | \"jpeg\" | \"png\" | \"gif\" | \"avif\";\n quality?: number;\n}\n\n/**\n * Merges Cloudflare Images transform params onto a CDN image URL.\n * Preserves any existing params in the URL — e.g. the editor's crop intent\n * (`fit=cover&gravity=auto` for smart crop, `trim.*` for a manual crop).\n *\n * @example\n * buildImageUrl(data.heroImage, { width: 1200, height: 630, format: \"webp\" })\n */\nexport function buildImageUrl(url: string, params: ImageTransformParams): string {\n if (!url) return url;\n const parsed = new URL(url);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return url;\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined) {\n parsed.searchParams.set(k, String(v));\n }\n }\n return parsed.toString();\n}\n","import type { MediaFieldValue, MediaValue } from \"./types\";\nimport {\n buildValueWithCrop,\n buildValueWithTrim,\n getBaseUrl,\n type CropMode,\n type TrimRect,\n} from \"./crop\";\n\n/**\n * Structural keys on a MediaValue that are not schema-driven metadata. Used to\n * avoid clobbering identity fields when copying metadata defaults into a value.\n */\nexport const STRUCTURAL_MEDIA_KEYS = new Set([\n \"assetId\",\n \"versionId\",\n \"url\",\n \"metaSchemaVersion\",\n]);\n\n/** Narrows a MediaFieldValue to the rich object form. */\nexport function isMediaValue(\n value: MediaFieldValue | null | undefined,\n): value is MediaValue {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Builds the value the `p1-media` field writes on asset selection.\n *\n * R10 invariant: never synthesize a MediaValue unless BOTH `assetId` and\n * `versionId` are present — otherwise fall back to a bare URL string (basic\n * mode). A value with undefined identity is unfindable by \"update usages\" and\n * can render `src=undefined`, so this fallback is load-bearing, not cosmetic.\n * It also means picking from a pre-upgrade Worker (bare-array `GET /media`, no\n * assetId) yields a string, so the plugin degrades cleanly before the cutover.\n */\nexport function makeMediaValue(input: {\n assetId?: string | null;\n versionId?: string | null;\n url: string;\n metaSchemaVersion?: number;\n metadata?: Record<string, string | number | undefined>;\n}): MediaFieldValue {\n if (!input.assetId || !input.versionId) {\n return input.url; // R10 fallback: string value\n }\n const value: MediaValue = {\n assetId: input.assetId,\n versionId: input.versionId,\n url: input.url,\n };\n if (typeof input.metaSchemaVersion === \"number\") {\n value.metaSchemaVersion = input.metaSchemaVersion;\n }\n if (input.metadata) {\n for (const [k, v] of Object.entries(input.metadata)) {\n if (v === undefined || v === \"\") continue;\n if (STRUCTURAL_MEDIA_KEYS.has(k)) continue; // never let metadata clobber identity\n value[k] = v;\n }\n }\n return value;\n}\n\n/** The asset fields the field editor needs to build a value on selection. */\nexport interface SelectableAsset {\n assetId?: string | null;\n versionId?: string | null;\n url: string;\n width?: number;\n height?: number;\n metadata?: Record<string, string>;\n metaSchemaVersion?: number;\n}\n\n/**\n * Builds the value written when an asset is picked from the library, applying\n * the current crop and copying metadata (flat, incl. alt) plus captured\n * dimensions. Delegates to makeMediaValue, so R10 still holds (a pre-cutover\n * asset with no identity yields a string).\n */\nexport function buildValueFromAsset(\n asset: SelectableAsset,\n crop: CropMode,\n): MediaFieldValue {\n const metadata: Record<string, string | number | undefined> = { ...(asset.metadata ?? {}) };\n if (typeof asset.width === \"number\") metadata.width = asset.width;\n if (typeof asset.height === \"number\") metadata.height = asset.height;\n return makeMediaValue({\n assetId: asset.assetId,\n versionId: asset.versionId,\n url: buildValueWithCrop(getBaseUrl(asset.url), crop),\n metaSchemaVersion: asset.metaSchemaVersion,\n metadata,\n });\n}\n\n/**\n * Applies a crop mode to a field value. R10: a string stays a string (basic\n * mode) — crop never promotes a value to a partial-identity object.\n */\nexport function applyCropToValue(\n value: MediaFieldValue,\n crop: CropMode,\n): MediaFieldValue {\n const currentUrl = isMediaValue(value) ? value.url : value;\n const baseUrl = getBaseUrl(typeof currentUrl === \"string\" ? currentUrl : \"\");\n if (!baseUrl) return value;\n const url = buildValueWithCrop(baseUrl, crop);\n return isMediaValue(value) ? { ...value, url } : url;\n}\n\n/**\n * Applies a manual crop region (the interactive cropper's result) to a field\n * value. Same R10 shape as applyCropToValue: a string stays a string, an\n * object keeps its identity and only swaps `url`.\n */\nexport function applyTrimToValue(\n value: MediaFieldValue,\n rect: TrimRect,\n): MediaFieldValue {\n const currentUrl = isMediaValue(value) ? value.url : value;\n const baseUrl = getBaseUrl(typeof currentUrl === \"string\" ? currentUrl : \"\");\n if (!baseUrl) return value;\n const url = buildValueWithTrim(baseUrl, rect);\n return isMediaValue(value) ? { ...value, url } : url;\n}\n\n/**\n * Sets one metadata field on a value. R10: refuses to operate on a string\n * (returns it unchanged — metadata can't be attached until an asset is picked)\n * and never lets a structural key clobber the pinned identity.\n */\nexport function setMetaOnValue(\n value: MediaFieldValue,\n fieldName: string,\n fieldValue: string,\n): MediaFieldValue {\n if (!isMediaValue(value)) return value;\n if (STRUCTURAL_MEDIA_KEYS.has(fieldName)) return value;\n return { ...value, [fieldName]: fieldValue };\n}\n","// Render-side helpers. Server-safe (no React context, no client-only hooks) so\n// they can be imported from RSC via the package's \"react-server\" export.\nimport type { ImgHTMLAttributes, ReactElement } from \"react\";\nimport type {\n MediaFieldValue,\n MediaProps,\n MetadataFieldDef,\n} from \"./types\";\nimport { isMediaValue, STRUCTURAL_MEDIA_KEYS } from \"./media-value\";\nimport { buildImageUrl, type ImageTransformParams } from \"./utils\";\n\nexport interface GetMediaPropsOptions {\n /**\n * The CDN image origin that serves media, e.g.\n * \"https://staging.media.p1.pantheon.io\". This is the public image host —\n * NOT the Worker API URL (`…workers.dev`), whose origin would reject every\n * real image URL.\n *\n * Security (required): a value's `url` is untrusted document content — anyone\n * who can edit a document, or call the CCR `/edits` API, controls it. Without\n * an origin check a crafted `https://evil.example/beacon.png` turns every\n * published render into a visitor-IP exfil beacon (and an SSRF under\n * server-fetching `next/image`). getMediaProps therefore rejects any url that\n * is not `https` on this exact origin. If `mediaBaseUrl` is omitted it\n * **fails closed** (empty src) rather than degrading to an insecure pass-through.\n *\n * Local dev exception: when the configured base is itself `http` on a\n * loopback host (`localhost`, `127.0.0.1`, `[::1]` — a local `wrangler dev`\n * worker), same-origin `http` urls are allowed so rich values render locally.\n */\n mediaBaseUrl?: string;\n /** Transform params merged onto the validated URL (width, height, format, quality). */\n transform?: ImageTransformParams;\n}\n\n// Hosts where an http mediaBaseUrl is honored (local `wrangler dev`). Exact\n// hostnames only — no *.localhost subdomains.\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n\n/**\n * Returns the url only if it is on the configured CDN origin AND https —\n * except that an http origin is honored when the configured mediaBaseUrl is\n * itself http on a loopback host (local dev against `wrangler dev`, which\n * serves plain http). Production is unaffected: real CDN bases are https, so\n * every http url still fails the origin check. Otherwise \"\". Fails closed\n * when no origin is configured (see GetMediaPropsOptions).\n */\nfunction validateSrc(url: string, mediaBaseUrl?: string): string {\n if (!url || !mediaBaseUrl) return \"\";\n let parsed: URL;\n let base: URL;\n try {\n parsed = new URL(url);\n base = new URL(mediaBaseUrl);\n } catch {\n return \"\";\n }\n if (parsed.origin !== base.origin) return \"\";\n if (parsed.protocol !== \"https:\") {\n // Non-https is allowed only as plain http against an explicitly-configured\n // local-dev loopback base. Checking parsed.protocol too keeps schemes whose\n // origin serializes to the base origin (blob:http://…) out of the carve-out.\n const isLocalDevBase = base.protocol === \"http:\" && LOOPBACK_HOSTS.has(base.hostname);\n if (!isLocalDevBase || parsed.protocol !== \"http:\") return \"\";\n }\n return url;\n}\n\n/**\n * Normalizes a `string | MediaValue | null` into `{ src, alt, width?, height? }`.\n * A string (basic mode) yields `alt: \"\"`. `src` is validated against the CDN\n * origin (see GetMediaPropsOptions) and empty on rejection.\n */\nexport function getMediaProps(\n value: MediaFieldValue | null | undefined,\n options?: GetMediaPropsOptions,\n): MediaProps {\n const mediaBaseUrl = options?.mediaBaseUrl;\n const transform = options?.transform;\n\n const finalize = (rawUrl: string): string => {\n const validated = validateSrc(rawUrl, mediaBaseUrl);\n return validated && transform ? buildImageUrl(validated, transform) : validated;\n };\n\n if (value == null) return { src: \"\", alt: \"\" };\n\n if (typeof value === \"string\") {\n return { src: finalize(value), alt: \"\" };\n }\n\n const props: MediaProps = {\n src: finalize(typeof value.url === \"string\" ? value.url : \"\"),\n alt: typeof value.alt === \"string\" ? value.alt : \"\",\n };\n if (typeof value.width === \"number\") props.width = value.width;\n if (typeof value.height === \"number\") props.height = value.height;\n return props;\n}\n\n/**\n * Common-case `<img>` wrapper. Renders nothing when the src is rejected/empty\n * (a broken foreign-origin image is never emitted). `alt` may be overridden;\n * otherwise it comes from the value.\n */\nexport function MediaImage({\n image,\n mediaBaseUrl,\n transform,\n alt,\n ...rest\n}: {\n image: MediaFieldValue | null | undefined;\n mediaBaseUrl?: string;\n transform?: ImageTransformParams;\n} & Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\">): ReactElement | null {\n const props = getMediaProps(image, { mediaBaseUrl, transform });\n if (!props.src) return null;\n return (\n <img\n src={props.src}\n alt={alt ?? props.alt}\n {...(props.width !== undefined ? { width: props.width } : {})}\n {...(props.height !== undefined ? { height: props.height } : {})}\n {...rest}\n />\n );\n}\n\n/**\n * Collects the metadata fields a figure should render, generically (req. R14):\n * with a schema, iterate its entries (canonical order/labels); without one,\n * iterate the value's own string metadata keys. Either way a new backend text\n * field appears with no plugin release. `alt` (on the `<img>`) and non-text\n * values (width/height/metaSchemaVersion) are excluded from the caption.\n */\nfunction collectFigureFields(\n image: MediaFieldValue | null | undefined,\n schema?: MetadataFieldDef[],\n): Array<{ name: string; label?: string; value: string }> {\n if (!isMediaValue(image)) return [];\n const out: Array<{ name: string; label?: string; value: string }> = [];\n if (schema && schema.length > 0) {\n for (const entry of schema) {\n if (entry.name === \"alt\") continue;\n const raw = image[entry.name];\n if (raw === undefined || raw === \"\") continue;\n out.push({ name: entry.name, label: entry.label, value: String(raw) });\n }\n } else {\n for (const [k, v] of Object.entries(image)) {\n if (k === \"alt\") continue;\n if (STRUCTURAL_MEDIA_KEYS.has(k)) continue;\n if (typeof v !== \"string\" || v === \"\") continue;\n out.push({ name: k, value: v });\n }\n }\n return out;\n}\n\n/**\n * `<figure>` that renders the image plus the schema-advertised text fields as\n * ESCAPED text (React default escaping — never dangerouslySetInnerHTML, req.\n * R6). Renders nothing when the src is rejected/empty.\n */\nexport function MediaFigure({\n image,\n schema,\n mediaBaseUrl,\n transform,\n className,\n captionClassName,\n}: {\n image: MediaFieldValue | null | undefined;\n schema?: MetadataFieldDef[];\n mediaBaseUrl?: string;\n transform?: ImageTransformParams;\n className?: string;\n captionClassName?: string;\n}): ReactElement | null {\n const props = getMediaProps(image, { mediaBaseUrl, transform });\n if (!props.src) return null;\n\n const fields = collectFigureFields(image, schema);\n\n return (\n <figure className={className}>\n <img\n src={props.src}\n alt={props.alt}\n {...(props.width !== undefined ? { width: props.width } : {})}\n {...(props.height !== undefined ? { height: props.height } : {})}\n />\n {fields.length > 0 && (\n <figcaption className={captionClassName}>\n {fields.map((f) => (\n <span key={f.name} data-field={f.name}>\n {f.value}\n </span>\n ))}\n </figcaption>\n )}\n </figure>\n );\n}\n","// Ready-made Puck component for the rich `p1-media` field. Server-safe (no\n// hooks, no context) so it can be registered in configs that render via RSC.\nimport type { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport type { ComponentConfig, Field } from \"@puckeditor/core\";\nimport { getMediaProps, MediaFigure } from \"./render\";\nimport type { ImageTransformParams } from \"./utils\";\nimport type { MediaFieldValue, MetadataFieldDef } from \"./types\";\n\nexport interface MediaFigureBlockProps {\n photo: MediaFieldValue | null;\n}\n\nexport interface MediaFigureBlockOptions {\n /**\n * The CDN image origin used to validate value URLs — NOT the Worker API\n * URL. See GetMediaPropsOptions. Defaults to the production origin\n * (\"https://media.p1.pantheon.io\"); override for sandbox/staging/local dev.\n */\n mediaBaseUrl?: string;\n /**\n * Render-time transform. Include BOTH width and height — the editor's crop\n * intent only changes the output when there is a target aspect ratio.\n * Defaults to `{ width: 1200, height: 630, format: \"auto\" }`.\n */\n transform?: ImageTransformParams;\n /** Component label in the Puck sidebar. Defaults to \"Media Figure\". */\n label?: string;\n /** Label of the media field. Defaults to \"Photo\". */\n fieldLabel?: string;\n /** Metadata schema passed to MediaFigure — pins figcaption field order/labels. */\n schema?: MetadataFieldDef[];\n className?: string;\n captionClassName?: string;\n /** Rendered when no photo is chosen (or its URL fails origin validation). */\n placeholder?: ReactNode;\n}\n\nconst DEFAULT_TRANSFORM: ImageTransformParams = { width: 1200, height: 630, format: \"auto\" };\n\n/** Same production default as createMediaPlugin's workerUrl — same host serves both. */\nconst DEFAULT_MEDIA_BASE_URL = \"https://media.p1.pantheon.io\";\n\nconst placeholderStyle: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"200px\",\n borderRadius: \"8px\",\n backgroundColor: \"#e5e5e5\",\n color: \"#525252\",\n fontSize: \"14px\",\n};\n\n/**\n * Builds a registerable Puck component around the rich `p1-media` field and\n * `MediaFigure`. Spares consumers the custom-field-type cast and the\n * width+height transform pitfall:\n *\n * ```tsx\n * const config = {\n * components: {\n * MediaFigureBlock: createMediaFigureBlock({ mediaBaseUrl: MEDIA_BASE }),\n * },\n * };\n * ```\n */\nexport function createMediaFigureBlock(\n options: MediaFigureBlockOptions,\n): ComponentConfig<MediaFigureBlockProps> {\n const {\n mediaBaseUrl = DEFAULT_MEDIA_BASE_URL,\n transform = DEFAULT_TRANSFORM,\n label = \"Media Figure\",\n fieldLabel = \"Photo\",\n schema,\n className,\n captionClassName,\n placeholder = \"Choose a photo from the media library\",\n } = options;\n\n return {\n label,\n fields: {\n // `p1-media` is registered by createMediaPlugin via overrides.fieldTypes,\n // so it is not part of Puck's built-in Field union.\n photo: { type: \"p1-media\", label: fieldLabel } as unknown as Field,\n },\n defaultProps: {\n photo: null,\n },\n render: ({ photo }: MediaFigureBlockProps): ReactElement => {\n const { src } = getMediaProps(photo ?? null, { mediaBaseUrl });\n if (!src) {\n return <div style={placeholderStyle}>{placeholder}</div>;\n }\n return (\n <MediaFigure\n image={photo ?? null}\n mediaBaseUrl={mediaBaseUrl}\n transform={transform}\n schema={schema}\n className={className}\n captionClassName={captionClassName}\n />\n );\n },\n };\n}\n"],"mappings":";AAgBO,SAAS,cAAc,KAAa,QAAsC;AAC/E,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,MAAI,OAAO,aAAa,YAAY,OAAO,aAAa,QAAS,QAAO;AACxE,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,QAAI,MAAM,QAAW;AACnB,aAAO,aAAa,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,IACtC;AAAA,EACF;AACA,SAAO,OAAO,SAAS;AACzB;;;ACbO,IAAM,wBAAwB,oBAAI,IAAI;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,SAAS,aACd,OACqB;AACrB,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAYO,SAAS,eAAe,OAMX;AAClB,MAAI,CAAC,MAAM,WAAW,CAAC,MAAM,WAAW;AACtC,WAAO,MAAM;AAAA,EACf;AACA,QAAM,QAAoB;AAAA,IACxB,SAAS,MAAM;AAAA,IACf,WAAW,MAAM;AAAA,IACjB,KAAK,MAAM;AAAA,EACb;AACA,MAAI,OAAO,MAAM,sBAAsB,UAAU;AAC/C,UAAM,oBAAoB,MAAM;AAAA,EAClC;AACA,MAAI,MAAM,UAAU;AAClB,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,QAAQ,GAAG;AACnD,UAAI,MAAM,UAAa,MAAM,GAAI;AACjC,UAAI,sBAAsB,IAAI,CAAC,EAAG;AAClC,YAAM,CAAC,IAAI;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;;;ACwDI,cAmEA,YAnEA;AAlFJ,IAAM,iBAAiB,oBAAI,IAAI,CAAC,aAAa,aAAa,OAAO,CAAC;AAUlE,SAAS,YAAY,KAAa,cAA+B;AAC/D,MAAI,CAAC,OAAO,CAAC,aAAc,QAAO;AAClC,MAAI;AACJ,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AACpB,WAAO,IAAI,IAAI,YAAY;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,WAAW,KAAK,OAAQ,QAAO;AAC1C,MAAI,OAAO,aAAa,UAAU;AAIhC,UAAM,iBAAiB,KAAK,aAAa,WAAW,eAAe,IAAI,KAAK,QAAQ;AACpF,QAAI,CAAC,kBAAkB,OAAO,aAAa,QAAS,QAAO;AAAA,EAC7D;AACA,SAAO;AACT;AAOO,SAAS,cACd,OACA,SACY;AACZ,QAAM,eAAe,SAAS;AAC9B,QAAM,YAAY,SAAS;AAE3B,QAAM,WAAW,CAAC,WAA2B;AAC3C,UAAM,YAAY,YAAY,QAAQ,YAAY;AAClD,WAAO,aAAa,YAAY,cAAc,WAAW,SAAS,IAAI;AAAA,EACxE;AAEA,MAAI,SAAS,KAAM,QAAO,EAAE,KAAK,IAAI,KAAK,GAAG;AAE7C,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,KAAK,SAAS,KAAK,GAAG,KAAK,GAAG;AAAA,EACzC;AAEA,QAAM,QAAoB;AAAA,IACxB,KAAK,SAAS,OAAO,MAAM,QAAQ,WAAW,MAAM,MAAM,EAAE;AAAA,IAC5D,KAAK,OAAO,MAAM,QAAQ,WAAW,MAAM,MAAM;AAAA,EACnD;AACA,MAAI,OAAO,MAAM,UAAU,SAAU,OAAM,QAAQ,MAAM;AACzD,MAAI,OAAO,MAAM,WAAW,SAAU,OAAM,SAAS,MAAM;AAC3D,SAAO;AACT;AAOO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAI2E;AACzE,QAAM,QAAQ,cAAc,OAAO,EAAE,cAAc,UAAU,CAAC;AAC9D,MAAI,CAAC,MAAM,IAAK,QAAO;AACvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,MAAM;AAAA,MACX,KAAK,OAAO,MAAM;AAAA,MACjB,GAAI,MAAM,UAAU,SAAY,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;AAAA,MAC1D,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;AAAA,MAC7D,GAAG;AAAA;AAAA,EACN;AAEJ;AASA,SAAS,oBACP,OACA,QACwD;AACxD,MAAI,CAAC,aAAa,KAAK,EAAG,QAAO,CAAC;AAClC,QAAM,MAA8D,CAAC;AACrE,MAAI,UAAU,OAAO,SAAS,GAAG;AAC/B,eAAW,SAAS,QAAQ;AAC1B,UAAI,MAAM,SAAS,MAAO;AAC1B,YAAM,MAAM,MAAM,MAAM,IAAI;AAC5B,UAAI,QAAQ,UAAa,QAAQ,GAAI;AACrC,UAAI,KAAK,EAAE,MAAM,MAAM,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,GAAG,EAAE,CAAC;AAAA,IACvE;AAAA,EACF,OAAO;AACL,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC1C,UAAI,MAAM,MAAO;AACjB,UAAI,sBAAsB,IAAI,CAAC,EAAG;AAClC,UAAI,OAAO,MAAM,YAAY,MAAM,GAAI;AACvC,UAAI,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;AAAA,IAChC;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOwB;AACtB,QAAM,QAAQ,cAAc,OAAO,EAAE,cAAc,UAAU,CAAC;AAC9D,MAAI,CAAC,MAAM,IAAK,QAAO;AAEvB,QAAM,SAAS,oBAAoB,OAAO,MAAM;AAEhD,SACE,qBAAC,YAAO,WACN;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,MAAM;AAAA,QACX,KAAK,MAAM;AAAA,QACV,GAAI,MAAM,UAAU,SAAY,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;AAAA,QAC1D,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;AAAA;AAAA,IAChE;AAAA,IACC,OAAO,SAAS,KACf,oBAAC,gBAAW,WAAW,kBACpB,iBAAO,IAAI,CAAC,MACX,oBAAC,UAAkB,cAAY,EAAE,MAC9B,YAAE,SADM,EAAE,IAEb,CACD,GACH;AAAA,KAEJ;AAEJ;;;AC/Ge,gBAAAA,YAAA;AAxDf,IAAM,oBAA0C,EAAE,OAAO,MAAM,QAAQ,KAAK,QAAQ,OAAO;AAG3F,IAAM,yBAAyB;AAE/B,IAAM,mBAAkC;AAAA,EACtC,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,WAAW;AAAA,EACX,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,OAAO;AAAA,EACP,UAAU;AACZ;AAeO,SAAS,uBACd,SACwC;AACxC,QAAM;AAAA,IACJ,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,EAChB,IAAI;AAEJ,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA;AAAA;AAAA,MAGN,OAAO,EAAE,MAAM,YAAY,OAAO,WAAW;AAAA,IAC/C;AAAA,IACA,cAAc;AAAA,MACZ,OAAO;AAAA,IACT;AAAA,IACA,QAAQ,CAAC,EAAE,MAAM,MAA2C;AAC1D,YAAM,EAAE,IAAI,IAAI,cAAc,SAAS,MAAM,EAAE,aAAa,CAAC;AAC7D,UAAI,CAAC,KAAK;AACR,eAAO,gBAAAA,KAAC,SAAI,OAAO,kBAAmB,uBAAY;AAAA,MACpD;AACA,aACE,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,SAAS;AAAA,UAChB;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA;AAAA,MACF;AAAA,IAEJ;AAAA,EACF;AACF;","names":["jsx"]}
1
+ {"version":3,"sources":["../src/utils.ts","../src/media-value.ts","../src/render.tsx","../src/media-figure-block.tsx"],"sourcesContent":["export interface ImageTransformParams {\n width?: number;\n height?: number;\n /** `auto` negotiates avif → webp → jpeg from the request's Accept header. */\n format?: \"auto\" | \"webp\" | \"jpeg\" | \"png\" | \"gif\" | \"avif\";\n quality?: number;\n}\n\n/**\n * Merges Cloudflare Images transform params onto a CDN image URL.\n * Preserves any existing params in the URL — e.g. the editor's crop intent\n * (`fit=cover&gravity=auto` for smart crop, `trim.*` for a manual crop).\n *\n * @example\n * buildImageUrl(data.heroImage, { width: 1200, height: 630, format: \"webp\" })\n */\nexport function buildImageUrl(url: string, params: ImageTransformParams): string {\n if (!url) return url;\n const parsed = new URL(url);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return url;\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined) {\n parsed.searchParams.set(k, String(v));\n }\n }\n return parsed.toString();\n}\n","import type { MediaFieldValue, MediaValue } from \"./types\";\nimport {\n buildValueWithCrop,\n buildValueWithTrim,\n getBaseUrl,\n type CropMode,\n type TrimRect,\n} from \"./crop\";\n\n/**\n * Structural keys on a MediaValue that are not schema-driven metadata. Used to\n * avoid clobbering identity fields when copying metadata defaults into a value.\n */\nexport const STRUCTURAL_MEDIA_KEYS = new Set([\n \"assetId\",\n \"versionId\",\n \"url\",\n \"metaSchemaVersion\",\n]);\n\n/** Narrows a MediaFieldValue to the rich object form. */\nexport function isMediaValue(\n value: MediaFieldValue | null | undefined,\n): value is MediaValue {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Builds the value the `p1-media` field writes on asset selection.\n *\n * R10 invariant: never synthesize a MediaValue unless BOTH `assetId` and\n * `versionId` are present — otherwise fall back to a bare URL string (basic\n * mode). A value with undefined identity is unfindable by \"update usages\" and\n * can render `src=undefined`, so this fallback is load-bearing, not cosmetic.\n * It also means picking from a pre-upgrade Worker (bare-array `GET /media`, no\n * assetId) yields a string, so the plugin degrades cleanly before the cutover.\n */\nexport function makeMediaValue(input: {\n assetId?: string | null;\n versionId?: string | null;\n url: string;\n metaSchemaVersion?: number;\n metadata?: Record<string, string | number | undefined>;\n}): MediaFieldValue {\n if (!input.assetId || !input.versionId) {\n return input.url; // R10 fallback: string value\n }\n const value: MediaValue = {\n assetId: input.assetId,\n versionId: input.versionId,\n url: input.url,\n };\n if (typeof input.metaSchemaVersion === \"number\") {\n value.metaSchemaVersion = input.metaSchemaVersion;\n }\n if (input.metadata) {\n for (const [k, v] of Object.entries(input.metadata)) {\n if (v === undefined || v === \"\") continue;\n if (STRUCTURAL_MEDIA_KEYS.has(k)) continue; // never let metadata clobber identity\n value[k] = v;\n }\n }\n return value;\n}\n\n/** The asset fields the field editor needs to build a value on selection. */\nexport interface SelectableAsset {\n assetId?: string | null;\n versionId?: string | null;\n url: string;\n width?: number;\n height?: number;\n metadata?: Record<string, string>;\n metaSchemaVersion?: number;\n}\n\n/**\n * Builds the value written when an asset is picked from the library, applying\n * the current crop and copying metadata (flat, incl. alt) plus captured\n * dimensions. Delegates to makeMediaValue, so R10 still holds (a pre-cutover\n * asset with no identity yields a string).\n */\nexport function buildValueFromAsset(\n asset: SelectableAsset,\n crop: CropMode,\n): MediaFieldValue {\n const metadata: Record<string, string | number | undefined> = { ...(asset.metadata ?? {}) };\n if (typeof asset.width === \"number\") metadata.width = asset.width;\n if (typeof asset.height === \"number\") metadata.height = asset.height;\n return makeMediaValue({\n assetId: asset.assetId,\n versionId: asset.versionId,\n url: buildValueWithCrop(getBaseUrl(asset.url), crop),\n metaSchemaVersion: asset.metaSchemaVersion,\n metadata,\n });\n}\n\n/**\n * Applies a crop mode to a field value. R10: a string stays a string (basic\n * mode) — crop never promotes a value to a partial-identity object.\n */\nexport function applyCropToValue(\n value: MediaFieldValue,\n crop: CropMode,\n): MediaFieldValue {\n const currentUrl = isMediaValue(value) ? value.url : value;\n const baseUrl = getBaseUrl(typeof currentUrl === \"string\" ? currentUrl : \"\");\n if (!baseUrl) return value;\n const url = buildValueWithCrop(baseUrl, crop);\n return isMediaValue(value) ? { ...value, url } : url;\n}\n\n/**\n * Applies a manual crop region (the interactive cropper's result) to a field\n * value. Same R10 shape as applyCropToValue: a string stays a string, an\n * object keeps its identity and only swaps `url`.\n */\nexport function applyTrimToValue(\n value: MediaFieldValue,\n rect: TrimRect,\n): MediaFieldValue {\n const currentUrl = isMediaValue(value) ? value.url : value;\n const baseUrl = getBaseUrl(typeof currentUrl === \"string\" ? currentUrl : \"\");\n if (!baseUrl) return value;\n const url = buildValueWithTrim(baseUrl, rect);\n return isMediaValue(value) ? { ...value, url } : url;\n}\n\n/**\n * Sets one metadata field on a value. R10: refuses to operate on a string\n * (returns it unchanged — metadata can't be attached until an asset is picked)\n * and never lets a structural key clobber the pinned identity.\n */\nexport function setMetaOnValue(\n value: MediaFieldValue,\n fieldName: string,\n fieldValue: string,\n): MediaFieldValue {\n if (!isMediaValue(value)) return value;\n if (STRUCTURAL_MEDIA_KEYS.has(fieldName)) return value;\n return { ...value, [fieldName]: fieldValue };\n}\n","// Render-side helpers. Server-safe (no React context, no client-only hooks) so\n// they can be imported from RSC via the package's \"react-server\" export.\nimport type { ImgHTMLAttributes, ReactElement } from \"react\";\nimport type {\n MediaFieldValue,\n MediaProps,\n MetadataFieldDef,\n} from \"./types\";\nimport { isMediaValue, STRUCTURAL_MEDIA_KEYS } from \"./media-value\";\nimport { buildImageUrl, type ImageTransformParams } from \"./utils\";\n\nexport interface GetMediaPropsOptions {\n /**\n * The CDN image origin that serves media, e.g.\n * \"https://staging.media.p1.pantheon.io\". This is the public image host —\n * NOT the Worker API URL (`…workers.dev`), whose origin would reject every\n * real image URL.\n *\n * Security (required): a value's `url` is untrusted document content — anyone\n * who can edit a document, or call the CCR `/edits` API, controls it. Without\n * an origin check a crafted `https://evil.example/beacon.png` turns every\n * published render into a visitor-IP exfil beacon (and an SSRF under\n * server-fetching `next/image`). getMediaProps therefore rejects any url that\n * is not `https` on this exact origin. If `mediaBaseUrl` is omitted it\n * **fails closed** (empty src) rather than degrading to an insecure pass-through.\n *\n * Local dev exception: when the configured base is itself `http` on a\n * loopback host (`localhost`, `127.0.0.1`, `[::1]` — a local `wrangler dev`\n * worker), same-origin `http` urls are allowed so rich values render locally.\n */\n mediaBaseUrl?: string;\n /** Transform params merged onto the validated URL (width, height, format, quality). */\n transform?: ImageTransformParams;\n}\n\n// Hosts where an http mediaBaseUrl is honored (local `wrangler dev`). Exact\n// hostnames only — no *.localhost subdomains.\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n\n/**\n * Returns the url only if it is on the configured CDN origin AND https —\n * except that an http origin is honored when the configured mediaBaseUrl is\n * itself http on a loopback host (local dev against `wrangler dev`, which\n * serves plain http). Production is unaffected: real CDN bases are https, so\n * every http url still fails the origin check. Otherwise \"\". Fails closed\n * when no origin is configured (see GetMediaPropsOptions).\n */\nfunction validateSrc(url: string, mediaBaseUrl?: string): string {\n if (!url || !mediaBaseUrl) return \"\";\n let parsed: URL;\n let base: URL;\n try {\n parsed = new URL(url);\n base = new URL(mediaBaseUrl);\n } catch {\n return \"\";\n }\n if (parsed.origin !== base.origin) return \"\";\n if (parsed.protocol !== \"https:\") {\n // Non-https is allowed only as plain http against an explicitly-configured\n // local-dev loopback base. Checking parsed.protocol too keeps schemes whose\n // origin serializes to the base origin (blob:http://…) out of the carve-out.\n const isLocalDevBase = base.protocol === \"http:\" && LOOPBACK_HOSTS.has(base.hostname);\n if (!isLocalDevBase || parsed.protocol !== \"http:\") return \"\";\n }\n return url;\n}\n\n/**\n * Normalizes a `string | MediaValue | null` into `{ src, alt, width?, height? }`.\n * A string (basic mode) yields `alt: \"\"`. `src` is validated against the CDN\n * origin (see GetMediaPropsOptions) and empty on rejection.\n */\nexport function getMediaProps(\n value: MediaFieldValue | null | undefined,\n options?: GetMediaPropsOptions,\n): MediaProps {\n const mediaBaseUrl = options?.mediaBaseUrl;\n const transform = options?.transform;\n\n const finalize = (rawUrl: string): string => {\n const validated = validateSrc(rawUrl, mediaBaseUrl);\n return validated && transform ? buildImageUrl(validated, transform) : validated;\n };\n\n if (value == null) return { src: \"\", alt: \"\" };\n\n if (typeof value === \"string\") {\n return { src: finalize(value), alt: \"\" };\n }\n\n const props: MediaProps = {\n src: finalize(typeof value.url === \"string\" ? value.url : \"\"),\n alt: typeof value.alt === \"string\" ? value.alt : \"\",\n };\n if (typeof value.width === \"number\") props.width = value.width;\n if (typeof value.height === \"number\") props.height = value.height;\n return props;\n}\n\n/**\n * Common-case `<img>` wrapper. Renders nothing when the src is rejected/empty\n * (a broken foreign-origin image is never emitted). `alt` may be overridden;\n * otherwise it comes from the value.\n */\nexport function MediaImage({\n image,\n mediaBaseUrl,\n transform,\n alt,\n loading = \"lazy\",\n decoding = \"async\",\n ...rest\n}: {\n image: MediaFieldValue | null | undefined;\n mediaBaseUrl?: string;\n transform?: ImageTransformParams;\n} & Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\">): ReactElement | null {\n const props = getMediaProps(image, { mediaBaseUrl, transform });\n if (!props.src) return null;\n return (\n <img\n src={props.src}\n alt={alt ?? props.alt}\n loading={loading}\n decoding={decoding}\n {...(props.width !== undefined ? { width: props.width } : {})}\n {...(props.height !== undefined ? { height: props.height } : {})}\n {...rest}\n />\n );\n}\n\n/**\n * Collects the metadata fields a figure should render, generically (req. R14):\n * with a schema, iterate its entries (canonical order/labels); without one,\n * iterate the value's own string metadata keys. Either way a new backend text\n * field appears with no plugin release. `alt` (on the `<img>`) and non-text\n * values (width/height/metaSchemaVersion) are excluded from the caption.\n */\nfunction collectFigureFields(\n image: MediaFieldValue | null | undefined,\n schema?: MetadataFieldDef[],\n): { name: string; label?: string; value: string }[] {\n if (!isMediaValue(image)) return [];\n const out: { name: string; label?: string; value: string }[] = [];\n if (schema && schema.length > 0) {\n for (const entry of schema) {\n if (entry.name === \"alt\") continue;\n const raw = image[entry.name];\n if (raw === undefined || raw === \"\") continue;\n out.push({ name: entry.name, label: entry.label, value: String(raw) });\n }\n } else {\n for (const [k, v] of Object.entries(image)) {\n if (k === \"alt\") continue;\n if (STRUCTURAL_MEDIA_KEYS.has(k)) continue;\n if (typeof v !== \"string\" || v === \"\") continue;\n out.push({ name: k, value: v });\n }\n }\n return out;\n}\n\n/**\n * `<figure>` that renders the image plus the schema-advertised text fields as\n * ESCAPED text (React default escaping — never dangerouslySetInnerHTML, req.\n * R6). Renders nothing when the src is rejected/empty.\n */\nexport function MediaFigure({\n image,\n schema,\n mediaBaseUrl,\n transform,\n className,\n captionClassName,\n loading = \"lazy\",\n decoding = \"async\",\n}: {\n image: MediaFieldValue | null | undefined;\n schema?: MetadataFieldDef[];\n mediaBaseUrl?: string;\n transform?: ImageTransformParams;\n className?: string;\n captionClassName?: string;\n /** Defaults to \"lazy\"; pass \"eager\" for an above-the-fold figure. */\n loading?: ImgHTMLAttributes<HTMLImageElement>[\"loading\"];\n decoding?: ImgHTMLAttributes<HTMLImageElement>[\"decoding\"];\n}): ReactElement | null {\n const props = getMediaProps(image, { mediaBaseUrl, transform });\n if (!props.src) return null;\n\n const fields = collectFigureFields(image, schema);\n\n return (\n <figure className={className}>\n <img\n src={props.src}\n alt={props.alt}\n loading={loading}\n decoding={decoding}\n {...(props.width !== undefined ? { width: props.width } : {})}\n {...(props.height !== undefined ? { height: props.height } : {})}\n />\n {fields.length > 0 && (\n <figcaption className={captionClassName}>\n {fields.map((f) => (\n <span key={f.name} data-field={f.name}>\n {f.value}\n </span>\n ))}\n </figcaption>\n )}\n </figure>\n );\n}\n","// Ready-made Puck component for the rich `p1-media` field. Server-safe (no\n// hooks, no context) so it can be registered in configs that render via RSC.\nimport type { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport type { ComponentConfig, Field } from \"@puckeditor/core\";\nimport { getMediaProps, MediaFigure } from \"./render\";\nimport type { ImageTransformParams } from \"./utils\";\nimport type { MediaFieldValue, MetadataFieldDef } from \"./types\";\n\nexport interface MediaFigureBlockProps {\n photo: MediaFieldValue | null;\n loading?: \"lazy\" | \"eager\";\n}\n\nexport interface MediaFigureBlockOptions {\n /**\n * The CDN image origin used to validate value URLs — NOT the Worker API\n * URL. See GetMediaPropsOptions. Defaults to the production origin\n * (\"https://media.p1.pantheon.io\"); override for sandbox/staging/local dev.\n */\n mediaBaseUrl?: string;\n /**\n * Render-time transform. Include BOTH width and height — the editor's crop\n * intent only changes the output when there is a target aspect ratio.\n * Defaults to `{ width: 1200, height: 630, format: \"auto\" }`.\n */\n transform?: ImageTransformParams;\n /** Component label in the Puck sidebar. Defaults to \"Media Figure\". */\n label?: string;\n /** Label of the media field. Defaults to \"Photo\". */\n fieldLabel?: string;\n /** Metadata schema passed to MediaFigure — pins figcaption field order/labels. */\n schema?: MetadataFieldDef[];\n className?: string;\n captionClassName?: string;\n /** Rendered when no photo is chosen (or its URL fails origin validation). */\n placeholder?: ReactNode;\n /**\n * Default for the block's \"Loading\" field. \"lazy\" (the default) keeps\n * off-screen images off the wire on image-heavy pages; use \"eager\" when the\n * block is reliably above the fold.\n */\n defaultLoading?: \"lazy\" | \"eager\";\n}\n\nconst DEFAULT_TRANSFORM: ImageTransformParams = { width: 1200, height: 630, format: \"auto\" };\n\n/** Same production default as createMediaPlugin's workerUrl — same host serves both. */\nconst DEFAULT_MEDIA_BASE_URL = \"https://media.p1.pantheon.io\";\n\nconst placeholderStyle: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"200px\",\n borderRadius: \"8px\",\n backgroundColor: \"#e5e5e5\",\n color: \"#525252\",\n fontSize: \"14px\",\n};\n\n/**\n * Builds a registerable Puck component around the rich `p1-media` field and\n * `MediaFigure`. Spares consumers the custom-field-type cast and the\n * width+height transform pitfall:\n *\n * ```tsx\n * const config = {\n * components: {\n * MediaFigureBlock: createMediaFigureBlock({ mediaBaseUrl: MEDIA_BASE }),\n * },\n * };\n * ```\n */\nexport function createMediaFigureBlock(\n options: MediaFigureBlockOptions,\n): ComponentConfig<MediaFigureBlockProps> {\n const {\n mediaBaseUrl = DEFAULT_MEDIA_BASE_URL,\n transform = DEFAULT_TRANSFORM,\n label = \"Media Figure\",\n fieldLabel = \"Photo\",\n schema,\n className,\n captionClassName,\n placeholder = \"Choose a photo from the media library\",\n defaultLoading = \"lazy\",\n } = options;\n\n return {\n label,\n fields: {\n // `p1-media` is registered by createMediaPlugin via overrides.fieldTypes,\n // so it is not part of Puck's built-in Field union.\n photo: { type: \"p1-media\", label: fieldLabel } as unknown as Field,\n loading: {\n type: \"radio\",\n label: \"Loading\",\n options: [\n { label: \"Lazy\", value: \"lazy\" },\n { label: \"Eager\", value: \"eager\" },\n ],\n },\n },\n defaultProps: {\n photo: null,\n loading: defaultLoading,\n },\n render: ({ photo, loading }: MediaFigureBlockProps): ReactElement => {\n const { src } = getMediaProps(photo ?? null, { mediaBaseUrl });\n if (!src) {\n return <div style={placeholderStyle}>{placeholder}</div>;\n }\n return (\n <MediaFigure\n image={photo ?? null}\n mediaBaseUrl={mediaBaseUrl}\n transform={transform}\n schema={schema}\n className={className}\n captionClassName={captionClassName}\n loading={loading === \"eager\" ? \"eager\" : \"lazy\"}\n />\n );\n },\n };\n}\n"],"mappings":"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","names":["jsx"]}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pantheon-systems/p1-media",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Puck media library plugin backed by Cloudflare R2",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/pantheon-systems/p1-media-r2.git",
9
- "directory": "packages/plugin"
8
+ "url": "git+https://github.com/pantheon-systems/p1-platform.git",
9
+ "directory": "packages/p1-media-r2"
10
10
  },
11
11
  "publishConfig": {
12
12
  "access": "public",
@@ -36,24 +36,29 @@
36
36
  "files": [
37
37
  "dist"
38
38
  ],
39
- "scripts": {
40
- "build": "tsup",
41
- "test": "vitest run",
42
- "lint": "tsc --noEmit"
43
- },
44
39
  "peerDependencies": {
45
40
  "@pantheon-systems/puck-css": ">=0.4.0",
46
41
  "@puckeditor/core": ">=0.18",
47
42
  "react": ">=18"
48
43
  },
49
44
  "devDependencies": {
50
- "@pantheon-systems/puck-css": "^0.7.0",
51
45
  "@puckeditor/core": "^0.22.0",
52
46
  "@types/react": "^19.0.0",
47
+ "eslint": "^9.27.0",
53
48
  "react": "^19.0.0",
54
49
  "react-image-crop": "^11.1.2",
55
50
  "tsup": "^8.0.0",
56
51
  "typescript": "^5.7.0",
57
- "vitest": "^3.2.7"
52
+ "vitest": "^3.2.7",
53
+ "@pantheon-systems/eslint-config": "0.1.0",
54
+ "@pantheon-systems/puck-css": "0.11.0"
55
+ },
56
+ "homepage": "https://github.com/pantheon-systems/p1-platform/tree/main/packages/p1-media-r2",
57
+ "scripts": {
58
+ "build": "tsup",
59
+ "lint": "eslint src",
60
+ "lint:fix": "eslint src --fix",
61
+ "test": "vitest run",
62
+ "typecheck": "tsc --noEmit"
58
63
  }
59
- }
64
+ }