@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/src/handlers/page-ops.ts
CHANGED
|
@@ -1,103 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
|
-
import
|
|
3
|
-
import { getProjectRoot } from '../config'
|
|
4
|
-
import type { CreatePageRequest, DeletePageRequest, DuplicatePageRequest, LayoutInfo, PageOperationResponse } from '../types'
|
|
5
|
-
import { escapeHtml, isNodeError, resolveAndValidatePath, slugify } from '../utils'
|
|
2
|
+
import { resolveAndValidatePath, slugify } from '../utils'
|
|
6
3
|
|
|
7
4
|
const PAGE_EXTENSIONS = ['.astro', '.md', '.mdx']
|
|
8
5
|
|
|
9
|
-
export async function handleCreatePage(request: CreatePageRequest): Promise<PageOperationResponse> {
|
|
10
|
-
const { title, slug } = request
|
|
11
|
-
const normalizedSlug = slugify(slug || title)
|
|
12
|
-
|
|
13
|
-
if (!normalizedSlug) {
|
|
14
|
-
return { success: false, error: 'Could not generate a valid slug from the provided title/slug' }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const filePath = `src/pages/${normalizedSlug}.astro`
|
|
18
|
-
const fullPath = resolveAndValidatePath(filePath)
|
|
19
|
-
|
|
20
|
-
const layoutImport = await resolveLayoutImport(request.layoutPath)
|
|
21
|
-
const content = generatePageContent(title, layoutImport)
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
await fs.mkdir(path.dirname(fullPath), { recursive: true })
|
|
25
|
-
// 'wx' flag atomically fails if file exists — no pre-check needed
|
|
26
|
-
await fs.writeFile(fullPath, content, { encoding: 'utf-8', flag: 'wx' })
|
|
27
|
-
|
|
28
|
-
const url = normalizedSlug === 'index' ? '/' : `/${normalizedSlug}`
|
|
29
|
-
return { success: true, filePath, slug: normalizedSlug, url }
|
|
30
|
-
} catch (error) {
|
|
31
|
-
if (isNodeError(error, 'EEXIST')) {
|
|
32
|
-
return { success: false, error: `Page already exists: ${filePath}` }
|
|
33
|
-
}
|
|
34
|
-
return { success: false, error: errorMessage(error) }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function handleDuplicatePage(request: DuplicatePageRequest): Promise<PageOperationResponse> {
|
|
39
|
-
const { sourcePagePath, slug, title } = request
|
|
40
|
-
const normalizedSlug = slugify(slug)
|
|
41
|
-
|
|
42
|
-
if (!normalizedSlug) {
|
|
43
|
-
return { success: false, error: 'Could not generate a valid slug' }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const sourceFile = await findPageFile(sourcePagePath)
|
|
47
|
-
if (!sourceFile) {
|
|
48
|
-
return { success: false, error: `Source page not found: ${sourcePagePath}` }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let content: string
|
|
52
|
-
try {
|
|
53
|
-
content = await fs.readFile(resolveAndValidatePath(sourceFile), 'utf-8')
|
|
54
|
-
} catch {
|
|
55
|
-
return { success: false, error: `Could not read source file: ${sourceFile}` }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (title) {
|
|
59
|
-
content = replacePageTitle(content, title)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const newFilePath = `src/pages/${normalizedSlug}.astro`
|
|
63
|
-
const newFullPath = resolveAndValidatePath(newFilePath)
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
await fs.mkdir(path.dirname(newFullPath), { recursive: true })
|
|
67
|
-
await fs.writeFile(newFullPath, content, { encoding: 'utf-8', flag: 'wx' })
|
|
68
|
-
|
|
69
|
-
const url = normalizedSlug === 'index' ? '/' : `/${normalizedSlug}`
|
|
70
|
-
return { success: true, filePath: newFilePath, slug: normalizedSlug, url }
|
|
71
|
-
} catch (error) {
|
|
72
|
-
if (isNodeError(error, 'EEXIST')) {
|
|
73
|
-
return { success: false, error: `Page already exists: ${newFilePath}` }
|
|
74
|
-
}
|
|
75
|
-
return { success: false, error: errorMessage(error) }
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function handleDeletePage(request: DeletePageRequest): Promise<PageOperationResponse> {
|
|
80
|
-
const { pagePath } = request
|
|
81
|
-
|
|
82
|
-
const pageFile = await findPageFile(pagePath)
|
|
83
|
-
if (!pageFile) {
|
|
84
|
-
return { success: false, error: `Page not found: ${pagePath}` }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
// No pre-check — just unlink and handle ENOENT
|
|
89
|
-
await fs.unlink(resolveAndValidatePath(pageFile))
|
|
90
|
-
return { success: true, filePath: pageFile, url: pagePath }
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if (isNodeError(error, 'ENOENT')) {
|
|
93
|
-
return { success: false, error: `File not found: ${pageFile}` }
|
|
94
|
-
}
|
|
95
|
-
return { success: false, error: errorMessage(error) }
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
6
|
/**
|
|
100
|
-
*
|
|
7
|
+
* Check whether a page slug is already taken. Page create/duplicate/delete and
|
|
8
|
+
* layout listing now live in `@nuasite/cms-core`; this slug check stays here
|
|
9
|
+
* because it is not part of the cms-core structural interface.
|
|
101
10
|
*/
|
|
102
11
|
export async function handleCheckSlugExists(slug: string): Promise<{ exists: boolean; filePath?: string }> {
|
|
103
12
|
const normalizedSlug = slugify(slug)
|
|
@@ -107,35 +16,8 @@ export async function handleCheckSlugExists(slug: string): Promise<{ exists: boo
|
|
|
107
16
|
return found ? { exists: true, filePath: found } : { exists: false }
|
|
108
17
|
}
|
|
109
18
|
|
|
110
|
-
export async function handleGetLayouts(): Promise<LayoutInfo[]> {
|
|
111
|
-
const layoutsDir = path.join(getProjectRoot(), 'src', 'layouts')
|
|
112
|
-
|
|
113
|
-
let entries
|
|
114
|
-
try {
|
|
115
|
-
entries = await fs.readdir(layoutsDir, { withFileTypes: true })
|
|
116
|
-
} catch {
|
|
117
|
-
return []
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const layouts: LayoutInfo[] = []
|
|
121
|
-
for (const entry of entries) {
|
|
122
|
-
if (entry.isFile() && entry.name.endsWith('.astro')) {
|
|
123
|
-
layouts.push({
|
|
124
|
-
name: path.basename(entry.name, '.astro'),
|
|
125
|
-
path: `src/layouts/${entry.name}`,
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return layouts.sort((a, b) => a.name.localeCompare(b.name))
|
|
131
|
-
}
|
|
132
|
-
|
|
133
19
|
// --- Internal helpers ---
|
|
134
20
|
|
|
135
|
-
function errorMessage(error: unknown): string {
|
|
136
|
-
return error instanceof Error ? error.message : String(error)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
21
|
async function fileExists(fullPath: string): Promise<boolean> {
|
|
140
22
|
try {
|
|
141
23
|
await fs.access(fullPath)
|
|
@@ -160,70 +42,3 @@ async function findPageFile(pagePath: string): Promise<string | null> {
|
|
|
160
42
|
|
|
161
43
|
return null
|
|
162
44
|
}
|
|
163
|
-
|
|
164
|
-
async function resolveLayoutImport(layoutPath?: string): Promise<{ importPath: string; componentName: string } | null> {
|
|
165
|
-
if (layoutPath) {
|
|
166
|
-
const name = path.basename(layoutPath, '.astro')
|
|
167
|
-
const importPath = `../${layoutPath.replace(/^src\//, '')}`
|
|
168
|
-
return { importPath, componentName: pascalCase(name) }
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const layouts = await handleGetLayouts()
|
|
172
|
-
if (layouts.length === 0) return null
|
|
173
|
-
|
|
174
|
-
const layout = layouts[0]!
|
|
175
|
-
const importPath = `../${layout.path.replace(/^src\//, '')}`
|
|
176
|
-
return { importPath, componentName: pascalCase(layout.name) }
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function pascalCase(name: string): string {
|
|
180
|
-
return name.replace(/(^|[-_])(\w)/g, (_, _sep, char) => char.toUpperCase())
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function generatePageContent(
|
|
184
|
-
title: string,
|
|
185
|
-
layoutImport: { importPath: string; componentName: string } | null,
|
|
186
|
-
): string {
|
|
187
|
-
const escapedTitle = title.replace(/'/g, "\\'").replace(/`/g, '\\`')
|
|
188
|
-
const htmlTitle = escapeHtml(title)
|
|
189
|
-
|
|
190
|
-
if (layoutImport) {
|
|
191
|
-
const { importPath, componentName } = layoutImport
|
|
192
|
-
return `---
|
|
193
|
-
import ${componentName} from '${importPath}'
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
<${componentName} title="${escapedTitle}" description="">
|
|
197
|
-
\t<main>
|
|
198
|
-
\t\t<h1>${htmlTitle}</h1>
|
|
199
|
-
\t</main>
|
|
200
|
-
</${componentName}>
|
|
201
|
-
`
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return `---
|
|
205
|
-
|
|
206
|
-
---
|
|
207
|
-
|
|
208
|
-
<html lang="en">
|
|
209
|
-
\t<head>
|
|
210
|
-
\t\t<meta charset="utf-8" />
|
|
211
|
-
\t\t<meta name="viewport" content="width=device-width" />
|
|
212
|
-
\t\t<title>${escapedTitle}</title>
|
|
213
|
-
\t</head>
|
|
214
|
-
\t<body>
|
|
215
|
-
\t\t<main>
|
|
216
|
-
\t\t\t<h1>${htmlTitle}</h1>
|
|
217
|
-
\t\t</main>
|
|
218
|
-
\t</body>
|
|
219
|
-
</html>
|
|
220
|
-
`
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function replacePageTitle(content: string, newTitle: string): string {
|
|
224
|
-
let result = content
|
|
225
|
-
result = result.replace(/(title\s*=\s*")([^"]*)(")/, `$1${newTitle}$3`)
|
|
226
|
-
result = result.replace(/(<title>)([^<]*)(<\/title>)/, `$1${newTitle}$3`)
|
|
227
|
-
result = result.replace(/(<h1[^>]*>)([^<]*)(<\/h1>)/, `$1${escapeHtml(newTitle)}$3`)
|
|
228
|
-
return result
|
|
229
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import type { AstroIntegration } from 'astro'
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
3
|
import fs from 'node:fs/promises'
|
|
4
4
|
import { dirname, join } from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
|
+
import { createLocalStorageAdapter } from '@nuasite/cms-core'
|
|
7
8
|
import { processBuildOutput } from './build-processor'
|
|
8
9
|
import { scanCollections } from './collection-scanner'
|
|
9
10
|
import { ComponentRegistry } from './component-registry'
|
|
10
11
|
import { resetProjectRoot } from './config'
|
|
11
12
|
import { createDevMiddleware } from './dev-middleware'
|
|
12
13
|
import { getErrorCollector, resetErrorCollector } from './error-collector'
|
|
14
|
+
import { ADMIN_ROUTE, createLocalAdminMiddleware } from './local-admin'
|
|
13
15
|
import { ManifestWriter } from './manifest-writer'
|
|
14
|
-
import { createLocalStorageAdapter } from './media/local'
|
|
15
16
|
import type { MediaStorageAdapter } from './media/types'
|
|
17
|
+
import { type CmsMode, resolveCmsMode } from './mode'
|
|
16
18
|
import { rehypeCmsMarker } from './rehype-cms-marker'
|
|
17
19
|
import type { CmsFeatures, CmsMarkerOptions, ComponentDefinition } from './types'
|
|
18
20
|
import { createPublicStaticFileChecker } from './utils'
|
|
@@ -20,8 +22,18 @@ import { createVitePlugin } from './vite-plugin'
|
|
|
20
22
|
|
|
21
23
|
export interface NuaCmsOptions extends CmsMarkerOptions {
|
|
22
24
|
/**
|
|
23
|
-
* URL to the CMS editor script.
|
|
24
|
-
*
|
|
25
|
+
* URL to the CMS editor (inline visual-editing widget) script.
|
|
26
|
+
*
|
|
27
|
+
* Resolution order in dev:
|
|
28
|
+
* 1. this `src`, if set;
|
|
29
|
+
* 2. the `NUA_CMS_EDITOR_SRC` environment variable (lets the host runtime point
|
|
30
|
+
* dev-preview at a specific CDN/local editor without a config change);
|
|
31
|
+
* 3. the public CDN editor (`DEFAULT_CDN_EDITOR_SRC`) when a pre-built editor
|
|
32
|
+
* bundle ships with the package (the npm-installed case — e.g. a generated
|
|
33
|
+
* site's dev-preview), so the inline editor updates via a CDN push,
|
|
34
|
+
* independent of the site's `@nuasite/cms` version (matching hosting);
|
|
35
|
+
* 4. the bundled editor served from the dev server, used only in the monorepo
|
|
36
|
+
* (no pre-built bundle) so editor source changes hot-reload.
|
|
25
37
|
*/
|
|
26
38
|
src?: string
|
|
27
39
|
/**
|
|
@@ -44,6 +56,22 @@ export interface NuaCmsOptions extends CmsMarkerOptions {
|
|
|
44
56
|
*/
|
|
45
57
|
siteTheme?: 'auto' | 'light' | 'dark'
|
|
46
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Run mode (cms-headless F7). Controls whether the plugin serves a local
|
|
61
|
+
* full-page collections admin:
|
|
62
|
+
*
|
|
63
|
+
* - `local` (default for `pletivo dev` on a developer machine): lazily spawn an
|
|
64
|
+
* in-process cms-sidecar over the project's `node:fs` and serve the
|
|
65
|
+
* `@nuasite/collections-admin` SPA at `/_nua/admin`. The inline widget runs
|
|
66
|
+
* in-page as usual.
|
|
67
|
+
* - `hosted` (inside the agent sandbox): a no-op for spawning/serving — the
|
|
68
|
+
* sidecar is a managed sandbox service and the admin is the webmaster tab.
|
|
69
|
+
* The plugin stays marker + CDN-inject only.
|
|
70
|
+
*
|
|
71
|
+
* Auto-detected from the environment when omitted (a sandbox env signal ⇒
|
|
72
|
+
* `hosted`; otherwise `local`). An explicit value always wins.
|
|
73
|
+
*/
|
|
74
|
+
mode?: CmsMode
|
|
47
75
|
/**
|
|
48
76
|
* Proxy /_nua/cms requests to this target URL during dev.
|
|
49
77
|
* Example: 'http://localhost:8787'
|
|
@@ -88,6 +116,20 @@ const DEFAULT_MAX_UPLOAD_SIZE = 10 * 1024 * 1024
|
|
|
88
116
|
|
|
89
117
|
const VIRTUAL_CMS_PATH = '/@nuasite/cms-editor.js'
|
|
90
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Virtual module id of the local `/_nua/admin` SPA entry (cms-headless F7). The
|
|
121
|
+
* HTML shell loads it as `<script type="module">`; the Vite plugin resolves it to
|
|
122
|
+
* `src/admin/entry.tsx` so the dev server transforms it (TSX → JS, real React).
|
|
123
|
+
*/
|
|
124
|
+
const VIRTUAL_ADMIN_ENTRY = '/@nuasite/cms-admin-entry.js'
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Public CDN editor script. Same URL hosting injects (see webmaster
|
|
128
|
+
* `packages/worker-hosting` `editorSrc`), so dev-preview and hosting load the
|
|
129
|
+
* identical inline editor and a CDN push updates both without touching the site.
|
|
130
|
+
*/
|
|
131
|
+
const DEFAULT_CDN_EDITOR_SRC = 'https://cdn.nuasite.com/script/latest/cms-editor.js'
|
|
132
|
+
|
|
91
133
|
export default function nuaCms(options: NuaCmsOptions = {}): AstroIntegration {
|
|
92
134
|
const {
|
|
93
135
|
// CMS editor options
|
|
@@ -111,6 +153,13 @@ export default function nuaCms(options: NuaCmsOptions = {}): AstroIntegration {
|
|
|
111
153
|
maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE,
|
|
112
154
|
} = options
|
|
113
155
|
|
|
156
|
+
// Run mode (cms-headless F7): `local` serves a full-page collections admin at
|
|
157
|
+
// /_nua/admin over an in-process sidecar; `hosted` (inside the agent sandbox)
|
|
158
|
+
// is a no-op for that — the managed sandbox sidecar + the webmaster tab own it.
|
|
159
|
+
// An explicit `options.mode` wins; otherwise auto-detect from the environment.
|
|
160
|
+
const mode = resolveCmsMode(options.mode)
|
|
161
|
+
const serveLocalAdmin = mode === 'local'
|
|
162
|
+
|
|
114
163
|
// When no proxy, enable local CMS API with default media adapter
|
|
115
164
|
const enableCmsApi = !proxy
|
|
116
165
|
const mediaAdapter = media ?? (enableCmsApi ? createLocalStorageAdapter() : undefined)
|
|
@@ -218,15 +267,41 @@ export default function nuaCms(options: NuaCmsOptions = {}): AstroIntegration {
|
|
|
218
267
|
}
|
|
219
268
|
|
|
220
269
|
const vitePlugins: any[] = [...(createVitePlugin(pluginContext) as any)]
|
|
221
|
-
const cmsDir =
|
|
270
|
+
const cmsDir = dirname(fileURLToPath(import.meta.url))
|
|
271
|
+
|
|
272
|
+
// Local-mode (cms-headless F7): resolve the /_nua/admin SPA entry virtual
|
|
273
|
+
// module to src/admin/entry.tsx so Vite transforms it (TSX → JS, real
|
|
274
|
+
// React) and HMR works. The HTML shell loads this id as a module script.
|
|
275
|
+
if (serveLocalAdmin) {
|
|
276
|
+
vitePlugins.push({
|
|
277
|
+
name: 'nuasite-cms-admin-entry',
|
|
278
|
+
resolveId(id: string) {
|
|
279
|
+
if (id === VIRTUAL_ADMIN_ENTRY) {
|
|
280
|
+
return join(cmsDir, 'admin/entry.tsx')
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
})
|
|
284
|
+
}
|
|
222
285
|
|
|
223
286
|
// Detect pre-built editor bundle (present when installed from npm)
|
|
224
|
-
const editorBundlePath =
|
|
225
|
-
const hasPrebuiltBundle =
|
|
287
|
+
const editorBundlePath = join(cmsDir, '../dist/editor.js')
|
|
288
|
+
const hasPrebuiltBundle = existsSync(editorBundlePath)
|
|
289
|
+
|
|
290
|
+
// Resolve which editor script dev-preview injects:
|
|
291
|
+
// 1. explicit `src`; 2. `NUA_CMS_EDITOR_SRC` env; 3. the public CDN editor
|
|
292
|
+
// when shipping a pre-built bundle (npm-installed site) — so dev-preview
|
|
293
|
+
// loads the same CDN editor as hosting and a CDN push updates it without a
|
|
294
|
+
// site rebuild; 4. the bundled source (monorepo only) for editor HMR.
|
|
295
|
+
const envSrc = process.env.NUA_CMS_EDITOR_SRC
|
|
296
|
+
const resolvedEditorSrc = src ?? envSrc ?? (hasPrebuiltBundle ? DEFAULT_CDN_EDITOR_SRC : VIRTUAL_CMS_PATH)
|
|
297
|
+
|
|
298
|
+
// Local virtual-module serving is only needed when injecting the bundled
|
|
299
|
+
// editor (the monorepo source path) — not for an external/CDN URL.
|
|
300
|
+
const servesBundledEditor = resolvedEditorSrc === VIRTUAL_CMS_PATH
|
|
226
301
|
|
|
227
302
|
// --- CMS Editor setup (dev only) ---
|
|
228
303
|
if (command === 'dev') {
|
|
229
|
-
const editorSrc =
|
|
304
|
+
const editorSrc = resolvedEditorSrc
|
|
230
305
|
|
|
231
306
|
const configScript = `window.NuaCmsConfig = ${JSON.stringify(resolvedCmsConfig)};`
|
|
232
307
|
|
|
@@ -248,52 +323,32 @@ export default function nuaCms(options: NuaCmsOptions = {}): AstroIntegration {
|
|
|
248
323
|
`,
|
|
249
324
|
)
|
|
250
325
|
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return join(cmsDir!, 'editor/index.tsx')
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
// Prepend @jsxImportSource pragma for editor .tsx files
|
|
283
|
-
// so Vite's esbuild uses Preact's h function
|
|
284
|
-
vitePlugins.push({
|
|
285
|
-
name: 'nuasite-cms-preact-jsx',
|
|
286
|
-
transform(code: string, id: string) {
|
|
287
|
-
if (id.includes('/src/editor/') && id.endsWith('.tsx') && !code.includes('@jsxImportSource')) {
|
|
288
|
-
return `/** @jsxImportSource preact */\n${code}`
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
// Add Tailwind CSS Vite plugin for editor styles
|
|
294
|
-
const tailwindcss = (await import('@tailwindcss/vite')).default
|
|
295
|
-
vitePlugins.push(tailwindcss())
|
|
296
|
-
}
|
|
326
|
+
if (servesBundledEditor) {
|
|
327
|
+
// Monorepo dev (no pre-built bundle): serve the editor source files
|
|
328
|
+
// directly so Vite transforms TSX, resolves imports, and HMR works.
|
|
329
|
+
vitePlugins.push({
|
|
330
|
+
name: 'nuasite-cms-editor',
|
|
331
|
+
resolveId(id: string) {
|
|
332
|
+
if (id === VIRTUAL_CMS_PATH) {
|
|
333
|
+
return join(cmsDir, 'editor/index.tsx')
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
// Prepend @jsxImportSource pragma for editor .tsx files
|
|
339
|
+
// so Vite's esbuild uses Preact's h function
|
|
340
|
+
vitePlugins.push({
|
|
341
|
+
name: 'nuasite-cms-preact-jsx',
|
|
342
|
+
transform(code: string, id: string) {
|
|
343
|
+
if (id.includes('/src/editor/') && id.endsWith('.tsx') && !code.includes('@jsxImportSource')) {
|
|
344
|
+
return `/** @jsxImportSource preact */\n${code}`
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// Add Tailwind CSS Vite plugin for editor styles
|
|
350
|
+
const tailwindcss = (await import('@tailwindcss/vite')).default
|
|
351
|
+
vitePlugins.push(tailwindcss())
|
|
297
352
|
}
|
|
298
353
|
}
|
|
299
354
|
|
|
@@ -306,8 +361,9 @@ export default function nuaCms(options: NuaCmsOptions = {}): AstroIntegration {
|
|
|
306
361
|
}
|
|
307
362
|
}
|
|
308
363
|
|
|
309
|
-
// Only add react->preact aliases when serving source files
|
|
310
|
-
|
|
364
|
+
// Only add react->preact aliases when serving the editor source files
|
|
365
|
+
// (monorepo dev) — an external/CDN editor URL needs no module rewriting.
|
|
366
|
+
const needsAliases = servesBundledEditor
|
|
311
367
|
|
|
312
368
|
updateConfig({
|
|
313
369
|
markdown: {
|
|
@@ -349,6 +405,22 @@ export default function nuaCms(options: NuaCmsOptions = {}): AstroIntegration {
|
|
|
349
405
|
if (enableCmsApi) {
|
|
350
406
|
logger.info('CMS API enabled at /_nua/cms/')
|
|
351
407
|
}
|
|
408
|
+
|
|
409
|
+
// Local-mode full-page collections admin (cms-headless F7). Registered
|
|
410
|
+
// only in `local` mode — in `hosted` (sandbox) this is a strict no-op:
|
|
411
|
+
// the managed sandbox sidecar (F2) + the webmaster tab (F3) own it.
|
|
412
|
+
// The in-process sidecar + SPA are built lazily on the first /_nua/admin
|
|
413
|
+
// hit, so dev startup stays fast. Media defaults to local public/uploads.
|
|
414
|
+
if (serveLocalAdmin) {
|
|
415
|
+
createLocalAdminMiddleware(server, {
|
|
416
|
+
contentDir,
|
|
417
|
+
componentDirs,
|
|
418
|
+
mediaAdapter: media ?? createLocalStorageAdapter(),
|
|
419
|
+
maxUploadSize,
|
|
420
|
+
entryModuleId: VIRTUAL_ADMIN_ENTRY,
|
|
421
|
+
})
|
|
422
|
+
logger.info(`CMS collections admin available at ${ADMIN_ROUTE}`)
|
|
423
|
+
}
|
|
352
424
|
},
|
|
353
425
|
|
|
354
426
|
'astro:build:done': async ({ dir, logger }) => {
|
|
@@ -391,12 +463,18 @@ async function mergeRedirects(dir: URL, logger: { info: (msg: string) => void })
|
|
|
391
463
|
logger.info(`Merged ${lineCount} CMS redirect(s) into _redirects`)
|
|
392
464
|
}
|
|
393
465
|
|
|
466
|
+
// Shared structural contract from @nuasite/cms-types — surfaced through the cms public API
|
|
467
|
+
// so consumers of @nuasite/cms get the field-type list + guard from one place.
|
|
468
|
+
export {
|
|
469
|
+
createContemberStorageAdapter as contemberMedia,
|
|
470
|
+
createLocalStorageAdapter as localMedia,
|
|
471
|
+
createS3StorageAdapter as s3Media,
|
|
472
|
+
} from '@nuasite/cms-core'
|
|
473
|
+
export { FIELD_TYPES, isFieldType } from '@nuasite/cms-types'
|
|
394
474
|
export { n } from './field-types'
|
|
395
475
|
export type { DateHints, ImageHints, NumberHints, TextareaHints, TextHints } from './field-types'
|
|
396
|
-
export { createContemberStorageAdapter as contemberMedia } from './media/contember'
|
|
397
|
-
export { createLocalStorageAdapter as localMedia } from './media/local'
|
|
398
|
-
export { createS3StorageAdapter as s3Media } from './media/s3'
|
|
399
476
|
export type { MediaFolderItem, MediaItem, MediaListOptions, MediaListResult, MediaStorageAdapter, MediaTypeFilter } from './media/types'
|
|
477
|
+
export { type CmsMode, detectHostedFromEnv, resolveCmsMode } from './mode'
|
|
400
478
|
export type { Color, Date, DateTime, Email, Image, Reference, Textarea, Time, Url } from './prop-types'
|
|
401
479
|
|
|
402
480
|
export { scanCollections } from './collection-scanner'
|