@delmaredigital/payload-puck 0.6.30 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,7 +32,8 @@ pnpm add @delmaredigital/payload-puck @puckeditor/core
32
32
 
33
33
  | Dependency | Version |
34
34
  |------------|---------|
35
- | `@puckeditor/core` | >= 0.21.0 |
35
+ | `node` | >= 20.9.0 |
36
+ | `@puckeditor/core` | >= 0.23.0 |
36
37
  | `payload` | >= 3.69.0 |
37
38
  | `@payloadcms/next` | >= 3.69.0 |
38
39
  | `next` | >= 15.4.8 (see security note below) |
@@ -42,6 +43,56 @@ pnpm add @delmaredigital/payload-puck @puckeditor/core
42
43
 
43
44
  > **Security:** If your app uses Next.js middleware (or proxy.ts) to protect dynamic routes, use `next` >= 15.5.16 / 16.2.5 to pick up the fix for [CVE-2026-44574](https://github.com/vercel/next.js/security/advisories/GHSA-492v-c6pp-mqqv) (middleware bypass via dynamic route parameter injection). Turbopack users need >= 15.5.18 / 16.2.6.
44
45
 
46
+ ### Upgrading to 0.8.0 (breaking)
47
+
48
+ **Editor CSS is now built by your app, not by this plugin.** Three options collapse into one, and `withPuckCSS` is gone.
49
+
50
+ Add a build step using Tailwind's own CLI:
51
+
52
+ ```jsonc
53
+ // package.json
54
+ "scripts": {
55
+ "build:puck-css": "tailwindcss -i ./src/app/(frontend)/globals.css -o ./public/puck-editor-styles.css",
56
+ "build": "pnpm build:puck-css && next build",
57
+ "dev": "pnpm build:puck-css --watch & next dev"
58
+ }
59
+ ```
60
+
61
+ Then pass the URL:
62
+
63
+ ```typescript
64
+ // before
65
+ createPuckPlugin({
66
+ editorStylesheet: 'src/app/(frontend)/globals.css',
67
+ editorStylesheetCompiled: '/puck-editor-styles.css',
68
+ editorStylesheetUrls: ['https://fonts.googleapis.com/css2?family=Inter'],
69
+ })
70
+
71
+ // after
72
+ createPuckPlugin({
73
+ editorStylesheets: ['/puck-editor-styles.css', 'https://fonts.googleapis.com/css2?family=Inter'],
74
+ })
75
+ ```
76
+
77
+ Finally, remove the `withPuckCSS` import and wrapper from `next.config.js`, and drop any `editorStylesheets` prop on `PuckConfigProvider` — the plugin wires it through automatically now.
78
+
79
+ > **Why:** the old approach compiled CSS at runtime in dev and via a **webpack plugin** in production. Next.js 16 defaults to Turbopack, which never runs `webpack()` hooks — so the production stylesheet was silently never generated and the editor rendered unstyled, while local dev looked perfect. One artifact, built by your own toolchain, now resolves identically everywhere.
80
+
81
+ Also removed: the `/api/puck/styles` endpoint, the `/next` entry point, and the `postcss` / `postcss-load-config` peer dependencies.
82
+
83
+ ### Upgrading to 0.7.0 (breaking)
84
+
85
+ `0.7.0` raises two floors. Both are a one-line change for most projects:
86
+
87
+ ```bash
88
+ pnpm add @puckeditor/core@^0.23.0 # peer floor moved from >=0.21.0
89
+ ```
90
+
91
+ - **`@puckeditor/core` now requires >= 0.23.0.** The Puck plugins this package bundles are versioned in lockstep with Puck core and import it from the host, so running them against an older core is not a supported combination. Puck 0.23 ships a rewritten canvas drag-and-drop engine and a redesigned outline — **the editing experience changes visibly**, even though no API you call has changed. Worth a pass through your editor after upgrading. See the [Puck 0.23 release notes](https://puckeditor.com/blog/puck-023).
92
+ - **Node 18 is no longer supported;** the floor is now `>=20.9.0`. Node 18 is end-of-life and Puck core 0.23 itself requires `>=20.0.0`.
93
+
94
+ No exports, props, or configuration options were removed or renamed. Full detail in the [changelog](./CHANGELOG.md).
95
+
45
96
  ---
46
97
 
47
98
  ## Quick Start
@@ -16,6 +16,7 @@ import { IframeWrapper } from './components/IframeWrapper.js';
16
16
  import { PreviewModal } from './components/PreviewModal.js';
17
17
  import { DarkModeStyles } from './components/DarkModeStyles.js';
18
18
  import { useUnsavedChanges } from './hooks/useUnsavedChanges.js';
19
+ import { isDataEqual } from './utils/isDataEqual.js';
19
20
  import { createVersionHistoryPlugin } from './plugins/versionHistoryPlugin.js';
20
21
  import { ThemeProvider } from '../theme/index.js';
21
22
  import { usePuckConfig } from '../views/PuckConfigContext.js';
@@ -117,6 +118,11 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
117
118
  ]);
118
119
  // Use a ref to track latest data without causing re-renders
119
120
  const latestDataRef = useRef(dataWithSlug);
121
+ // Track the last loaded/saved data so we can distinguish real user edits from
122
+ // no-op onChange dispatches (e.g. Puck's mount-time resolve pass). Refreshed at
123
+ // every markClean() site (save / publish) so subsequent edits diff against the
124
+ // most recently persisted state.
125
+ const savedDataRef = useRef(dataWithSlug);
120
126
  // Get editor stylesheets from PuckConfigProvider context (as fallback)
121
127
  const { editorStylesheets: contextStylesheets, editorCss: contextCss } = usePuckConfig();
