@nuasite/cms 0.42.1 → 0.43.0-beta.2
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/editor.js +2221 -2219
- package/package.json +18 -2
- package/src/admin/entry.tsx +35 -0
- package/src/admin/env.d.ts +15 -0
- package/src/admin/tsconfig.json +15 -0
- package/src/admin/tsconfig.tsbuildinfo +1 -0
- package/src/dev-middleware.ts +12 -1
- package/src/editor/components/collections-browser.tsx +4 -1
- package/src/editor/components/toolbar.tsx +20 -11
- package/src/editor/signals.ts +4 -2
- package/src/handlers/api-routes.ts +192 -48
- package/src/handlers/page-ops.ts +4 -189
- package/src/index.ts +137 -59
- package/src/local-admin.ts +232 -0
- package/src/media/types.ts +11 -55
- package/src/mode.ts +53 -0
- package/src/tsconfig.json +10 -1
- package/src/types.ts +38 -225
- package/src/handlers/markdown-ops.ts +0 -474
- package/src/handlers/redirect-ops.ts +0 -163
- package/src/media/contember.ts +0 -85
- package/src/media/local.ts +0 -152
- package/src/media/project-images.ts +0 -81
- package/src/media/s3.ts +0 -154
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"directory": "packages/astro-cms"
|
|
15
15
|
},
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.43.0-beta.2",
|
|
18
18
|
"module": "src/index.ts",
|
|
19
19
|
"types": "src/index.ts",
|
|
20
20
|
"type": "module",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@nuasite/cms-core": "0.43.0-beta.2",
|
|
30
|
+
"@nuasite/cms-types": "0.43.0-beta.2",
|
|
31
|
+
"@nuasite/cms-sidecar": "0.43.0-beta.2",
|
|
32
|
+
"@nuasite/collections-admin": "0.43.0-beta.2",
|
|
29
33
|
"@astrojs/compiler": "^3.0.1",
|
|
30
34
|
"@babel/parser": "^7.29.2",
|
|
31
35
|
"node-html-parser": "^7.1.0",
|
|
@@ -33,6 +37,10 @@
|
|
|
33
37
|
},
|
|
34
38
|
"devDependencies": {
|
|
35
39
|
"@babel/types": "^7.29.0",
|
|
40
|
+
"@types/react": "^19.2.7",
|
|
41
|
+
"@types/react-dom": "^19.2.3",
|
|
42
|
+
"react": "^19.2.1",
|
|
43
|
+
"react-dom": "^19.2.1",
|
|
36
44
|
"@milkdown/core": "^7.20.0",
|
|
37
45
|
"@milkdown/ctx": "^7.20.0",
|
|
38
46
|
"@milkdown/plugin-listener": "^7.20.0",
|
|
@@ -63,11 +71,19 @@
|
|
|
63
71
|
"astro": "6.1.4",
|
|
64
72
|
"typescript": "^6.0.2",
|
|
65
73
|
"vite": "^7.0.0",
|
|
66
|
-
"@aws-sdk/client-s3": "^3.0.0"
|
|
74
|
+
"@aws-sdk/client-s3": "^3.0.0",
|
|
75
|
+
"react": "^19.0.0",
|
|
76
|
+
"react-dom": "^19.0.0"
|
|
67
77
|
},
|
|
68
78
|
"peerDependenciesMeta": {
|
|
69
79
|
"@aws-sdk/client-s3": {
|
|
70
80
|
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"react": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"react-dom": {
|
|
86
|
+
"optional": true
|
|
71
87
|
}
|
|
72
88
|
},
|
|
73
89
|
"scripts": {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local `/_nua/admin` SPA entry (cms-headless F7).
|
|
3
|
+
*
|
|
4
|
+
* Mounts the host-agnostic `@nuasite/collections-admin` SPA into the page served
|
|
5
|
+
* by the local-admin dev middleware. The only host-specific input is `apiBase`,
|
|
6
|
+
* which the HTML shell injects as `window.__NUA_ADMIN_API_BASE__` — it points at
|
|
7
|
+
* the in-process cms-sidecar mounted by the dev middleware (`…/cms/v1`). The same
|
|
8
|
+
* lib backs the webmaster Collections tab; only `apiBase` differs.
|
|
9
|
+
*
|
|
10
|
+
* This file is served as a virtual module through Astro's Vite dev server, so it
|
|
11
|
+
* is transformed (TSX → JS, React resolved) and HMR-capable like any app module.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { CollectionsAdminApp } from '@nuasite/collections-admin'
|
|
15
|
+
import { StrictMode } from 'react'
|
|
16
|
+
import { createRoot } from 'react-dom/client'
|
|
17
|
+
|
|
18
|
+
function readApiBase(): string {
|
|
19
|
+
const value = window.__NUA_ADMIN_API_BASE__
|
|
20
|
+
if (typeof value !== 'string' || value === '') {
|
|
21
|
+
throw new Error('[nua-cms] /_nua/admin: window.__NUA_ADMIN_API_BASE__ is not set')
|
|
22
|
+
}
|
|
23
|
+
return value
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const container = document.getElementById('nua-admin-root')
|
|
27
|
+
if (!container) {
|
|
28
|
+
throw new Error('[nua-cms] /_nua/admin: #nua-admin-root container is missing')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
createRoot(container).render(
|
|
32
|
+
<StrictMode>
|
|
33
|
+
<CollectionsAdminApp apiBase={readApiBase()} />
|
|
34
|
+
</StrictMode>,
|
|
35
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient globals for the local `/_nua/admin` SPA entry (cms-headless F7).
|
|
3
|
+
*
|
|
4
|
+
* The dev middleware injects the cms-sidecar `apiBase` into the HTML shell as a
|
|
5
|
+
* window global; the entry reads it to drive `<CollectionsAdminApp apiBase={…} />`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
/** Base URL the in-process cms-sidecar is mounted at (the SPA adds nothing else; it is already the `/cms/v1` base). */
|
|
11
|
+
__NUA_ADMIN_API_BASE__?: string
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.settings.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": false,
|
|
5
|
+
"noEmit": true,
|
|
6
|
+
"emitDeclarationOnly": false,
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
9
|
+
"types": ["react", "react-dom"]
|
|
10
|
+
},
|
|
11
|
+
"references": [
|
|
12
|
+
{ "path": "../../../collections-admin/src" },
|
|
13
|
+
{ "path": "../../../cms-types/src" }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"fileNames":["../../../../node_modules/typescript/lib/lib.es5.d.ts","../../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../../node_modules/typescript/lib/lib.es2021.d.ts","../../../../node_modules/typescript/lib/lib.es2022.d.ts","../../../../node_modules/typescript/lib/lib.es2023.d.ts","../../../../node_modules/typescript/lib/lib.es2024.d.ts","../../../../node_modules/typescript/lib/lib.es2025.d.ts","../../../../node_modules/typescript/lib/lib.esnext.d.ts","../../../../node_modules/typescript/lib/lib.dom.d.ts","../../../../node_modules/typescript/lib/lib.dom.iterable.d.ts","../../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../../../node_modules/typescript/lib/lib.es2023.array.d.ts","../../../../node_modules/typescript/lib/lib.es2023.collection.d.ts","../../../../node_modules/typescript/lib/lib.es2023.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../../../node_modules/typescript/lib/lib.es2024.collection.d.ts","../../../../node_modules/typescript/lib/lib.es2024.object.d.ts","../../../../node_modules/typescript/lib/lib.es2024.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../../../node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.es2024.string.d.ts","../../../../node_modules/typescript/lib/lib.es2025.collection.d.ts","../../../../node_modules/typescript/lib/lib.es2025.float16.d.ts","../../../../node_modules/typescript/lib/lib.es2025.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2025.iterator.d.ts","../../../../node_modules/typescript/lib/lib.es2025.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2025.regexp.d.ts","../../../../node_modules/typescript/lib/lib.esnext.array.d.ts","../../../../node_modules/typescript/lib/lib.esnext.collection.d.ts","../../../../node_modules/typescript/lib/lib.esnext.date.d.ts","../../../../node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../../../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../../../node_modules/typescript/lib/lib.esnext.error.d.ts","../../../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../../../node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.esnext.temporal.d.ts","../../../../node_modules/typescript/lib/lib.esnext.typedarrays.d.ts","../../../../node_modules/typescript/lib/lib.decorators.d.ts","../../../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../../../node_modules/@types/react/global.d.ts","../../../../node_modules/csstype/index.d.ts","../../../../node_modules/@types/react/index.d.ts","../../../../node_modules/@types/react/jsx-runtime.d.ts","../../../collections-admin/dist/types/app.d.ts","../../../cms-types/dist/types/index.d.ts","../../../collections-admin/dist/types/client.d.ts","../../../collections-admin/dist/types/form-model.d.ts","../../../collections-admin/dist/types/index.d.ts","../../../../node_modules/@types/react-dom/client.d.ts","./entry.tsx","./env.d.ts","../../../../node_modules/@types/react-dom/index.d.ts"],"fileIdsList":[[92],[90,91],[92,93,98,99],[95],[94,95,96,97]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"1e9332c23e9a907175e0ffc6a49e236f97b48838cc8aec9ce7e4cec21e544b65","impliedFormat":1},{"version":"3753fbc1113dc511214802a2342280a8b284ab9094f6420e7aa171e868679f91","impliedFormat":1},{"version":"999ca32883495a866aa5737fe1babc764a469e4cde6ee6b136a4b9ae68853e4b","impliedFormat":1},{"version":"17f13ecb98cbc39243f2eee1f16d45cd8ec4706b03ee314f1915f1a8b42f6984","impliedFormat":1},{"version":"d6b1eba8496bdd0eed6fc8a685768fe01b2da4a0388b5fe7df558290bffcf32f","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"0bd714129fca875f7d4c477a1a392200b0bcd13fb2e80928cd334b63830ea047","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2c9037ae6cd2c52d80ceef0b3c5ffdb488627d71529cf4f63776daf11161c9a","affectsGlobalScope":true,"impliedFormat":1},{"version":"135d5cf4d345f59f1a9caadfafcd858d3d9cc68290db616cc85797224448cccc","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc238c3f81c2984751932b6aab223cd5b830e0ac6cad76389e5e9d2ffc03287d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4a07f9b76d361f572620927e5735b77d6d2101c23cdd94383eb5b706e7b36357","affectsGlobalScope":true,"impliedFormat":1},{"version":"7c4e8dc6ab834cc6baa0227e030606d29e3e8449a9f67cdf5605ea5493c4db29","affectsGlobalScope":true,"impliedFormat":1},{"version":"de7ba0fd02e06cd9a5bd4ab441ed0e122735786e67dde1e849cced1cd8b46b78","affectsGlobalScope":true,"impliedFormat":1},{"version":"6148e4e88d720a06855071c3db02069434142a8332cf9c182cda551adedf3156","affectsGlobalScope":true,"impliedFormat":1},{"version":"d63dba625b108316a40c95a4425f8d4294e0deeccfd6c7e59d819efa19e23409","affectsGlobalScope":true,"impliedFormat":1},{"version":"0568d6befee03dd435bed4fc25c4e46865b24bdcb8c563fdc21f580a2c301904","affectsGlobalScope":true,"impliedFormat":1},{"version":"30d62269b05b584741f19a5369852d5d34895aa2ac4fd948956f886d15f9cc0d","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"ffbe6d7b295306b2ba88030f65b74c107d8d99bdcf596ea99c62a02f606108b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"996fb27b15277369c68a4ba46ed138b4e9e839a02fb4ec756f7997629242fd9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"79b712591b270d4778c89706ca2cfc56ddb8c3f895840e477388f1710dc5eda9","affectsGlobalScope":true,"impliedFormat":1},{"version":"20884846cef428b992b9bd032e70a4ef88e349263f63aeddf04dda837a7dba26","affectsGlobalScope":true,"impliedFormat":1},{"version":"5fcab789c73a97cd43828ee3cc94a61264cf24d4c44472ce64ced0e0f148bdb2","affectsGlobalScope":true,"impliedFormat":1},{"version":"db59a81f070c1880ad645b2c0275022baa6a0c4f0acdc58d29d349c6efcf0903","affectsGlobalScope":true,"impliedFormat":1},{"version":"673294292640f5722b700e7d814e17aaf7d93f83a48a2c9b38f33cbc940ad8b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"d786b48f934cbca483b3c6d0a798cb43bbb4ada283e76fb22c28e53ae05b9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"142efd4ce210576f777dc34df121777be89eda476942d6d6663b03dcb53be3ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"379bc41580c2d774f82e828c70308f24a005b490c25ba34d679d84bcf05c3d9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"ed484fb2aa8a1a23d0277056ec3336e0a0b52f9b8d6a961f338a642faf43235d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ffedae1d1c2d53fdbca1c96d3c7dda544281f7d262f99b6880634f8fd8d9820","affectsGlobalScope":true,"impliedFormat":1},{"version":"83a730b125d477dd264df8ba479afab27a3dae7152b005c214ab94dc7ee44fd3","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"2577e7e800bdece2956ca027cb5c17aa359abd968eb9355760110c853f4fb9da","impliedFormat":1},{"version":"b838d4c72740eb0afd284bf7575b74c624b105eff2e8c7b4aeead57e7ac320ff","impliedFormat":1},"af732cf34bfa338fff320258775aa469d8bc3e0f25b5c82e231046ae5dccabf7","c43c2a0107a2a099fe50bcc754e90b50aa6b265ce57e464a2489798b38e4dc53","18be101c2011a0c364fc7e5ee231d6a9397808e9a095b4bb25c171adf4f23ff5","35ac5e9e25a23b55393daa079c3e7b85817598eed090c2b6b92d156fbde16c6b","d1a6a20b9ba01e597090d73d614ea6e39dc0a5424261f6980280f189c05b36b1",{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},"3044ee76859e5dae484bafae0e31525f2a3378d41a3b24c495b2e8e7b690e52c",{"version":"55793e486e9c2ad120621fcba96981896ee79991dbfe795758e6e77e75d0df54","affectsGlobalScope":true},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1}],"root":[100,101],"options":{"allowImportingTsExtensions":true,"allowJs":true,"composite":false,"declaration":true,"declarationMap":true,"emitDeclarationOnly":false,"jsx":4,"module":200,"noEmitOnError":true,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noPropertyAccessFromIndexSignature":false,"noUncheckedIndexedAccess":true,"noUnusedLocals":false,"noUnusedParameters":false,"skipLibCheck":true,"strict":true,"target":99,"verbatimModuleSyntax":true},"referencedMap":[[99,1],[102,1],[92,2],[93,1],[100,3],[94,1],[96,4],[97,4],[98,5]],"affectedFilesPendingEmit":[[100,49]],"version":"6.0.2"}
|
package/src/dev-middleware.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createCmsCore, createNodeFs } from '@nuasite/cms-core'
|
|
1
2
|
import fs from 'node:fs/promises'
|
|
2
3
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
3
4
|
import path from 'node:path'
|
|
@@ -118,7 +119,15 @@ export function createDevMiddleware(
|
|
|
118
119
|
|
|
119
120
|
// CMS API endpoints (local dev server backend)
|
|
120
121
|
if (options.enableCmsApi) {
|
|
121
|
-
|
|
122
|
+
// One cms-core instance per project root. Structural routes (entry/page/redirect
|
|
123
|
+
// CRUD, media) delegate to this brain over a node:fs adapter; the media adapter
|
|
124
|
+
// and component dirs mirror the selection @nuasite/cms makes for the dev server.
|
|
125
|
+
const cmsFs = createNodeFs(getProjectRoot())
|
|
126
|
+
const core = createCmsCore(cmsFs, {
|
|
127
|
+
contentDir: config.contentDir,
|
|
128
|
+
media: options.mediaAdapter,
|
|
129
|
+
componentDirs: config.componentDirs,
|
|
130
|
+
})
|
|
122
131
|
|
|
123
132
|
server.middlewares.use((req, res, next) => {
|
|
124
133
|
const url = req.url || ''
|
|
@@ -136,6 +145,8 @@ export function createDevMiddleware(
|
|
|
136
145
|
res,
|
|
137
146
|
route,
|
|
138
147
|
manifestWriter,
|
|
148
|
+
core,
|
|
149
|
+
fs: cmsFs,
|
|
139
150
|
contentDir: config.contentDir,
|
|
140
151
|
mediaAdapter: options.mediaAdapter,
|
|
141
152
|
maxUploadSize: options.maxUploadSize,
|
|
@@ -22,7 +22,10 @@ const confirmDeleteSlug = signal<string | null>(null)
|
|
|
22
22
|
const EMPTY_ENTRIES: never[] = []
|
|
23
23
|
|
|
24
24
|
export function CollectionsBrowser() {
|
|
25
|
-
|
|
25
|
+
// Collection management (browse/list/open entries) can be owned by the host
|
|
26
|
+
// app; when disabled, the in-preview browser is hidden entirely.
|
|
27
|
+
const collectionManagementEnabled = config.value.features?.collectionManagement !== false
|
|
28
|
+
const visible = collectionManagementEnabled && isCollectionsBrowserOpen.value
|
|
26
29
|
const selected = selectedBrowserCollection.value
|
|
27
30
|
|
|
28
31
|
const collectionDefinitions = manifest.value.collectionDefinitions ?? {}
|
|
@@ -95,6 +95,11 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
95
95
|
})
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// When collection management is disabled (e.g. it lives in the host app's
|
|
99
|
+
// Collections tab), hide the in-preview collection browser entry points.
|
|
100
|
+
// Inline editing of the current page's content ("Edit Content") is kept.
|
|
101
|
+
const collectionManagementEnabled = signals.config.value.features?.collectionManagement !== false
|
|
102
|
+
|
|
98
103
|
if (collectionDefinitions) {
|
|
99
104
|
const entries = Object.entries(collectionDefinitions)
|
|
100
105
|
if (entries.length > 0) {
|
|
@@ -111,12 +116,14 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
111
116
|
ordered.push([name, def, false])
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
const contentItems: MenuItem[] =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
const contentItems: MenuItem[] = collectionManagementEnabled
|
|
120
|
+
? ordered.map(([name, def, indented]) => ({
|
|
121
|
+
label: def.label,
|
|
122
|
+
icon: <GridIcon />,
|
|
123
|
+
indented,
|
|
124
|
+
onClick: () => callbacks.onOpenCollection?.(name),
|
|
125
|
+
}))
|
|
126
|
+
: []
|
|
120
127
|
|
|
121
128
|
if (currentPageCollection && callbacks.onEditContent) {
|
|
122
129
|
contentItems.unshift({
|
|
@@ -134,11 +141,13 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
134
141
|
})
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
if (contentItems.length > 0) {
|
|
145
|
+
menuSections.push({
|
|
146
|
+
label: 'Content',
|
|
147
|
+
icon: <GridIcon />,
|
|
148
|
+
items: contentItems,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
142
151
|
}
|
|
143
152
|
}
|
|
144
153
|
|
package/src/editor/signals.ts
CHANGED
|
@@ -1470,8 +1470,10 @@ export function setConfig(newConfig: CmsConfig): void {
|
|
|
1470
1470
|
|
|
1471
1471
|
export function setFeatures(features: CmsConfig['features']): void {
|
|
1472
1472
|
const current = config.value.features
|
|
1473
|
-
|
|
1474
|
-
|
|
1473
|
+
const next = { ...current, ...features }
|
|
1474
|
+
// Bail out when nothing actually changes, to avoid re-triggering effects.
|
|
1475
|
+
if (current?.selectElement === next.selectElement && current?.collectionManagement === next.collectionManagement) return
|
|
1476
|
+
config.value = { ...config.value, features: next }
|
|
1475
1477
|
}
|
|
1476
1478
|
|
|
1477
1479
|
// ============================================================================
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
+
import type { CmsCore, CmsFileSystem } from '@nuasite/cms-core'
|
|
2
|
+
import { listProjectImages } from '@nuasite/cms-core'
|
|
1
3
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
4
|
import path from 'node:path'
|
|
3
5
|
import { scanCollections } from '../collection-scanner'
|
|
4
6
|
import { getProjectRoot } from '../config'
|
|
5
7
|
import { expectedDeletions } from '../dev-middleware'
|
|
6
8
|
import type { ManifestWriter } from '../manifest-writer'
|
|
7
|
-
import { listProjectImages } from '../media/project-images'
|
|
8
9
|
import type { MediaStorageAdapter } from '../media/types'
|
|
9
10
|
import { handleAddArrayItem, handleRemoveArrayItem } from './array-ops'
|
|
10
11
|
import { tryAstroImageUpload } from './astro-image-upload'
|
|
11
12
|
import { handleInsertComponent, handleRemoveComponent } from './component-ops'
|
|
12
|
-
import {
|
|
13
|
-
import { handleCheckSlugExists, handleCreatePage, handleDeletePage, handleDuplicatePage, handleGetLayouts } from './page-ops'
|
|
14
|
-
import { handleAddRedirect, handleDeleteRedirect, handleGetRedirects, handleUpdateRedirect } from './redirect-ops'
|
|
13
|
+
import { handleCheckSlugExists } from './page-ops'
|
|
15
14
|
import { BodyTooLargeError, parseJsonBody, parseMultipartFile, readBody, sendError, sendJson } from './request-utils'
|
|
16
15
|
import { handleUpdate } from './source-writer'
|
|
17
16
|
|
|
@@ -20,6 +19,10 @@ export interface RouteContext {
|
|
|
20
19
|
res: ServerResponse
|
|
21
20
|
route: string
|
|
22
21
|
manifestWriter: ManifestWriter
|
|
22
|
+
/** The framework-agnostic brain. Structural routes delegate here. */
|
|
23
|
+
core: CmsCore
|
|
24
|
+
/** Raw FileSystem port (for the few helpers that scan the project directly). */
|
|
25
|
+
fs: CmsFileSystem
|
|
23
26
|
contentDir: string
|
|
24
27
|
mediaAdapter?: MediaStorageAdapter
|
|
25
28
|
maxUploadSize: number
|
|
@@ -39,6 +42,37 @@ function getQuery(ctx: RouteContext): URLSearchParams {
|
|
|
39
42
|
return new URL(ctx.req.url!, `http://${ctx.req.headers.host}`).searchParams
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Derive `{ collection, slug }` from a root-relative content-entry `filePath`.
|
|
47
|
+
*
|
|
48
|
+
* The dev API is addressed by `filePath` (e.g. `src/content/blog/hello.md`),
|
|
49
|
+
* while cms-core is addressed by `{ collection, slug }`. The collection is the
|
|
50
|
+
* first path segment under `contentDir`; the slug is the remainder with the
|
|
51
|
+
* extension stripped (and a trailing `/index` collapsed for index-layout
|
|
52
|
+
* entries). cms-core's path resolution is the exact inverse: a flat `<slug>.<ext>`
|
|
53
|
+
* resolves first, an index `<slug>/index.{md,mdx}` after — so the derived pair
|
|
54
|
+
* resolves back to the same file.
|
|
55
|
+
*/
|
|
56
|
+
function filePathToEntry(contentDir: string, filePath: string): { collection: string; slug: string } | null {
|
|
57
|
+
const normalized = filePath.replace(/^\/+/, '')
|
|
58
|
+
const prefix = `${contentDir.replace(/\/+$/, '')}/`
|
|
59
|
+
if (!normalized.startsWith(prefix)) return null
|
|
60
|
+
|
|
61
|
+
const rel = normalized.slice(prefix.length)
|
|
62
|
+
const firstSlash = rel.indexOf('/')
|
|
63
|
+
if (firstSlash < 0) return null
|
|
64
|
+
|
|
65
|
+
const collection = rel.slice(0, firstSlash)
|
|
66
|
+
const entryPath = rel.slice(firstSlash + 1)
|
|
67
|
+
if (!collection || !entryPath) return null
|
|
68
|
+
|
|
69
|
+
const withoutExt = entryPath.replace(/\.(md|mdx|json|yaml|yml)$/, '')
|
|
70
|
+
const slug = withoutExt.replace(/\/index$/, '')
|
|
71
|
+
if (!slug) return null
|
|
72
|
+
|
|
73
|
+
return { collection, slug }
|
|
74
|
+
}
|
|
75
|
+
|
|
42
76
|
// -- Route helper factories --
|
|
43
77
|
|
|
44
78
|
/** POST route: parse JSON body → handler(body, manifestWriter) → sendJson */
|
|
@@ -49,19 +83,19 @@ function post<T>(route: string, handler: (body: T, mw: ManifestWriter) => Promis
|
|
|
49
83
|
}]
|
|
50
84
|
}
|
|
51
85
|
|
|
52
|
-
/** POST route: parse JSON body → handler(body) → sendJson with success-based status */
|
|
53
|
-
function
|
|
54
|
-
return [`POST:${route}`, async ({ req, res }) => {
|
|
86
|
+
/** POST route through cms-core: parse JSON body → handler(body, core) → sendJson with success-based status */
|
|
87
|
+
function postCore<T>(route: string, handler: (body: T, core: CmsCore) => Promise<{ success: boolean }>): [string, RouteHandler] {
|
|
88
|
+
return [`POST:${route}`, async ({ req, res, core }) => {
|
|
55
89
|
const body = await parseJsonBody<T>(req)
|
|
56
|
-
const result = await handler(body)
|
|
90
|
+
const result = await handler(body, core)
|
|
57
91
|
sendJson(res, result, result.success ? 200 : 400)
|
|
58
92
|
}]
|
|
59
93
|
}
|
|
60
94
|
|
|
61
|
-
/** GET route: handler() → sendJson */
|
|
62
|
-
function
|
|
63
|
-
return [`GET:${route}`, async ({ res }) => {
|
|
64
|
-
sendJson(res, await handler())
|
|
95
|
+
/** GET route through cms-core: handler(core) → sendJson */
|
|
96
|
+
function getCore(route: string, handler: (core: CmsCore) => Promise<unknown>): [string, RouteHandler] {
|
|
97
|
+
return [`GET:${route}`, async ({ res, core }) => {
|
|
98
|
+
sendJson(res, await handler(core))
|
|
65
99
|
}]
|
|
66
100
|
}
|
|
67
101
|
|
|
@@ -82,54 +116,150 @@ const ALLOWED_UPLOAD_TYPES = new Set([
|
|
|
82
116
|
'application/pdf',
|
|
83
117
|
])
|
|
84
118
|
|
|
119
|
+
/** Frontmatter shape the create route enriches with title/date for markdown entries. */
|
|
120
|
+
interface CreateMarkdownBody {
|
|
121
|
+
collection: string
|
|
122
|
+
title: string
|
|
123
|
+
slug: string
|
|
124
|
+
frontmatter?: Record<string, unknown>
|
|
125
|
+
content?: string
|
|
126
|
+
fileExtension?: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface UpdateMarkdownBody {
|
|
130
|
+
filePath: string
|
|
131
|
+
frontmatter?: Record<string, unknown>
|
|
132
|
+
content?: string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface DeleteMarkdownBody {
|
|
136
|
+
filePath: string
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface RenameMarkdownBody {
|
|
140
|
+
filePath: string
|
|
141
|
+
newSlug: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface DuplicatePageBody {
|
|
145
|
+
sourcePagePath: string
|
|
146
|
+
slug: string
|
|
147
|
+
title?: string
|
|
148
|
+
layoutPath?: string
|
|
149
|
+
createRedirect?: boolean
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface DeletePageBody {
|
|
153
|
+
pagePath: string
|
|
154
|
+
createRedirect?: boolean
|
|
155
|
+
redirectTo?: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const DATA_EXTENSIONS = new Set(['json', 'yaml', 'yml'])
|
|
159
|
+
|
|
85
160
|
/** O(1) route lookup map: "METHOD:route" → handler */
|
|
86
161
|
const routeMap = new Map<string, RouteHandler>([
|
|
87
|
-
// Source editing
|
|
162
|
+
// Source editing — manifest-coupled, stays in @nuasite/cms
|
|
88
163
|
post('update', (body: Parameters<typeof handleUpdate>[0], mw) => handleUpdate(body, mw)),
|
|
89
164
|
post('insert-component', (body: Parameters<typeof handleInsertComponent>[0], mw) => handleInsertComponent(body, mw)),
|
|
90
165
|
post('remove-component', (body: Parameters<typeof handleRemoveComponent>[0], mw) => handleRemoveComponent(body, mw)),
|
|
91
166
|
post('add-array-item', (body: Parameters<typeof handleAddArrayItem>[0], mw) => handleAddArrayItem(body, mw)),
|
|
92
167
|
post('remove-array-item', (body: Parameters<typeof handleRemoveArrayItem>[0], mw) => handleRemoveArrayItem(body, mw)),
|
|
93
168
|
|
|
94
|
-
// Markdown CRUD
|
|
95
|
-
custom('GET', 'markdown/content', async ({ req, res }) => {
|
|
169
|
+
// Markdown / entry CRUD — structural, delegated to cms-core
|
|
170
|
+
custom('GET', 'markdown/content', async ({ req, res, core, contentDir }) => {
|
|
96
171
|
const filePath = getQuery({ req } as RouteContext).get('filePath')
|
|
97
172
|
if (!filePath) {
|
|
98
173
|
sendError(res, 'filePath query parameter required')
|
|
99
174
|
return
|
|
100
175
|
}
|
|
101
|
-
const
|
|
176
|
+
const entry = filePathToEntry(contentDir, filePath)
|
|
177
|
+
const result = entry ? await core.getEntry(entry.collection, entry.slug) : null
|
|
102
178
|
if (!result) {
|
|
103
179
|
sendError(res, 'File not found', 404)
|
|
104
180
|
return
|
|
105
181
|
}
|
|
106
|
-
sendJson(res, result)
|
|
182
|
+
sendJson(res, { content: result.content, frontmatter: result.frontmatter, filePath })
|
|
183
|
+
}),
|
|
184
|
+
custom('POST', 'markdown/update', async ({ req, res, core, contentDir }) => {
|
|
185
|
+
const body = await parseJsonBody<UpdateMarkdownBody>(req)
|
|
186
|
+
const entry = filePathToEntry(contentDir, body.filePath)
|
|
187
|
+
if (!entry) {
|
|
188
|
+
sendJson(res, { success: false, error: `Invalid content path: ${body.filePath}` })
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
// cms-core's updateEntry resolves component definitions internally for MDX imports.
|
|
192
|
+
const result = await core.updateEntry({
|
|
193
|
+
collection: entry.collection,
|
|
194
|
+
slug: entry.slug,
|
|
195
|
+
frontmatter: body.frontmatter,
|
|
196
|
+
body: body.content,
|
|
197
|
+
})
|
|
198
|
+
sendJson(res, { success: result.success, ...(result.error ? { error: result.error } : {}) })
|
|
107
199
|
}),
|
|
108
|
-
custom('POST', 'markdown/
|
|
109
|
-
const body = await parseJsonBody<
|
|
110
|
-
const
|
|
111
|
-
|
|
200
|
+
custom('POST', 'markdown/rename', async ({ req, res, core, contentDir }) => {
|
|
201
|
+
const body = await parseJsonBody<RenameMarkdownBody>(req)
|
|
202
|
+
const entry = filePathToEntry(contentDir, body.filePath)
|
|
203
|
+
if (!entry) {
|
|
204
|
+
sendJson(res, { success: false, error: `Invalid content path: ${body.filePath}` })
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
const result = await core.renameEntry(entry.collection, entry.slug, body.newSlug)
|
|
208
|
+
if (!result.success) {
|
|
209
|
+
sendJson(res, { success: false, error: result.error })
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
const newSlug = result.sourcePath ? lastSlug(result.sourcePath) : undefined
|
|
213
|
+
sendJson(res, { success: true, newFilePath: result.sourcePath, newSlug })
|
|
112
214
|
}),
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const
|
|
215
|
+
custom('POST', 'markdown/create', async ({ req, res, core, manifestWriter, contentDir }) => {
|
|
216
|
+
const body = await parseJsonBody<CreateMarkdownBody>(req)
|
|
217
|
+
const ext = body.fileExtension ?? 'md'
|
|
218
|
+
const isData = DATA_EXTENSIONS.has(ext)
|
|
219
|
+
// Markdown entries get title + an ISO date injected; data entries take frontmatter verbatim.
|
|
220
|
+
const frontmatter = isData
|
|
221
|
+
? { ...(body.frontmatter ?? {}) }
|
|
222
|
+
: { title: body.title, date: new Date().toISOString().split('T')[0]!, ...(body.frontmatter ?? {}) }
|
|
223
|
+
|
|
224
|
+
const result = await core.createEntry({
|
|
225
|
+
collection: body.collection,
|
|
226
|
+
slug: body.slug || body.title,
|
|
227
|
+
frontmatter,
|
|
228
|
+
body: body.content,
|
|
229
|
+
fileExtension: body.fileExtension,
|
|
230
|
+
})
|
|
231
|
+
|
|
117
232
|
if (result.success) {
|
|
118
233
|
manifestWriter.setCollectionDefinitions(await scanCollections(contentDir))
|
|
119
234
|
}
|
|
120
|
-
|
|
235
|
+
const slug = result.sourcePath ? lastSlug(result.sourcePath) : undefined
|
|
236
|
+
sendJson(
|
|
237
|
+
res,
|
|
238
|
+
{
|
|
239
|
+
success: result.success,
|
|
240
|
+
...(result.sourcePath ? { filePath: result.sourcePath } : {}),
|
|
241
|
+
...(slug ? { slug } : {}),
|
|
242
|
+
...(result.error ? { error: result.error } : {}),
|
|
243
|
+
},
|
|
244
|
+
result.success ? 200 : 400,
|
|
245
|
+
)
|
|
121
246
|
}),
|
|
122
|
-
custom('POST', 'markdown/delete', async ({ req, res, manifestWriter, contentDir }) => {
|
|
123
|
-
const body = await parseJsonBody<
|
|
124
|
-
const
|
|
247
|
+
custom('POST', 'markdown/delete', async ({ req, res, core, manifestWriter, contentDir }) => {
|
|
248
|
+
const body = await parseJsonBody<DeleteMarkdownBody>(req)
|
|
249
|
+
const entry = filePathToEntry(contentDir, body.filePath)
|
|
250
|
+
if (!entry) {
|
|
251
|
+
sendJson(res, { success: false, error: `Invalid content path: ${body.filePath}` }, 400)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
const fullPath = path.resolve(getProjectRoot(), body.filePath.replace(/^\//, ''))
|
|
125
255
|
expectedDeletions.add(fullPath)
|
|
126
|
-
const result = await
|
|
256
|
+
const result = await core.deleteEntry(entry.collection, entry.slug)
|
|
127
257
|
if (result.success) {
|
|
128
258
|
manifestWriter.setCollectionDefinitions(await scanCollections(contentDir))
|
|
129
259
|
} else {
|
|
130
260
|
expectedDeletions.delete(fullPath)
|
|
131
261
|
}
|
|
132
|
-
sendJson(res, result, result.success ? 200 : 400)
|
|
262
|
+
sendJson(res, { success: result.success, ...(result.error ? { error: result.error } : {}) }, result.success ? 200 : 400)
|
|
133
263
|
}),
|
|
134
264
|
|
|
135
265
|
// Media
|
|
@@ -143,7 +273,7 @@ const routeMap = new Map<string, RouteHandler>([
|
|
|
143
273
|
}),
|
|
144
274
|
custom('GET', 'media/project-images', async (ctx) => {
|
|
145
275
|
const excludeDir = ctx.mediaAdapter?.staticFiles?.dir
|
|
146
|
-
const items = await listProjectImages({ excludeDir })
|
|
276
|
+
const items = await listProjectImages(ctx.fs, { excludeDir })
|
|
147
277
|
sendJson(ctx.res, { items })
|
|
148
278
|
}),
|
|
149
279
|
custom('POST', 'media/upload', async (ctx) => {
|
|
@@ -225,24 +355,24 @@ const routeMap = new Map<string, RouteHandler>([
|
|
|
225
355
|
sendJson(ctx.res, result, result.success ? 200 : 400)
|
|
226
356
|
}),
|
|
227
357
|
|
|
228
|
-
// Page operations
|
|
229
|
-
|
|
230
|
-
custom('POST', 'page/duplicate', async ({ req, res }) => {
|
|
231
|
-
const body = await parseJsonBody<
|
|
232
|
-
const result = await
|
|
358
|
+
// Page operations — structural, delegated to cms-core
|
|
359
|
+
postCore('page/create', (body: Parameters<CmsCore['createPage']>[0], core) => core.createPage(body)),
|
|
360
|
+
custom('POST', 'page/duplicate', async ({ req, res, core }) => {
|
|
361
|
+
const body = await parseJsonBody<DuplicatePageBody>(req)
|
|
362
|
+
const result = await core.duplicatePage(body)
|
|
233
363
|
if (result.success && body.createRedirect) {
|
|
234
|
-
await
|
|
364
|
+
await core.addRedirect({ source: body.sourcePagePath, destination: result.url!, statusCode: 307 })
|
|
235
365
|
}
|
|
236
366
|
sendJson(res, result, result.success ? 200 : 400)
|
|
237
367
|
}),
|
|
238
|
-
custom('POST', 'page/delete', async ({ req, res }) => {
|
|
239
|
-
const body = await parseJsonBody<
|
|
240
|
-
const result = await
|
|
368
|
+
custom('POST', 'page/delete', async ({ req, res, core }) => {
|
|
369
|
+
const body = await parseJsonBody<DeletePageBody>(req)
|
|
370
|
+
const result = await core.deletePage(body)
|
|
241
371
|
if (result.success && result.filePath) {
|
|
242
372
|
expectedDeletions.add(path.resolve(getProjectRoot(), result.filePath))
|
|
243
373
|
}
|
|
244
374
|
if (result.success && body.createRedirect && body.redirectTo) {
|
|
245
|
-
await
|
|
375
|
+
await core.addRedirect({ source: body.pagePath, destination: body.redirectTo, statusCode: 307 })
|
|
246
376
|
}
|
|
247
377
|
sendJson(res, result, result.success ? 200 : 400)
|
|
248
378
|
}),
|
|
@@ -254,18 +384,32 @@ const routeMap = new Map<string, RouteHandler>([
|
|
|
254
384
|
}
|
|
255
385
|
sendJson(ctx.res, await handleCheckSlugExists(slug))
|
|
256
386
|
}),
|
|
257
|
-
|
|
387
|
+
getCore('page/layouts', async (core) => ({ layouts: await core.getLayouts() })),
|
|
258
388
|
|
|
259
|
-
// Redirects
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
389
|
+
// Redirects — structural, delegated to cms-core
|
|
390
|
+
getCore('redirects', (core) => core.listRedirects()),
|
|
391
|
+
postCore('redirects/add', (body: Parameters<CmsCore['addRedirect']>[0], core) => core.addRedirect(body)),
|
|
392
|
+
postCore('redirects/update', (body: Parameters<CmsCore['updateRedirect']>[0], core) => core.updateRedirect(body)),
|
|
393
|
+
postCore('redirects/delete', (body: Parameters<CmsCore['deleteRedirect']>[0], core) => core.deleteRedirect(body)),
|
|
264
394
|
|
|
265
395
|
// Deployment
|
|
266
396
|
get('deployment/status', async () => ({ currentDeployment: null, pendingCount: 0, deploymentEnabled: false })),
|
|
267
397
|
])
|
|
268
398
|
|
|
399
|
+
/** GET route returning a fixed/static payload (no cms-core needed). */
|
|
400
|
+
function get(route: string, handler: () => Promise<unknown>): [string, RouteHandler] {
|
|
401
|
+
return [`GET:${route}`, async ({ res }) => {
|
|
402
|
+
sendJson(res, await handler())
|
|
403
|
+
}]
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Last path segment of a root-relative source path, with the extension and a trailing `/index` stripped. */
|
|
407
|
+
function lastSlug(sourcePath: string): string {
|
|
408
|
+
const withoutExt = sourcePath.replace(/\.(md|mdx|json|yaml|yml)$/, '').replace(/\/index$/, '')
|
|
409
|
+
const slash = withoutExt.lastIndexOf('/')
|
|
410
|
+
return slash >= 0 ? withoutExt.slice(slash + 1) : withoutExt
|
|
411
|
+
}
|
|
412
|
+
|
|
269
413
|
export async function handleCmsApiRoute(ctx: RouteContext): Promise<void> {
|
|
270
414
|
const { req, res, route } = ctx
|
|
271
415
|
|