@allejo/decap-extras 0.0.1 → 0.0.3
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/.github/workflows/publish.yml +26 -0
- package/README.md +214 -0
- package/dist/next/index.d.mts +27 -0
- package/dist/next/index.d.mts.map +1 -0
- package/dist/next/index.mjs +49 -0
- package/dist/next/index.mjs.map +1 -0
- package/dist/react/index.d.mts +107 -0
- package/dist/react/index.d.mts.map +1 -0
- package/dist/react/index.mjs +46 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +21 -5
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # Required for OIDC
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v6
|
|
19
|
+
with:
|
|
20
|
+
node-version: '24'
|
|
21
|
+
registry-url: 'https://registry.npmjs.org'
|
|
22
|
+
package-manager-cache: false
|
|
23
|
+
- run: npm ci
|
|
24
|
+
- run: npm run build --if-present
|
|
25
|
+
- run: npm test
|
|
26
|
+
- run: npm publish
|
package/README.md
CHANGED
|
@@ -248,8 +248,209 @@ function getPageProps<F extends PageName>(
|
|
|
248
248
|
}
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
+
## React Integration
|
|
252
|
+
|
|
253
|
+
Decap CMS is distributed either as [a single `.js` bundle you can load in an HTML file with `<script>` or as an ESM module you can import](https://decapcms.org/docs/install-decap-cms/). In order to integrate type-safety into working with Decap, we want to incorporate Decap CMS as a Next.js page so that we can get all the type-safety from working within it as a framework. There's one small problem with this though...
|
|
254
|
+
|
|
255
|
+
> You can also use Decap CMS as an npm module. Wherever you import Decap CMS, it automatically runs, taking over the current page.
|
|
256
|
+
|
|
257
|
+
This means, we can't just import Decap CMS in a Next.js page and call it a day. This is because at build time, Next.js will run the code in the page to generate the static HTML, and if Decap CMS is imported and initialized at that time, it's going to try to take over the page during the build process, which is not what we want. We only want Decap CMS to take over the page when it's running in the browser.
|
|
258
|
+
|
|
259
|
+
To work around this, we can use [dynamic imports (i.e., `import()`)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) inside a `useEffect` hook to initialize Decap CMS only on the client after the Next.js boilerplate has rendered and allow the CMS instance to take over the page.
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
Promise.all([
|
|
264
|
+
import('decap-cms-app'),
|
|
265
|
+
|
|
266
|
+
// You can import other Decap CMS modules
|
|
267
|
+
// import('decap-cms-media-library-cloudinary')
|
|
268
|
+
]).then(([cmsMod /*, cloudinaryMod*/]) => {
|
|
269
|
+
const cms = cmsMod.default;
|
|
270
|
+
const cloudinary = cloudinaryMod.default;
|
|
271
|
+
|
|
272
|
+
cms.init({ config });
|
|
273
|
+
|
|
274
|
+
// ...
|
|
275
|
+
// cms.registerPreviewStyle(...)
|
|
276
|
+
// cms.registerPreviewTemplate(...);
|
|
277
|
+
// cms.registerWidget(...);
|
|
278
|
+
// cms.registerMediaLibrary(cloudinary);
|
|
279
|
+
});
|
|
280
|
+
}, []);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
> The import() syntax, commonly called dynamic import, is a function-like expression that allows loading an ECMAScript module asynchronously and dynamically into a potentially non-module environment.
|
|
284
|
+
|
|
285
|
+
Is there a more elegant solution to this problem? I'm not sure! If you can think of one, I would gladly welcome the idea.
|
|
286
|
+
|
|
287
|
+
But knowing all of this and setting it up gets tedious and boilerplate-y really fast. So, in this package, we export a React component called `DecapInstance` that abstracts away all of this dynamic importing and initialization logic, so you can just use it as a wrapper around your admin page and pass in your config, preview styles, templates, widgets, and Cloudinary flag as props.
|
|
288
|
+
|
|
289
|
+
### The `<DecapInstance />` Component
|
|
290
|
+
|
|
291
|
+
We provide a React component called `DecapInstance` that handles the dynamic import and initialization of Decap CMS for you.
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
import type { PropsByCollectionAndFile } from '@allejo/decap-extras';
|
|
295
|
+
import { getCssFilesFromManifest } from '@allejo/decap-extras/next';
|
|
296
|
+
import { DecapInstance } from '@allejo/decap-extras/react';
|
|
297
|
+
import type { ComponentType } from 'react';
|
|
298
|
+
|
|
299
|
+
import type { cmsConfig } from '@/cms/config';
|
|
300
|
+
|
|
301
|
+
// The HomePageProps type and HomePreview component are normally imported from
|
|
302
|
+
// other files, but are included here inline for demonstration purposes.
|
|
303
|
+
type HomePageProps = PropsByCollectionAndFile<
|
|
304
|
+
typeof cmsConfig,
|
|
305
|
+
'pages',
|
|
306
|
+
'home'
|
|
307
|
+
>;
|
|
308
|
+
function HomePreview(props: HomePageProps) {
|
|
309
|
+
return (
|
|
310
|
+
<article>
|
|
311
|
+
<h1>{props.title}</h1>
|
|
312
|
+
{props.subtitle ? <p>{props.subtitle}</p> : null}
|
|
313
|
+
</article>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
type AdminProps = {
|
|
318
|
+
cssFiles: string[];
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export async function getStaticProps() {
|
|
322
|
+
const css = getCssFilesFromManifest(process.env.NODE_ENV ?? 'development', [
|
|
323
|
+
'/',
|
|
324
|
+
'/_app',
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
props: {
|
|
329
|
+
cssFiles: css.flattenedCssFiles,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export default function Admin({ cssFiles }: AdminProps) {
|
|
335
|
+
return (
|
|
336
|
+
<DecapInstance
|
|
337
|
+
config={cmsConfig}
|
|
338
|
+
cssFiles={cssFiles}
|
|
339
|
+
// Pass explicitly so media-library behavior is visible in admin setup.
|
|
340
|
+
useCloudinary={true}
|
|
341
|
+
pages={{
|
|
342
|
+
home: HomePreview as unknown as ComponentType<never>,
|
|
343
|
+
}}
|
|
344
|
+
widgets={{}}
|
|
345
|
+
onInit={(cms) => {
|
|
346
|
+
console.log('Decap initialized', cms);
|
|
347
|
+
}}
|
|
348
|
+
/>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### How it Works
|
|
354
|
+
|
|
355
|
+
`getCssFilesFromManifest` reads Next.js's `.next/build-manifest.json` at build time and returns all CSS files associated with the `/` and `/_app` routes. These are passed as props to the admin page so that we can [register the CSS files into Decap's preview functionality](https://decapcms.org/docs/customization/#registerpreviewstyle).
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
export async function getStaticProps() {
|
|
359
|
+
const cssFiles = getCssFilesFromManifest(process.env.NODE_ENV, [
|
|
360
|
+
'/',
|
|
361
|
+
'/_app',
|
|
362
|
+
]);
|
|
363
|
+
return { props: { cssFiles: cssFiles['flattenedCssFiles'] } };
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
> [!WARNING]
|
|
368
|
+
>
|
|
369
|
+
> This is a bit of a hacky solution to get the CSS files and therefore has some known issues.
|
|
370
|
+
>
|
|
371
|
+
> The CSS for a page might not exist during local development because Next.js only generates the build manifest as the pages are requested. Therefore, if launch `next dev` and immediately open the admin page, the CSS files won't be found for the homepage (i.e., `/`) and won't be registered in the CMS preview, resulting in an unstyled preview. To work around this, you can either visit the homepage first to trigger the generation of the CSS files in the manifest, or run `next build` to generate the manifest with all CSS files before running `next dev`. During local development, this function will check the dev manifest at `.next/dev/build-manifest.json` first, and fallback to the regular manifest at `.next/build-manifest.json` if the dev manifest doesn't exist, so it should work in either case as long as the CSS files have been recorded.
|
|
372
|
+
>
|
|
373
|
+
> If you have a better idea of handling this, please reach out!
|
|
374
|
+
|
|
375
|
+
#### Initialization sequence inside `useEffect`
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
cms.init({ config });
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Bootstraps the CMS with the config from `src/cms/config.ts`.
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
cssFiles.forEach((css) => cms.registerPreviewStyle(css));
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Registers every collected CSS file with the CMS preview panel so the editor renders with production styles.
|
|
388
|
+
|
|
389
|
+
> [!WARNING]
|
|
390
|
+
>
|
|
391
|
+
> **FIXME**: There HAS to be a better way of handling this so that we only load the relevant stylesheets for specific previews. I'm not sure how to solve this yet, but I'll come back to it.
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
cms.registerPreviewTemplate(pageId, ({ entry }) => {
|
|
395
|
+
return <Component {...entry.get('data').toJSON()} />;
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Maps each CMS page entry (`home`, etc.) to its real Next.js page component, passing the editor's live data as props. Editors see the actual production component rendering their changes in real time.
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
cms.registerMediaLibrary(cloudinary);
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Registers the Cloudinary media picker.
|
|
406
|
+
|
|
407
|
+
> [!WARNING]
|
|
408
|
+
>
|
|
409
|
+
> Being able to _disable_ Cloudinary is a work-in-progress. I've always needed it for the projects I'm building so it's enabled by default. I'll get to respecting the "disable" environment variable... eventually.
|
|
410
|
+
|
|
411
|
+
## Server utilities
|
|
412
|
+
|
|
413
|
+
A server-side utility for getting CSS from Next.js build manifests. This is useful for registering CSS stylesheets into Decap's CMS preview system.
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
import { getCssFilesFromManifest } from '@allejo/decap-extras/next';
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Use `getCssFilesFromManifest` in `getStaticProps` to collect the CSS paths needed by your page and pass them as props:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
export async function getStaticProps() {
|
|
423
|
+
const cssFiles = getCssFilesFromManifest(process.env.NODE_ENV, [
|
|
424
|
+
'/',
|
|
425
|
+
'/_app',
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
props: {
|
|
430
|
+
cssFiles: cssFiles['flattenedCssFiles'],
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Then register each path as a preview stylesheet in your Decap CMS admin component:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
export default function Admin({ cssFiles }: Props) {
|
|
440
|
+
// ...
|
|
441
|
+
|
|
442
|
+
cssFiles.forEach((css) => {
|
|
443
|
+
cms.registerPreviewStyle(css);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
> **Note:** This utility reads `.next/build-manifest.json` (or `.next/dev/build-manifest.json` in development) and is intended for Next.js projects only.
|
|
449
|
+
|
|
251
450
|
---
|
|
252
451
|
|
|
452
|
+
## API reference
|
|
453
|
+
|
|
253
454
|
### Widget functions
|
|
254
455
|
|
|
255
456
|
| Function | Decap widget | TypeScript type |
|
|
@@ -283,6 +484,19 @@ function getPageProps<F extends PageName>(
|
|
|
283
484
|
| `OptionalWidget<T>` | Brands a `CmsField` as optional (added by `optional()`). |
|
|
284
485
|
| `WidgetOpts<T>` | The options type for a given widget field type, with `widget` and `fields` stripped. |
|
|
285
486
|
|
|
487
|
+
### Server utilities
|
|
488
|
+
|
|
489
|
+
| Function | Description |
|
|
490
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
491
|
+
| `getCssFilesFromManifest(env, pages)` | Reads the Next.js build manifest and returns CSS paths for the given page routes, grouped by page (`allCssFiles`) and as a deduplicated flat list (`flattenedCssFiles`). |
|
|
492
|
+
|
|
493
|
+
### React utilities
|
|
494
|
+
|
|
495
|
+
| Type/Function | Description |
|
|
496
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
497
|
+
| `DecapInstance` | React component that initializes Decap CMS, registers preview CSS/templates/widgets, optionally enables Cloudinary, and returns `null` after setup. |
|
|
498
|
+
| `DecapInstanceProps` | Props contract for `DecapInstance`, including config, preview CSS paths, page template registry, widget registry, and optional `onInit`/`useCloudinary`. |
|
|
499
|
+
|
|
286
500
|
## License
|
|
287
501
|
|
|
288
502
|
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/next/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Reads the Next.js build manifest and extracts CSS file paths for the given
|
|
4
|
+
* pages, resolving the manifest location based on the current environment.
|
|
5
|
+
*
|
|
6
|
+
* In development, reads from `.next/dev/build-manifest.json`; in all other
|
|
7
|
+
* environments, reads from `.next/build-manifest.json`.
|
|
8
|
+
*
|
|
9
|
+
* CSS paths are rewritten from the `static/` prefix used internally by Next.js
|
|
10
|
+
* to the `/_next/static/` public URL path.
|
|
11
|
+
*
|
|
12
|
+
* @param env - The current environment (e.g. `"development"` or `"production"`).
|
|
13
|
+
* @param pages - List of Next.js page routes to extract CSS files for
|
|
14
|
+
* (e.g. `["/", "/_app"]`).
|
|
15
|
+
*
|
|
16
|
+
* @returns An object with two properties:
|
|
17
|
+
* - `allCssFiles` — a map of page route to its CSS file paths.
|
|
18
|
+
* - `flattenedCssFiles` — a deduplicated flat list of all CSS file paths
|
|
19
|
+
* across every requested page.
|
|
20
|
+
*/
|
|
21
|
+
declare function getCssFilesFromManifest(env: string, pages: string[]): {
|
|
22
|
+
flattenedCssFiles: string[];
|
|
23
|
+
allCssFiles: Record<string, string[]>;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { getCssFilesFromManifest };
|
|
27
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/next/index.ts"],"mappings":";;AAsBA;;;;;;;;;;;;;;;;;;iBAAgB,uBAAA,CAAwB,GAAA,UAAa,KAAA;;eAAd,MAAA;AAAA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/next/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* Reads the Next.js build manifest and extracts CSS file paths for the given
|
|
7
|
+
* pages, resolving the manifest location based on the current environment.
|
|
8
|
+
*
|
|
9
|
+
* In development, reads from `.next/dev/build-manifest.json`; in all other
|
|
10
|
+
* environments, reads from `.next/build-manifest.json`.
|
|
11
|
+
*
|
|
12
|
+
* CSS paths are rewritten from the `static/` prefix used internally by Next.js
|
|
13
|
+
* to the `/_next/static/` public URL path.
|
|
14
|
+
*
|
|
15
|
+
* @param env - The current environment (e.g. `"development"` or `"production"`).
|
|
16
|
+
* @param pages - List of Next.js page routes to extract CSS files for
|
|
17
|
+
* (e.g. `["/", "/_app"]`).
|
|
18
|
+
*
|
|
19
|
+
* @returns An object with two properties:
|
|
20
|
+
* - `allCssFiles` — a map of page route to its CSS file paths.
|
|
21
|
+
* - `flattenedCssFiles` — a deduplicated flat list of all CSS file paths
|
|
22
|
+
* across every requested page.
|
|
23
|
+
*/
|
|
24
|
+
function getCssFilesFromManifest(env, pages) {
|
|
25
|
+
const devManifestPath = path.join(process.cwd(), ".next", "dev", "build-manifest.json");
|
|
26
|
+
const manifestPath = path.join(process.cwd(), ".next", "build-manifest.json");
|
|
27
|
+
const devBuildManifest = fs.existsSync(devManifestPath) ? JSON.parse(fs.readFileSync(devManifestPath, "utf8")) : null;
|
|
28
|
+
const buildManifest = fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, "utf8")) : null;
|
|
29
|
+
const allCssFiles = {};
|
|
30
|
+
const flattenedCssFilesSet = /* @__PURE__ */ new Set();
|
|
31
|
+
pages.forEach((page) => {
|
|
32
|
+
const assets = env === "production" ? buildManifest?.pages[page] : devBuildManifest?.pages[page] ?? buildManifest?.pages[page];
|
|
33
|
+
if (!assets) throw new Error(`No manifest found for ${page}. Ensure "next build" has been run at least once.`);
|
|
34
|
+
assets.filter((file) => file.endsWith(".css")).forEach((file) => {
|
|
35
|
+
if (!allCssFiles[page]) allCssFiles[page] = [];
|
|
36
|
+
const fullPath = file.replace("static/", "/_next/static/");
|
|
37
|
+
allCssFiles[page].push(fullPath);
|
|
38
|
+
flattenedCssFilesSet.add(fullPath);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
flattenedCssFiles: [...flattenedCssFilesSet],
|
|
43
|
+
allCssFiles
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { getCssFilesFromManifest };
|
|
49
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/next/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\n/**\n * Reads the Next.js build manifest and extracts CSS file paths for the given\n * pages, resolving the manifest location based on the current environment.\n *\n * In development, reads from `.next/dev/build-manifest.json`; in all other\n * environments, reads from `.next/build-manifest.json`.\n *\n * CSS paths are rewritten from the `static/` prefix used internally by Next.js\n * to the `/_next/static/` public URL path.\n *\n * @param env - The current environment (e.g. `\"development\"` or `\"production\"`).\n * @param pages - List of Next.js page routes to extract CSS files for\n * (e.g. `[\"/\", \"/_app\"]`).\n *\n * @returns An object with two properties:\n * - `allCssFiles` — a map of page route to its CSS file paths.\n * - `flattenedCssFiles` — a deduplicated flat list of all CSS file paths\n * across every requested page.\n */\nexport function getCssFilesFromManifest(env: string, pages: string[]) {\n\tconst devManifestPath = path.join(\n\t\tprocess.cwd(),\n\t\t'.next',\n\t\t'dev',\n\t\t'build-manifest.json',\n\t);\n\tconst manifestPath = path.join(process.cwd(), '.next', 'build-manifest.json');\n\n\tconst devBuildManifest = fs.existsSync(devManifestPath)\n\t\t? JSON.parse(fs.readFileSync(devManifestPath, 'utf8'))\n\t\t: null;\n\tconst buildManifest = fs.existsSync(manifestPath)\n\t\t? JSON.parse(fs.readFileSync(manifestPath, 'utf8'))\n\t\t: null;\n\n\tconst allCssFiles: Record<string, string[]> = {};\n\tconst flattenedCssFilesSet = new Set<string>();\n\n\tpages.forEach((page) => {\n\t\tconst assets: string[] | undefined =\n\t\t\tenv === 'production'\n\t\t\t\t? buildManifest?.pages[page]\n\t\t\t\t: (devBuildManifest?.pages[page] ?? buildManifest?.pages[page]);\n\n\t\tif (!assets) {\n\t\t\tthrow new Error(\n\t\t\t\t`No manifest found for ${page}. Ensure \"next build\" has been run at least once.`,\n\t\t\t);\n\t\t}\n\n\t\tassets\n\t\t\t.filter((file: string) => file.endsWith('.css'))\n\t\t\t.forEach((file: string) => {\n\t\t\t\tif (!allCssFiles[page]) {\n\t\t\t\t\tallCssFiles[page] = [];\n\t\t\t\t}\n\n\t\t\t\tconst fullPath = file.replace('static/', '/_next/static/');\n\n\t\t\t\tallCssFiles[page].push(fullPath);\n\t\t\t\tflattenedCssFilesSet.add(fullPath);\n\t\t\t});\n\t});\n\n\treturn {\n\t\tflattenedCssFiles: [...flattenedCssFilesSet],\n\t\tallCssFiles,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,wBAAwB,KAAa,OAAiB;CACrE,MAAM,kBAAkB,KAAK,KAC5B,QAAQ,IAAI,GACZ,SACA,OACA,qBACD;CACA,MAAM,eAAe,KAAK,KAAK,QAAQ,IAAI,GAAG,SAAS,qBAAqB;CAE5E,MAAM,mBAAmB,GAAG,WAAW,eAAe,IACnD,KAAK,MAAM,GAAG,aAAa,iBAAiB,MAAM,CAAC,IACnD;CACH,MAAM,gBAAgB,GAAG,WAAW,YAAY,IAC7C,KAAK,MAAM,GAAG,aAAa,cAAc,MAAM,CAAC,IAChD;CAEH,MAAM,cAAwC,CAAC;CAC/C,MAAM,uCAAuB,IAAI,IAAY;CAE7C,MAAM,SAAS,SAAS;EACvB,MAAM,SACL,QAAQ,eACL,eAAe,MAAM,QACpB,kBAAkB,MAAM,SAAS,eAAe,MAAM;EAE3D,IAAI,CAAC,QACJ,MAAM,IAAI,MACT,yBAAyB,KAAK,kDAC/B;EAGD,OACE,QAAQ,SAAiB,KAAK,SAAS,MAAM,CAAC,EAC9C,SAAS,SAAiB;GAC1B,IAAI,CAAC,YAAY,OAChB,YAAY,QAAQ,CAAC;GAGtB,MAAM,WAAW,KAAK,QAAQ,WAAW,gBAAgB;GAEzD,YAAY,MAAM,KAAK,QAAQ;GAC/B,qBAAqB,IAAI,QAAQ;EAClC,CAAC;CACH,CAAC;CAED,OAAO;EACN,mBAAmB,CAAC,GAAG,oBAAoB;EAC3C;CACD;AACD"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ComponentClass, ComponentType } from "react";
|
|
2
|
+
import { CMS, CmsConfig, CmsWidgetControlProps, CmsWidgetPreviewProps } from "decap-cms-core";
|
|
3
|
+
|
|
4
|
+
//#region src/react/DecapInstance.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Props for {@link DecapInstance}.
|
|
7
|
+
*
|
|
8
|
+
* This object describes everything needed to initialize the Decap CMS runtime
|
|
9
|
+
* in a React/Next.js admin page.
|
|
10
|
+
*/
|
|
11
|
+
interface DecapInstanceProps {
|
|
12
|
+
/**
|
|
13
|
+
* Decap CMS configuration object passed to `cms.init({ config })`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const config = {
|
|
17
|
+
* backend: { name: 'git-gateway' },
|
|
18
|
+
* collections: [...],
|
|
19
|
+
* } as const;
|
|
20
|
+
*/
|
|
21
|
+
config: CmsConfig;
|
|
22
|
+
/**
|
|
23
|
+
* Stylesheets that should be available inside preview panes.
|
|
24
|
+
*
|
|
25
|
+
* This is commonly generated in Next.js via
|
|
26
|
+
* `getCssFilesFromManifest(process.env.NODE_ENV, ['/', '/_app'])` and passed
|
|
27
|
+
* through page props.
|
|
28
|
+
*/
|
|
29
|
+
cssFiles: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Map of preview template IDs to React components.
|
|
32
|
+
*
|
|
33
|
+
* Each component receives the serialized entry data as props
|
|
34
|
+
* (`entry.get('data').toJSON()`). In practice, this lets you pair inferred
|
|
35
|
+
* content types from `PropsByCollectionAndFile` with your preview templates.
|
|
36
|
+
*/
|
|
37
|
+
pages: Record<string, ComponentType<never>>;
|
|
38
|
+
/**
|
|
39
|
+
* Optional callback invoked after Decap has been initialized and all
|
|
40
|
+
* templates/widgets have been registered.
|
|
41
|
+
*
|
|
42
|
+
* Use this hook for custom registration that depends on the CMS instance.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* onInit: (cms) => {
|
|
46
|
+
* cms.registerEditorComponent({
|
|
47
|
+
* id: 'callout',
|
|
48
|
+
* label: 'Callout',
|
|
49
|
+
* });
|
|
50
|
+
* }
|
|
51
|
+
*/
|
|
52
|
+
onInit?: (cms: CMS) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Enables Cloudinary media library registration when set to `true`.
|
|
55
|
+
*
|
|
56
|
+
* Defaults to `false`. Pass this explicitly so the admin setup remains
|
|
57
|
+
* obvious to future maintainers.
|
|
58
|
+
*/
|
|
59
|
+
useCloudinary?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Registry of custom widgets to register with Decap.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* widgets: {
|
|
65
|
+
* color: {
|
|
66
|
+
* control: ColorControl,
|
|
67
|
+
* preview: ColorPreview,
|
|
68
|
+
* },
|
|
69
|
+
* }
|
|
70
|
+
*/
|
|
71
|
+
widgets: Record<string, {
|
|
72
|
+
/**
|
|
73
|
+
* Widget editor component used in the Decap entry form.
|
|
74
|
+
*/
|
|
75
|
+
control: ComponentClass<CmsWidgetControlProps<unknown>>;
|
|
76
|
+
/**
|
|
77
|
+
* Widget preview component used in Decap preview rendering.
|
|
78
|
+
*/
|
|
79
|
+
preview: ComponentType<CmsWidgetPreviewProps<unknown>>;
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Initializes Decap CMS from a React component, then returns `null`.
|
|
84
|
+
*
|
|
85
|
+
* This component performs all setup in a `useEffect` call: it boots Decap,
|
|
86
|
+
* registers preview CSS, wires preview templates, registers custom widgets,
|
|
87
|
+
* optionally enables Cloudinary, and finally calls `onInit`.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* import type { PropsByCollectionAndFile } from '@allejo/decap-extras';
|
|
91
|
+
* import { DecapInstance } from '@allejo/decap-extras/react';
|
|
92
|
+
*
|
|
93
|
+
* import type { cmsConfig } from '@/cms/config';
|
|
94
|
+
*
|
|
95
|
+
* type HomePageProps = PropsByCollectionAndFile<typeof cmsConfig, 'pages', 'home'>;
|
|
96
|
+
*/
|
|
97
|
+
declare const DecapInstance: ({
|
|
98
|
+
config,
|
|
99
|
+
cssFiles,
|
|
100
|
+
onInit,
|
|
101
|
+
pages,
|
|
102
|
+
useCloudinary,
|
|
103
|
+
widgets
|
|
104
|
+
}: DecapInstanceProps) => null;
|
|
105
|
+
//#endregion
|
|
106
|
+
export { DecapInstance, type DecapInstanceProps };
|
|
107
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/react/DecapInstance.tsx"],"mappings":";;;;;;AAcA;;;;UAAiB,kBAAA;EA0BT;;;;;;;;;EAhBP,MAAA,EAAQ,SAAA;EAAA;;;;;;;EAQR,QAAA;EA8BA;;;;;;;EAtBA,KAAA,EAAO,MAAA,SAAe,aAAA;EA4CG;;AAAqB;AAoB/C;;;;;;;;;;;EAjDC,MAAA,IAAU,GAAA,EAAK,GAAA;EAiDc;;;;;;EA1C7B,aAAA;EA0C6B;;;;;;;AAOT;;;;EArCpB,OAAA,EAAS,MAAA;;;;IAMP,OAAA,EAAS,cAAA,CAAe,qBAAA;;;;IAIxB,OAAA,EAAS,aAAA,CAAc,qBAAA;EAAA;AAAA;;;;;;;;;;;;;;;;cAoBb,aAAA;EAAiB,MAAA;EAAA,QAAA;EAAA,MAAA;EAAA,KAAA;EAAA,aAAA;EAAA;AAAA,GAO3B,kBAAA"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/react/DecapInstance.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Initializes Decap CMS from a React component, then returns `null`.
|
|
7
|
+
*
|
|
8
|
+
* This component performs all setup in a `useEffect` call: it boots Decap,
|
|
9
|
+
* registers preview CSS, wires preview templates, registers custom widgets,
|
|
10
|
+
* optionally enables Cloudinary, and finally calls `onInit`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import type { PropsByCollectionAndFile } from '@allejo/decap-extras';
|
|
14
|
+
* import { DecapInstance } from '@allejo/decap-extras/react';
|
|
15
|
+
*
|
|
16
|
+
* import type { cmsConfig } from '@/cms/config';
|
|
17
|
+
*
|
|
18
|
+
* type HomePageProps = PropsByCollectionAndFile<typeof cmsConfig, 'pages', 'home'>;
|
|
19
|
+
*/
|
|
20
|
+
const DecapInstance = ({ config, cssFiles, onInit, pages, useCloudinary = false, widgets }) => {
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
Promise.all([import("decap-cms-app"), useCloudinary ? import("decap-cms-media-library-cloudinary") : Promise.resolve(null)]).then(([cmsMod, cloudinaryMod]) => {
|
|
23
|
+
const cms = cmsMod.default;
|
|
24
|
+
const cloudinary = cloudinaryMod.default;
|
|
25
|
+
cms.init({ config });
|
|
26
|
+
cssFiles.forEach((css) => {
|
|
27
|
+
cms.registerPreviewStyle(css);
|
|
28
|
+
});
|
|
29
|
+
Object.entries(pages).forEach(([pageId, PageComponent]) => {
|
|
30
|
+
cms.registerPreviewTemplate(pageId, ({ entry }) => {
|
|
31
|
+
return /* @__PURE__ */ jsx(PageComponent, { ...entry.get("data").toJSON() });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
Object.entries(widgets).forEach(([widgetId, { control, preview }]) => {
|
|
35
|
+
cms.registerWidget(widgetId, control, preview);
|
|
36
|
+
});
|
|
37
|
+
if (cloudinaryMod) cms.registerMediaLibrary(cloudinary);
|
|
38
|
+
onInit?.(cms);
|
|
39
|
+
});
|
|
40
|
+
}, [cssFiles]);
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { DecapInstance };
|
|
46
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["Component"],"sources":["../../src/react/DecapInstance.tsx"],"sourcesContent":["import type {\n\tCMS,\n\tCmsConfig,\n\tCmsWidgetControlProps,\n\tCmsWidgetPreviewProps,\n} from 'decap-cms-core';\nimport { ComponentClass, ComponentType, useEffect } from 'react';\n\n/**\n * Props for {@link DecapInstance}.\n *\n * This object describes everything needed to initialize the Decap CMS runtime\n * in a React/Next.js admin page.\n */\nexport interface DecapInstanceProps {\n\t/**\n\t * Decap CMS configuration object passed to `cms.init({ config })`.\n\t *\n\t * @example\n\t * const config = {\n\t * \tbackend: { name: 'git-gateway' },\n\t * \tcollections: [...],\n\t * } as const;\n\t */\n\tconfig: CmsConfig;\n\t/**\n\t * Stylesheets that should be available inside preview panes.\n\t *\n\t * This is commonly generated in Next.js via\n\t * `getCssFilesFromManifest(process.env.NODE_ENV, ['/', '/_app'])` and passed\n\t * through page props.\n\t */\n\tcssFiles: string[];\n\t/**\n\t * Map of preview template IDs to React components.\n\t *\n\t * Each component receives the serialized entry data as props\n\t * (`entry.get('data').toJSON()`). In practice, this lets you pair inferred\n\t * content types from `PropsByCollectionAndFile` with your preview templates.\n\t */\n\tpages: Record<string, ComponentType<never>>;\n\t/**\n\t * Optional callback invoked after Decap has been initialized and all\n\t * templates/widgets have been registered.\n\t *\n\t * Use this hook for custom registration that depends on the CMS instance.\n\t *\n\t * @example\n\t * onInit: (cms) => {\n\t * \tcms.registerEditorComponent({\n\t * \t\tid: 'callout',\n\t * \t\tlabel: 'Callout',\n\t * \t});\n\t * }\n\t */\n\tonInit?: (cms: CMS) => void;\n\t/**\n\t * Enables Cloudinary media library registration when set to `true`.\n\t *\n\t * Defaults to `false`. Pass this explicitly so the admin setup remains\n\t * obvious to future maintainers.\n\t */\n\tuseCloudinary?: boolean;\n\t/**\n\t * Registry of custom widgets to register with Decap.\n\t *\n\t * @example\n\t * widgets: {\n\t * \tcolor: {\n\t * \t\tcontrol: ColorControl,\n\t * \t\tpreview: ColorPreview,\n\t * \t},\n\t * }\n\t */\n\twidgets: Record<\n\t\tstring,\n\t\t{\n\t\t\t/**\n\t\t\t * Widget editor component used in the Decap entry form.\n\t\t\t */\n\t\t\tcontrol: ComponentClass<CmsWidgetControlProps<unknown>>;\n\t\t\t/**\n\t\t\t * Widget preview component used in Decap preview rendering.\n\t\t\t */\n\t\t\tpreview: ComponentType<CmsWidgetPreviewProps<unknown>>;\n\t\t}\n\t>;\n}\n\n/**\n * Initializes Decap CMS from a React component, then returns `null`.\n *\n * This component performs all setup in a `useEffect` call: it boots Decap,\n * registers preview CSS, wires preview templates, registers custom widgets,\n * optionally enables Cloudinary, and finally calls `onInit`.\n *\n * @example\n * import type { PropsByCollectionAndFile } from '@allejo/decap-extras';\n * import { DecapInstance } from '@allejo/decap-extras/react';\n *\n * import type { cmsConfig } from '@/cms/config';\n *\n * type HomePageProps = PropsByCollectionAndFile<typeof cmsConfig, 'pages', 'home'>;\n */\nexport const DecapInstance = ({\n\tconfig,\n\tcssFiles,\n\tonInit,\n\tpages,\n\tuseCloudinary = false,\n\twidgets,\n}: DecapInstanceProps) => {\n\tuseEffect(() => {\n\t\tPromise.all([\n\t\t\timport('decap-cms-app'),\n\t\t\tuseCloudinary\n\t\t\t\t? // @ts-expect-error package has no types\n\t\t\t\t\timport('decap-cms-media-library-cloudinary')\n\t\t\t\t: Promise.resolve(null),\n\t\t]).then(([cmsMod, cloudinaryMod]) => {\n\t\t\tconst cms = cmsMod.default;\n\t\t\tconst cloudinary = cloudinaryMod.default;\n\n\t\t\tcms.init({ config });\n\n\t\t\tcssFiles.forEach((css) => {\n\t\t\t\tcms.registerPreviewStyle(css);\n\t\t\t});\n\n\t\t\tObject.entries(pages).forEach(([pageId, PageComponent]) => {\n\t\t\t\tcms.registerPreviewTemplate(pageId, ({ entry }) => {\n\t\t\t\t\tconst Component = PageComponent as ComponentType<\n\t\t\t\t\t\tRecord<string, unknown>\n\t\t\t\t\t>;\n\n\t\t\t\t\treturn <Component {...entry.get('data').toJSON()} />;\n\t\t\t\t});\n\t\t\t});\n\n\t\t\tObject.entries(widgets).forEach(([widgetId, { control, preview }]) => {\n\t\t\t\tcms.registerWidget(widgetId, control, preview);\n\t\t\t});\n\n\t\t\tif (cloudinaryMod) {\n\t\t\t\tcms.registerMediaLibrary(cloudinary);\n\t\t\t}\n\n\t\t\tonInit?.(cms);\n\t\t});\n\t}, [cssFiles]);\n\n\treturn null;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAwGA,MAAa,iBAAiB,EAC7B,QACA,UACA,QACA,OACA,gBAAgB,OAChB,cACyB;CACzB,gBAAgB;EACf,QAAQ,IAAI,CACX,OAAO,kBACP,gBAEE,OAAO,wCACN,QAAQ,QAAQ,IAAI,CACxB,CAAC,EAAE,MAAM,CAAC,QAAQ,mBAAmB;GACpC,MAAM,MAAM,OAAO;GACnB,MAAM,aAAa,cAAc;GAEjC,IAAI,KAAK,EAAE,OAAO,CAAC;GAEnB,SAAS,SAAS,QAAQ;IACzB,IAAI,qBAAqB,GAAG;GAC7B,CAAC;GAED,OAAO,QAAQ,KAAK,EAAE,SAAS,CAAC,QAAQ,mBAAmB;IAC1D,IAAI,wBAAwB,SAAS,EAAE,YAAY;KAKlD,OAAO,oBAACA,eAAD,EAAW,GAAI,MAAM,IAAI,MAAM,EAAE,OAAO,EAAI;IACpD,CAAC;GACF,CAAC;GAED,OAAO,QAAQ,OAAO,EAAE,SAAS,CAAC,UAAU,EAAE,SAAS,eAAe;IACrE,IAAI,eAAe,UAAU,SAAS,OAAO;GAC9C,CAAC;GAED,IAAI,eACH,IAAI,qBAAqB,UAAU;GAGpC,SAAS,GAAG;EACb,CAAC;CACF,GAAG,CAAC,QAAQ,CAAC;CAEb,OAAO;AACR"}
|
package/package.json
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allejo/decap-extras",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "A TypeScript utility library for Decap CMS with utility types to derive types from Decap CMS configuration files.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"decapcms"
|
|
7
7
|
],
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/allejo/decap-extras"
|
|
10
|
+
"url": "git+https://github.com/allejo/decap-extras.git"
|
|
11
11
|
},
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"author": "Vladimir \"allejo\" Jimenez",
|
|
14
14
|
"type": "module",
|
|
15
|
-
"
|
|
16
|
-
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.mts",
|
|
18
|
+
"import": "./dist/index.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./react": {
|
|
21
|
+
"types": "./dist/react/index.d.mts",
|
|
22
|
+
"import": "./dist/react/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./next": {
|
|
25
|
+
"types": "./dist/next/index.d.mts",
|
|
26
|
+
"import": "./dist/next/index.mjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
17
29
|
"scripts": {
|
|
18
30
|
"build": "tsdown",
|
|
19
31
|
"format": "prettier --write .",
|
|
@@ -31,15 +43,19 @@
|
|
|
31
43
|
"@types/eslint": "^8.44.7",
|
|
32
44
|
"@types/node": "^24.12.4",
|
|
33
45
|
"decap-cms-app": "^3.12.2",
|
|
46
|
+
"decap-cms-media-library-cloudinary": "^3.1.0",
|
|
34
47
|
"eslint": "^9.21.0",
|
|
35
48
|
"husky": "^9.0.10",
|
|
36
49
|
"lint-staged": "^15.4.3",
|
|
37
50
|
"prettier": "^3.1.1",
|
|
51
|
+
"react": "^19.2.6",
|
|
38
52
|
"tsdown": "^0.22.0",
|
|
39
53
|
"typescript": "^5.9.3",
|
|
40
54
|
"vitest": "^3.2.4"
|
|
41
55
|
},
|
|
42
56
|
"peerDependencies": {
|
|
43
|
-
"decap-cms-core": "^3.12.2"
|
|
57
|
+
"decap-cms-core": "^3.12.2",
|
|
58
|
+
"decap-cms-media-library-cloudinary": "^3.1.0",
|
|
59
|
+
"react": "16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
44
60
|
}
|
|
45
61
|
}
|