122
128
  // Props take precedence over context
@@ -239,6 +245,7 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
239
245
  setSaveError(null); // Clear any previous error
240
246
  // After saving as draft, update status to draft (shows "Unpublished Changes" if was published)
241
247
  setDocumentStatus('draft');
248
+ savedDataRef.current = typedData;
242
249
  markClean();
243
250
  onSaveSuccess?.(data);
244
251
  } catch (error) {
@@ -287,6 +294,7 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
287
294
  setSaveError(null); // Clear any previous error
288
295
  setDocumentStatus('published'); // Update status after successful publish
289
296
  setWasPublished(true); // Mark as having been published
297
+ savedDataRef.current = typedData;
290
298
  markClean();
291
299
  onSaveSuccess?.(data);
292
300
  } catch (error) {
@@ -340,9 +348,19 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
340
348
  // Handle data change
341
349
  const handleChange = useCallback((data)=>{
342
350
  latestDataRef.current = data;
343
- markDirty();
351
+ // Only mark dirty when the data actually differs from the last loaded/saved
352
+ // state. Puck fires onChange for no-op mount-time resolves (which can differ
353
+ // from the loaded data only by undefined-valued keys); treating those as
354
+ // edits produces a spurious "Unsaved" flag on load. isDataEqual is
355
+ // undefined-tolerant so those resolves compare equal and clear the flag.
356
+ if (isDataEqual(data, savedDataRef.current)) {
357
+ markClean();
358
+ } else {
359
+ markDirty();
360
+ }
344
361
  onChangeProp?.(data);
345
362
  }, [
363
+ markClean,
346
364
  markDirty,
347
365
  onChangeProp
348
366
  ]);
@@ -415,6 +433,7 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
415
433
  setLastSaved(new Date());
416
434
  setSaveError(null);
417
435
  setDocumentStatus('draft');
436
+ savedDataRef.current = data;
418
437
  markClean();
419
438
  onSaveSuccess?.(data);
420
439
  } catch (error) {
@@ -505,18 +524,32 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
505
524
  const defaultPlugins = [
506
525
  headingAnalyzer
507
526
  ];
527
+ // Restoring a version has already persisted that data server-side, so it
528
+ // becomes the new saved baseline. Without this, the `setData` dispatch the
529
+ // restore performs fires an onChange that diffs against the pre-restore
530
+ // baseline and immediately re-flags the (already saved) document as dirty.
531
+ const handleRestoreSuccess = useCallback((restoredData)=>{
532
+ if (restoredData) {
533
+ const typedData = restoredData;
534
+ savedDataRef.current = typedData;
535
+ latestDataRef.current = typedData;
536
+ }
537
+ markClean();
538
+ }, [
539
+ markClean
540
+ ]);
508
541
  // Version history plugin for the plugin rail
509
542
  const versionHistoryPlugin = useMemo(()=>{
510
543
  if (!pageId) return null;
511
544
  return createVersionHistoryPlugin({
512
545
  pageId,
513
546
  apiEndpoint,
514
- onRestoreSuccess: markClean
547
+ onRestoreSuccess: handleRestoreSuccess
515
548
  });
516
549
  }, [
517
550
  pageId,
518
551
  apiEndpoint,
519
- markClean
552
+ handleRestoreSuccess
520
553
  ]);
521
554
  // Fetch AI prompts client-side when prompts collection is enabled
522
555
  // This allows prompts to update in real-time when edited via the prompt editor panel
@@ -29,9 +29,13 @@ export interface VersionHistoryPanelProps {
29
29
  */
30
30
  apiEndpoint?: string;
31
31
  /**
32
- * Callback after successful restore (e.g., to mark editor as clean)
32
+ * Callback after successful restore (e.g., to mark editor as clean).
33
+ *
34
+ * Receives the restored data that was dispatched into the editor, so the
35
+ * caller can treat it as the new "last saved" baseline — the restore endpoint
36
+ * has already persisted it server-side.
33
37
  */
34
- onRestoreSuccess?: () => void;
38
+ onRestoreSuccess?: (restoredData?: Data) => void;
35
39
  }
36
40
  /**
37
41
  * Version history panel for the Puck plugin rail
@@ -256,8 +256,9 @@ const styles = {
256
256
  // Show success message
257
257
  setSuccessMessage(`Restored version from ${formatDate(version.updatedAt)}`);
258
258
  setTimeout(()=>setSuccessMessage(null), 3000);
259
- // Notify parent to mark as clean
260
- onRestoreSuccess?.();
259
+ // Notify parent to mark as clean, handing over the restored data so it
260
+ // becomes the new saved baseline (the restore already persisted it).
261
+ onRestoreSuccess?.(restoredDoc?.puckData);
261
262
  // Refresh version list
262
263
  fetchVersions();
263
264
  } catch (err) {
@@ -1,4 +1,4 @@
1
- import type { Plugin } from '@puckeditor/core';
1
+ import type { Data, Plugin } from '@puckeditor/core';
2
2
  export interface VersionHistoryPluginOptions {
3
3
  /**
4
4
  * Page ID to fetch versions for
@@ -10,9 +10,10 @@ export interface VersionHistoryPluginOptions {
10
10
  */
11
11
  apiEndpoint?: string;
12
12
  /**
13
- * Callback after successful restore (e.g., to mark editor as clean)
13
+ * Callback after successful restore (e.g., to mark editor as clean).
14
+ * Receives the restored data that was dispatched into the editor.
14
15
  */
15
- onRestoreSuccess?: () => void;
16
+ onRestoreSuccess?: (restoredData?: Data) => void;
16
17
  }
17
18
  /**
18
19
  * Creates a Puck plugin for version history
@@ -1,2 +1,3 @@
1
1
  export { injectPageTreeFields } from './injectPageTreeFields.js';
2
2
  export { detectPageTree, hasPageTreeFields } from './detectPageTree.js';
3
+ export { isDataEqual } from './isDataEqual.js';
@@ -1,2 +1,3 @@
1
1
  export { injectPageTreeFields } from './injectPageTreeFields.js';
2
2
  export { detectPageTree, hasPageTreeFields } from './detectPageTree.js';
3
+ export { isDataEqual } from './isDataEqual.js';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Undefined-tolerant deep equality for Puck data.
3
+ *
4
+ * Puck's mount-time resolve pass (`resolveAndCommitData`) can dispatch a `replace`
5
+ * even when the net content is unchanged — e.g. a resolved node differs from the
6
+ * loaded node only by an `undefined`-valued key. Strict deep-equal libraries such
7
+ * as `fast-equals` treat `{}` and `{ k: undefined }` as different, so that no-op
8
+ * resolve looks like a real edit and spuriously marks the document dirty.
9
+ *
10
+ * This comparison treats an absent key and an `undefined`-valued key as equal, so
11
+ * a resolve that only adds/removes `undefined` values compares equal to the loaded
12
+ * data. Object key order is irrelevant (as with any deep equal); array order is
13
+ * significant (Puck content order is meaningful).
14
+ */
15
+ export declare function isDataEqual(a: unknown, b: unknown): boolean;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Undefined-tolerant deep equality for Puck data.
3
+ *
4
+ * Puck's mount-time resolve pass (`resolveAndCommitData`) can dispatch a `replace`
5
+ * even when the net content is unchanged — e.g. a resolved node differs from the
6
+ * loaded node only by an `undefined`-valued key. Strict deep-equal libraries such
7
+ * as `fast-equals` treat `{}` and `{ k: undefined }` as different, so that no-op
8
+ * resolve looks like a real edit and spuriously marks the document dirty.
9
+ *
10
+ * This comparison treats an absent key and an `undefined`-valued key as equal, so
11
+ * a resolve that only adds/removes `undefined` values compares equal to the loaded
12
+ * data. Object key order is irrelevant (as with any deep equal); array order is
13
+ * significant (Puck content order is meaningful).
14
+ */ export function isDataEqual(a, b) {
15
+ if (Object.is(a, b)) {
16
+ return true;
17
+ }
18
+ // NaN is handled by Object.is above; remaining primitives that differ are unequal.
19
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
20
+ return false;
21
+ }
22
+ const aIsArray = Array.isArray(a);
23
+ const bIsArray = Array.isArray(b);
24
+ if (aIsArray !== bIsArray) {
25
+ return false;
26
+ }
27
+ if (aIsArray && bIsArray) {
28
+ if (a.length !== b.length) {
29
+ return false;
30
+ }
31
+ for(let i = 0; i < a.length; i++){
32
+ if (!isDataEqual(a[i], b[i])) {
33
+ return false;
34
+ }
35
+ }
36
+ return true;
37
+ }
38
+ const aObj = a;
39
+ const bObj = b;
40
+ // Compare the union of keys, treating a missing key and an `undefined` value as
41
+ // equivalent so that `{}` and `{ k: undefined }` are considered equal.
42
+ const keys = new Set([
43
+ ...Object.keys(aObj),
44
+ ...Object.keys(bObj)
45
+ ]);
46
+ for (const key of keys){
47
+ const aVal = aObj[key];
48
+ const bVal = bObj[key];
49
+ if (aVal === undefined && bVal === undefined) {
50
+ continue;
51
+ }
52
+ if (!isDataEqual(aVal, bVal)) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ }
@@ -42,7 +42,6 @@ export { generatePagesCollection } from './collections/Pages.js';
42
42
  export { TemplatesCollection } from '../collections/Templates.js';
43
43
  export { getPuckFields, getPuckCollectionConfig, puckDataField, editorVersionField, createEditorVersionField, pageLayoutField, createPageLayoutField, isHomepageField, seoFieldGroup, conversionFieldGroup, } from './fields/index.js';
44
44
  export { generatePuckEditField };
45
- export { PUCK_STYLES_ENDPOINT } from '../endpoints/styles.js';
46
45
  export { createIsHomepageUniqueHook, unsetHomepage, HomepageConflictError, } from './hooks/index.js';
47
46
  export type { IsHomepageUniqueHookOptions } from './hooks/index.js';
48
47
  export type { PuckPluginOptions, PuckAdminConfig } from '../types/index.js';
@@ -5,7 +5,6 @@ import { AiContextCollection } from '../ai/collections/AiContext.js';
5
5
  import { getPuckFields } from './fields/index.js';
6
6
  import { createIsHomepageUniqueHook } from './hooks/isHomepageUnique.js';
7
7
  import { createListHandler, createCreateHandler, createGetHandler, createUpdateHandler, createDeleteHandler, createVersionsHandler, createRestoreHandler } from '../endpoints/index.js';
8
- import { createStylesHandler, PUCK_STYLES_ENDPOINT } from '../endpoints/styles.js';
9
8
  import { createAiEndpointHandler } from '../endpoints/ai.js';
10
9
  import { createPromptsListHandler, createPromptsCreateHandler, createPromptsUpdateHandler, createPromptsDeleteHandler } from '../endpoints/prompts.js';
11
10
  import { createContextListHandler, createContextCreateHandler, createContextUpdateHandler, createContextDeleteHandler } from '../endpoints/context.js';
@@ -105,7 +104,7 @@ import { createContextListHandler, createContextCreateHandler, createContextUpda
105
104
  * })
106
105
  * ```
107
106
  */ export function createPuckPlugin(options = {}) {
108
- const { pagesCollection = 'pages', autoGenerateCollection = true, admin: pluginAdminConfig = {}, enableAdminView = true, adminViewPath = '/puck-editor', enableEndpoints = true, pageTreeIntegration, editorStylesheet, editorStylesheetUrls = [], editorStylesheetCompiled, ai: aiConfig, previewUrl, rootPropsMapping } = options;
107
+ const { pagesCollection = 'pages', autoGenerateCollection = true, admin: pluginAdminConfig = {}, enableAdminView = true, adminViewPath = '/puck-editor', enableEndpoints = true, pageTreeIntegration, editorStylesheets: editorStylesheetsOption = [], ai: aiConfig, previewUrl, rootPropsMapping } = options;
109
108
  const { addEditButton = true } = pluginAdminConfig;
110
109
  // Parse page-tree integration config
111
110
  // - undefined: auto-detect at runtime (null stored, view will check for pageSegment field)
@@ -274,32 +273,17 @@ import { createContextListHandler, createContextCreateHandler, createContextUpda
274
273
  '/puck/:collection/:id/versions',
275
274
  '/puck/:collection/:id/restore'
276
275
  ]);
277
- // Build styles endpoint URL list for PuckConfigProvider
278
- // In production, prefer the pre-compiled static CSS file if provided
279
- // In development, use runtime compilation endpoint for hot reload
280
- const isProduction = process.env.NODE_ENV === 'production';
281
- const useCompiledCss = isProduction && editorStylesheetCompiled;
276
+ // Stylesheet URLs for the editor preview iframe. These are passed straight
277
+ // through: the same URLs resolve in development and production, so the
278
+ // editor can no longer look correct locally and unstyled in production.
282
279
  const editorStylesheets = [
283
- ...useCompiledCss ? [
284
- editorStylesheetCompiled
285
- ] : editorStylesheet ? [
286
- PUCK_STYLES_ENDPOINT
287
- ] : [],
288
- ...editorStylesheetUrls
280
+ ...editorStylesheetsOption
289
281
  ];
290
282
  // Filter out parameterized puck endpoints from previous plugin instances
291
283
  // so we can re-register them with the merged collections list
292
284
  const incomingEndpoints = (incomingConfig.endpoints || []).filter((ep)=>!parameterizedPuckPaths.has(ep.path));
293
285
  const endpoints = enableEndpoints ? [
294
286
  ...incomingEndpoints,
295
- // Styles endpoint MUST be first - exact match before parameterized routes
296
- ...editorStylesheet ? [
297
- {
298
- path: '/puck/styles',
299
- method: 'get',
300
- handler: createStylesHandler(editorStylesheet)
301
- }
302
- ] : [],
303
287
  // AI endpoint (exact match, before parameterized routes)
304
288
  ...aiConfig?.enabled ? [
305
289
  {
@@ -441,7 +425,5 @@ export { TemplatesCollection } from '../collections/Templates.js';
441
425
  export { getPuckFields, getPuckCollectionConfig, puckDataField, editorVersionField, createEditorVersionField, pageLayoutField, createPageLayoutField, isHomepageField, seoFieldGroup, conversionFieldGroup } from './fields/index.js';
442
426
  // Export the edit button generator for hybrid collections
443
427
  export { generatePuckEditField };
444
- // Export styles endpoint constant
445
- export { PUCK_STYLES_ENDPOINT } from '../endpoints/styles.js';
446
428
  // Re-export hooks for hybrid collection integration
447
429
  export { createIsHomepageUniqueHook, unsetHomepage, HomepageConflictError } from './hooks/index.js';
@@ -141,42 +141,44 @@ export interface PuckPluginOptions {
141
141
  */
142
142
  pageTreeIntegration?: boolean | PageTreeIntegrationOptions;
143
143
  /**
144
- * Path to CSS file for editor iframe styling.
145
- * The plugin compiles this file with PostCSS/Tailwind and serves it at /api/puck/styles.
146
- * This allows the editor preview to display your frontend styles (CSS variables, Tailwind utilities).
144
+ * Stylesheet URLs to load inside the editor preview iframe, in order.
147
145
  *
148
- * @example 'src/app/(frontend)/globals.css'
149
- * @example 'src/styles/globals.css'
150
- */
151
- editorStylesheet?: string;
152
- /**
153
- * Additional stylesheet URLs to load in the editor iframe.
154
- * Use this for external stylesheets like Google Fonts that can't be compiled.
146
+ * These are plain URLs the browser fetches — a static file your build emits,
147
+ * or any external stylesheet. The plugin does not compile CSS: your app's own
148
+ * toolchain already does that far better than we can, and compiling it a
149
+ * second time was the source of a dev/production split where the editor
150
+ * looked correct locally and unstyled in production.
155
151
  *
156
- * @example ['https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700']
157
- */
158
- editorStylesheetUrls?: string[];
159
- /**
160
- * Path to pre-compiled CSS file for production use.
161
- * When set, the editor will load this static file instead of using the runtime compilation endpoint.
162
- * Use with `withPuckCSS()` from `@delmaredigital/payload-puck/next` to compile CSS at build time.
152
+ * Generate the file with Tailwind's own CLI as part of your build, so the
153
+ * editor loads byte-identical CSS in every environment:
163
154
  *
164
- * @example '/puck-editor-styles.css'
155
+ * ```jsonc
156
+ * // package.json
157
+ * {
158
+ * "scripts": {
159
+ * "build:puck-css": "tailwindcss -i ./src/app/(frontend)/globals.css -o ./public/puck-editor-styles.css",
160
+ * "build": "pnpm build:puck-css && next build",
161
+ * "dev": "pnpm build:puck-css --watch & next dev"
162
+ * }
163
+ * }
164
+ * ```
165
165
  *
166
- * @example
167
166
  * ```typescript
168
- * // next.config.js
169
- * import { withPuckCSS } from '@delmaredigital/payload-puck/next'
170
- * export default withPuckCSS({ cssInput: 'src/globals.css' })(nextConfig)
171
- *
172
- * // payload.config.ts
173
167
  * createPuckPlugin({
174
- * editorStylesheet: 'src/globals.css', // For dev (runtime compilation)
175
- * editorStylesheetCompiled: '/puck-editor-styles.css', // For prod (static file)
168
+ * editorStylesheets: [
169
+ * '/puck-editor-styles.css',
170
+ * 'https://fonts.googleapis.com/css2?family=Inter:wght@400;700',
171
+ * ],
176
172
  * })
177
173
  * ```
174
+ *
175
+ * Resolved URLs are published on `config.custom.puck.editorStylesheets` and
176
+ * passed to the editor automatically — you do not need to repeat them on
177
+ * `PuckConfigProvider`.
178
+ *
179
+ * @example ['/puck-editor-styles.css']
178
180
  */
179
- editorStylesheetCompiled?: string;
181
+ editorStylesheets?: string[];
180
182
  /**
181
183
  * AI configuration for the plugin.
182
184
  * Enables AI-powered page generation in the editor.
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.6.30";
1
+ export declare const VERSION = "0.8.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/generate-version.js - do not edit manually
2
- export const VERSION = '0.6.30';
2
+ export const VERSION = '0.8.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delmaredigital/payload-puck",
3
- "version": "0.6.30",
3
+ "version": "0.8.0",
4
4
  "description": "Puck visual page builder plugin for Payload CMS",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -82,11 +82,6 @@
82
82
  "types": "./dist/ai/index.d.ts",
83
83
  "default": "./dist/ai/index.js"
84
84
  },
85
- "./next": {
86
- "import": "./dist/next/index.js",
87
- "types": "./dist/next/index.d.ts",
88
- "default": "./dist/next/index.js"
89
- },
90
85
  "./email": {
91
86
  "import": "./dist/email/index.js",
92
87
  "types": "./dist/email/index.d.ts",
@@ -104,25 +99,25 @@
104
99
  "scripts": {
105
100
  "dev-publish": "pnpm build && pnpm version prerelease --preid=dev --no-git-tag-version && pnpm publish --registry http://localhost:4873 --no-git-checks --tag dev",
106
101
  "generate-version": "node scripts/generate-version.js",
107
- "prebuild": "pnpm generate-version",
108
- "build": "pnpm build:swc && pnpm build:types && pnpm copyfiles",
102
+ "build": "pnpm generate-version && pnpm build:swc && pnpm build:types && pnpm copyfiles",
109
103
  "build:swc": "swc ./src -d ./dist --strip-leading-paths",
110
104
  "build:types": "tsc --outDir dist",
111
105
  "copyfiles": "copyfiles -u 1 \"src/**/*.css\" dist",
112
106
  "dev": "pnpm generate-version && swc ./src -d ./dist --strip-leading-paths --watch",
113
- "typecheck": "tsc --noEmit",
107
+ "typecheck": "pnpm generate-version && tsc --noEmit && tsc -p tsconfig.test.json",
108
+ "test": "pnpm generate-version && vitest run",
109
+ "test:watch": "pnpm generate-version && vitest",
110
+ "test:coverage": "pnpm generate-version && vitest run --coverage",
114
111
  "clean": "rm -rf dist",
115
112
  "prepublishOnly": "pnpm build"
116
113
  },
117
114
  "peerDependencies": {
118
115
  "@payloadcms/next": ">=3.69.0",
119
116
  "@payloadcms/ui": ">=3.69.0",
120
- "@puckeditor/core": ">=0.21.0",
117
+ "@puckeditor/core": ">=0.23.0",
121
118
  "@tailwindcss/postcss": ">=4.0.0",
122
119
  "next": ">=15.4.8",
123
120
  "payload": ">=3.69.0",
124
- "postcss": ">=8.0.0",
125
- "postcss-load-config": ">=4.0.0",
126
121
  "react": ">=19.2.1",
127
122
  "react-dom": ">=19.2.1",
128
123
  "tailwindcss": ">=3.0.0 || >=4.0.0",
@@ -135,12 +130,6 @@
135
130
  "@payloadcms/next": {
136
131
  "optional": true
137
132
  },
138
- "postcss": {
139
- "optional": true
140
- },
141
- "postcss-load-config": {
142
- "optional": true
143
- },
144
133
  "tailwindcss": {
145
134
  "optional": true
146
135
  },
@@ -152,9 +141,9 @@
152
141
  }
153
142
  },
154
143
  "dependencies": {
155
- "@puckeditor/cloud-client": "^0.7.0",
156
- "@puckeditor/plugin-ai": "^0.7.0",
157
- "@puckeditor/plugin-heading-analyzer": "^0.21.2",
144
+ "@puckeditor/cloud-client": "^0.8.2",
145
+ "@puckeditor/plugin-ai": "^0.8.2",
146
+ "@puckeditor/plugin-heading-analyzer": "^0.23.0",
158
147
  "@radix-ui/react-popover": "^1.1.15",
159
148
  "@tiptap/core": "^3.20.1",
160
149
  "@tiptap/extension-color": "^3.20.1",
@@ -171,20 +160,22 @@
171
160
  "lucide-react": "^0.469.0"
172
161
  },
173
162
  "devDependencies": {
174
- "@payloadcms/next": "^3.84.1",
175
- "@payloadcms/ui": "^3.84.1",
176
- "@puckeditor/core": "^0.22.0",
163
+ "@payloadcms/next": "^3.87.1",
164
+ "@payloadcms/ui": "^3.87.1",
165
+ "@puckeditor/core": "^0.23.0",
177
166
  "@swc/cli": "^0.6.0",
178
167
  "@swc/core": "^1.15.18",
179
168
  "@types/node": "^24.12.0",
180
169
  "@types/react": "^19.2.14",
181
170
  "@types/react-dom": "^19.2.3",
171
+ "@vitest/coverage-v8": "^3.2.7",
182
172
  "copyfiles": "^2.4.1",
183
- "next": "^16.2.6",
184
- "payload": "^3.84.1",
173
+ "next": "^16.3.0",
174
+ "payload": "^3.87.1",
185
175
  "react": "^19.2.4",
186
176
  "react-dom": "^19.2.4",
187
- "typescript": "^5.9.3"
177
+ "typescript": "^5.9.3",
178
+ "vitest": "^3.2.7"
188
179
  },
189
180
  "keywords": [
190
181
  "payload",
@@ -196,7 +187,7 @@
196
187
  "cms"
197
188
  ],
198
189
  "engines": {
199
- "node": "^18.20.2 || >=20.9.0"
190
+ "node": ">=20.9.0"
200
191
  },
201
192
  "author": "Delmare Digital",
202
193
  "repository": {
@@ -1,4 +0,0 @@
1
- /**
2
- * Ambient type declarations for optional PostCSS/Tailwind peer dependencies
3
- * These modules are dynamically imported at runtime from the consumer's project
4
- */
@@ -1,19 +0,0 @@
1
- /**
2
- * Styles Endpoint Handler
3
- *
4
- * Compiles and serves CSS for the editor iframe.
5
- * Uses the consumer's PostCSS/Tailwind installation via peer dependencies.
6
- * Loads the project's postcss.config.js for proper plugin configuration.
7
- */
8
- import type { PayloadHandler } from 'payload';
9
- /**
10
- * Creates a handler that serves compiled CSS for the editor iframe
11
- *
12
- * @param cssFilePath - Path to CSS file relative to project root
13
- * @returns PayloadHandler that serves compiled CSS
14
- */
15
- export declare function createStylesHandler(cssFilePath: string): PayloadHandler;
16
- /**
17
- * Helper constant for the styles endpoint URL
18
- */
19
- export declare const PUCK_STYLES_ENDPOINT = "/api/puck/styles";
@@ -1,153 +0,0 @@
1
- /**
2
- * Styles Endpoint Handler
3
- *
4
- * Compiles and serves CSS for the editor iframe.
5
- * Uses the consumer's PostCSS/Tailwind installation via peer dependencies.
6
- * Loads the project's postcss.config.js for proper plugin configuration.
7
- */ import { readFileSync, statSync, existsSync } from 'fs';
8
- import { join } from 'path';
9
- const cssCache = new Map();
10
- /**
11
- * Compile CSS using PostCSS with the project's configuration
12
- * Loads postcss.config.js from project root for proper plugin setup
13
- * Falls back to minimal Tailwind-only config if no config file found
14
- */ async function compileCss(css, filePath) {
15
- try {
16
- // Dynamic import to use consumer's PostCSS installation
17
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
- let postcss;
19
- try {
20
- postcss = (await import(/* webpackIgnore: true */ 'postcss')).default;
21
- } catch {
22
- console.warn('[payload-puck] PostCSS not found. CSS will not be processed. Install postcss as a dependency.');
23
- return css;
24
- }
25
- // Try to load the project's postcss.config.js using postcss-load-config
26
- // This ensures all plugins (typography, etc.) are properly loaded
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- let processor;
29
- let usedProjectConfig = false;
30
- try {
31
- // Dynamic import of postcss-load-config (optional peer dependency)
32
- // This package is commonly installed alongside PostCSS
33
- const loadConfigModule = await import(/* webpackIgnore: true */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
34
- // @ts-ignore - optional dependency may not have types
35
- 'postcss-load-config');
36
- const postcssLoadConfig = loadConfigModule.default;
37
- // Load config from project root (where postcss.config.js lives)
38
- const { plugins } = await postcssLoadConfig({}, process.cwd());
39
- processor = postcss(plugins);
40
- usedProjectConfig = true;
41
- } catch {
42
- // postcss-load-config not available or no config found - this is fine
43
- // Fall back to direct Tailwind import silently
44
- // Try Tailwind v4 first (@tailwindcss/postcss)
45
- try {
46
- const tailwindcss = (await import(/* webpackIgnore: true */ '@tailwindcss/postcss')).default;
47
- processor = postcss([
48
- tailwindcss
49
- ]);
50
- } catch {
51
- // Fall back to Tailwind v3 (tailwindcss)
52
- try {
53
- const tailwindcss = (await import(/* webpackIgnore: true */ 'tailwindcss')).default;
54
- processor = postcss([
55
- tailwindcss
56
- ]);
57
- } catch {
58
- // No Tailwind available - just return the CSS as-is
59
- console.warn('[payload-puck] No Tailwind CSS installation found. CSS will not be processed.');
60
- return css;
61
- }
62
- }
63
- }
64
- const result = await processor.process(css, {
65
- from: filePath
66
- });
67
- return result.css;
68
- } catch (error) {
69
- console.error('[payload-puck] CSS compilation error:', error);
70
- throw error;
71
- }
72
- }
73
- /**
74
- * Creates a handler that serves compiled CSS for the editor iframe
75
- *
76
- * @param cssFilePath - Path to CSS file relative to project root
77
- * @returns PayloadHandler that serves compiled CSS
78
- */ export function createStylesHandler(cssFilePath) {
79
- return async (req)=>{
80
- try {
81
- const fullPath = join(process.cwd(), cssFilePath);
82
- // Check if file exists
83
- if (!existsSync(fullPath)) {
84
- console.error(`[payload-puck] CSS file not found: ${fullPath}`);
85
- return new Response(`/* CSS file not found: ${cssFilePath} */`, {
86
- status: 404,
87
- headers: {
88
- 'Content-Type': 'text/css'
89
- }
90
- });
91
- }
92
- // Get file modification time for cache invalidation
93
- const stats = statSync(fullPath);
94
- const mtime = stats.mtimeMs;
95
- // ETag derived from mtime - changes whenever the source file changes,
96
- // whether from a dev-mode edit or a version upgrade recompiling output.
97
- const etag = `"${mtime}"`;
98
- // If the browser's cached copy is still fresh, tell it so without
99
- // doing any file read/compilation work.
100
- const ifNoneMatch = req.headers?.get('if-none-match');
101
- if (ifNoneMatch === etag) {
102
- return new Response(null, {
103
- status: 304,
104
- headers: {
105
- ETag: etag,
106
- 'Cache-Control': 'no-cache'
107
- }
108
- });
109
- }
110
- // Check cache
111
- const cached = cssCache.get(cssFilePath);
112
- if (cached && cached.mtime === mtime) {
113
- return new Response(cached.css, {
114
- headers: {
115
- 'Content-Type': 'text/css',
116
- 'Cache-Control': 'no-cache',
117
- ETag: etag,
118
- 'Last-Modified': new Date(mtime).toUTCString(),
119
- 'X-Puck-Cache': 'hit'
120
- }
121
- });
122
- }
123
- // Read and compile CSS
124
- const rawCss = readFileSync(fullPath, 'utf-8');
125
- const compiledCss = await compileCss(rawCss, fullPath);
126
- // Update cache
127
- cssCache.set(cssFilePath, {
128
- css: compiledCss,
129
- mtime
130
- });
131
- return new Response(compiledCss, {
132
- headers: {
133
- 'Content-Type': 'text/css',
134
- 'Cache-Control': 'no-cache',
135
- ETag: etag,
136
- 'Last-Modified': new Date(mtime).toUTCString(),
137
- 'X-Puck-Cache': 'miss'
138
- }
139
- });
140
- } catch (error) {
141
- console.error('[payload-puck] Styles endpoint error:', error);
142
- return new Response(`/* Error compiling CSS: ${error instanceof Error ? error.message : 'Unknown error'} */`, {
143
- status: 500,
144
- headers: {
145
- 'Content-Type': 'text/css'
146
- }
147
- });
148
- }
149
- };
150
- }
151
- /**
152
- * Helper constant for the styles endpoint URL
153
- */ export const PUCK_STYLES_ENDPOINT = '/api/puck/styles';
@@ -1,64 +0,0 @@
1
- /**
2
- * Next.js Configuration Wrapper for Puck CSS
3
- *
4
- * Compiles CSS at build time using the project's PostCSS/Tailwind configuration.
5
- * This ensures the editor iframe styles work in production (Vercel, etc.) where
6
- * source files aren't available at runtime.
7
- *
8
- * @example
9
- * ```js
10
- * // next.config.js
11
- * import { withPuckCSS } from '@delmaredigital/payload-puck/next'
12
- * import { withPayload } from '@payloadcms/next/withPayload'
13
- *
14
- * export default withPuckCSS({
15
- * cssInput: 'src/app/(frontend)/globals.css',
16
- * })(withPayload(nextConfig))
17
- * ```
18
- */
19
- import type { NextConfig } from 'next';
20
- /**
21
- * Options for the withPuckCSS wrapper
22
- */
23
- export interface WithPuckCSSOptions {
24
- /**
25
- * Path to the source CSS file (relative to project root)
26
- * @example 'src/app/(frontend)/globals.css'
27
- */
28
- cssInput: string;
29
- /**
30
- * Output path for compiled CSS (relative to public/)
31
- * @default 'puck-editor-styles.css'
32
- */
33
- cssOutput?: string;
34
- /**
35
- * Whether to skip compilation in development
36
- * @default true
37
- */
38
- skipInDev?: boolean;
39
- }
40
- /**
41
- * Default output filename for compiled CSS
42
- */
43
- export declare const PUCK_CSS_OUTPUT_DEFAULT = "puck-editor-styles.css";
44
- /**
45
- * Next.js configuration wrapper that compiles Puck editor CSS at build time
46
- *
47
- * @param options - Configuration options
48
- * @returns A function that wraps your Next.js config
49
- *
50
- * @example
51
- * ```js
52
- * import { withPuckCSS } from '@delmaredigital/payload-puck/next'
53
- *
54
- * export default withPuckCSS({
55
- * cssInput: 'src/app/(frontend)/globals.css',
56
- * })(nextConfig)
57
- * ```
58
- */
59
- export declare function withPuckCSS(options: WithPuckCSSOptions): (nextConfig: NextConfig) => NextConfig;
60
- /**
61
- * Get the URL path for the compiled CSS file
62
- * Use this in your plugin configuration
63
- */
64
- export declare function getPuckCSSPath(cssOutput?: string): string;
@@ -1,155 +0,0 @@
1
- /**
2
- * Next.js Configuration Wrapper for Puck CSS
3
- *
4
- * Compiles CSS at build time using the project's PostCSS/Tailwind configuration.
5
- * This ensures the editor iframe styles work in production (Vercel, etc.) where
6
- * source files aren't available at runtime.
7
- *
8
- * @example
9
- * ```js
10
- * // next.config.js
11
- * import { withPuckCSS } from '@delmaredigital/payload-puck/next'
12
- * import { withPayload } from '@payloadcms/next/withPayload'
13
- *
14
- * export default withPuckCSS({
15
- * cssInput: 'src/app/(frontend)/globals.css',
16
- * })(withPayload(nextConfig))
17
- * ```
18
- */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
19
- import { dirname, resolve } from 'path';
20
- /**
21
- * Default output filename for compiled CSS
22
- */ export const PUCK_CSS_OUTPUT_DEFAULT = 'puck-editor-styles.css';
23
- /**
24
- * Compile CSS using PostCSS with the project's configuration
25
- */ async function compileCss(css, filePath) {
26
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
- let postcss;
28
- try {
29
- postcss = (await import('postcss')).default;
30
- } catch {
31
- console.warn('[payload-puck] PostCSS not found. CSS will not be compiled.');
32
- return css;
33
- }
34
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
- let processor;
36
- try {
37
- // Try to load project's postcss.config.js
38
- const loadConfigModule = await import('postcss-load-config');
39
- const postcssLoadConfig = loadConfigModule.default;
40
- const { plugins } = await postcssLoadConfig({}, process.cwd());
41
- processor = postcss(plugins);
42
- } catch {
43
- // Fall back to direct Tailwind import
44
- try {
45
- const tailwindcss = (await import('@tailwindcss/postcss')).default;
46
- processor = postcss([
47
- tailwindcss
48
- ]);
49
- } catch {
50
- try {
51
- const tailwindcss = (await import('tailwindcss')).default;
52
- processor = postcss([
53
- tailwindcss
54
- ]);
55
- } catch {
56
- console.warn('[payload-puck] No Tailwind CSS found. CSS will not be compiled.');
57
- return css;
58
- }
59
- }
60
- }
61
- const result = await processor.process(css, {
62
- from: filePath
63
- });
64
- return result.css;
65
- }
66
- /**
67
- * Webpack plugin that compiles CSS at build time
68
- */ class PuckCSSWebpackPlugin {
69
- options;
70
- compiled = false;
71
- constructor(options){
72
- this.options = options;
73
- }
74
- apply(compiler) {
75
- compiler.hooks.beforeCompile.tapPromise('PuckCSSWebpackPlugin', async ()=>{
76
- // Only compile once per build
77
- if (this.compiled) return;
78
- this.compiled = true;
79
- const { cssInput, cssOutput, skipInDev } = this.options;
80
- // Skip in development if configured
81
- if (skipInDev && process.env.NODE_ENV === 'development') {
82
- console.log('[payload-puck] Skipping CSS compilation in development');
83
- return;
84
- }
85
- const inputPath = resolve(process.cwd(), cssInput);
86
- const outputPath = resolve(process.cwd(), 'public', cssOutput);
87
- // Check if source file exists
88
- if (!existsSync(inputPath)) {
89
- console.error(`[payload-puck] CSS source file not found: ${inputPath}`);
90
- return;
91
- }
92
- try {
93
- console.log(`[payload-puck] Compiling CSS: ${cssInput} -> public/${cssOutput}`);
94
- // Read source CSS
95
- const rawCss = readFileSync(inputPath, 'utf-8');
96
- // Compile with PostCSS/Tailwind
97
- const compiledCss = await compileCss(rawCss, inputPath);
98
- // Ensure public directory exists
99
- const outputDir = dirname(outputPath);
100
- if (!existsSync(outputDir)) {
101
- mkdirSync(outputDir, {
102
- recursive: true
103
- });
104
- }
105
- // Write compiled CSS
106
- writeFileSync(outputPath, compiledCss, 'utf-8');
107
- console.log(`[payload-puck] CSS compiled successfully (${(compiledCss.length / 1024).toFixed(1)}KB)`);
108
- } catch (error) {
109
- console.error('[payload-puck] CSS compilation failed:', error);
110
- }
111
- });
112
- }
113
- }
114
- /**
115
- * Next.js configuration wrapper that compiles Puck editor CSS at build time
116
- *
117
- * @param options - Configuration options
118
- * @returns A function that wraps your Next.js config
119
- *
120
- * @example
121
- * ```js
122
- * import { withPuckCSS } from '@delmaredigital/payload-puck/next'
123
- *
124
- * export default withPuckCSS({
125
- * cssInput: 'src/app/(frontend)/globals.css',
126
- * })(nextConfig)
127
- * ```
128
- */ export function withPuckCSS(options) {
129
- const resolvedOptions = {
130
- cssInput: options.cssInput,
131
- cssOutput: options.cssOutput ?? PUCK_CSS_OUTPUT_DEFAULT,
132
- skipInDev: options.skipInDev ?? true
133
- };
134
- return (nextConfig)=>{
135
- return {
136
- ...nextConfig,
137
- webpack: (webpackConfig, context)=>{
138
- // Add our CSS compilation plugin
139
- webpackConfig.plugins = webpackConfig.plugins || [];
140
- webpackConfig.plugins.push(new PuckCSSWebpackPlugin(resolvedOptions));
141
- // Call existing webpack config if present
142
- if (typeof nextConfig.webpack === 'function') {
143
- return nextConfig.webpack(webpackConfig, context);
144
- }
145
- return webpackConfig;
146
- }
147
- };
148
- };
149
- }
150
- /**
151
- * Get the URL path for the compiled CSS file
152
- * Use this in your plugin configuration
153
- */ export function getPuckCSSPath(cssOutput) {
154
- return `/${cssOutput ?? PUCK_CSS_OUTPUT_DEFAULT}`;
155
- }