@plutocms/docs 0.0.0-stage → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -0
- package/.prettierignore +5 -0
- package/CHANGELOG.md +178 -0
- package/FEATURES.md +30 -0
- package/README.md +39 -2
- package/app/assets/css/pluto-layer.css +40 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +163 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockInspector.vue +519 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +159 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockModeSwitch.vue +46 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockOutline.vue +146 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockOutlineList.vue +347 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPalette.vue +85 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +233 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +246 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockRawEditor.vue +62 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockTextEditor.vue +92 -0
- package/app/components/Pluto/Docs/Blocks/PlutoPreviewBlock.vue +80 -0
- package/app/components/Pluto/Docs/PlutoFieldMdc.vue +117 -0
- package/app/components/Pluto/Docs/PlutoMdcInsertMenu.vue +36 -0
- package/app/components/PlutoContentPreview.vue +35 -0
- package/app/composables/pluto-block-editor.ts +443 -0
- package/app/composables/pluto-content-edit-link.ts +19 -0
- package/app/composables/pluto-content-preview.ts +29 -0
- package/app/composables/pluto-content-runtime.ts +237 -0
- package/app/pages/_pluto/preview.vue +555 -0
- package/app/plugins/pluto-docs.ts +50 -0
- package/i18n/locales/en.json +253 -0
- package/i18n/locales/pt-BR.json +289 -0
- package/modules/pluto-docs/index.ts +123 -0
- package/modules/pluto-docs/manifest.ts +159 -0
- package/modules/pluto-docs/options.ts +224 -0
- package/nuxt.config.ts +36 -0
- package/package.json +55 -4
- package/server/handlers/navigation.get.ts +7 -0
- package/server/handlers/page.get.ts +7 -0
- package/server/handlers/surround.get.ts +7 -0
- package/server/plugins/pluto-docs.ts +25 -0
- package/server/utils/pluto-docs-runtime.ts +504 -0
- package/shared/types/docs.d.ts +40 -0
- package/shared/types/pluto-manifest.d.ts +6 -0
- package/shared/utils/blocks/editor.ts +357 -0
- package/shared/utils/blocks/history.ts +94 -0
- package/shared/utils/blocks/mdc-props.ts +321 -0
- package/shared/utils/blocks/operations.ts +692 -0
- package/shared/utils/blocks/parse.ts +444 -0
- package/shared/utils/blocks/presets.ts +383 -0
- package/shared/utils/blocks/preview-marks.ts +98 -0
- package/shared/utils/blocks/preview-protocol.ts +195 -0
- package/shared/utils/blocks/registry.ts +163 -0
- package/shared/utils/blocks/serialize.ts +107 -0
- package/shared/utils/blocks/tree.ts +85 -0
- package/shared/utils/blocks/types.ts +165 -0
- package/shared/utils/content-route.ts +40 -0
- package/shared/utils/docs-type.ts +290 -0
- package/shared/utils/mdc-snippets.ts +114 -0
- package/shared/utils/navigation-config.ts +19 -0
- package/shared/utils/runtime-content.ts +403 -0
- package/tailwind.lint.css +10 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// The build manifest of the hybrid mode. Node code that runs at build time. It lists the
|
|
2
|
+
// content files of each type with the version that the content store reports for the file
|
|
3
|
+
// (`_version`). At run time the server compares the store with this list to see which pages
|
|
4
|
+
// changed after the build. See the docs-setup skill.
|
|
5
|
+
|
|
6
|
+
import { Buffer } from 'node:buffer'
|
|
7
|
+
import { createHash } from 'node:crypto'
|
|
8
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
9
|
+
import { join, relative, sep } from 'node:path'
|
|
10
|
+
|
|
11
|
+
/** Content id → version, for one type. */
|
|
12
|
+
export type TypeManifest = Record<string, string>
|
|
13
|
+
|
|
14
|
+
/** Type name → its manifest. */
|
|
15
|
+
export type ContentManifest = Record<string, TypeManifest>
|
|
16
|
+
|
|
17
|
+
/** The same dirs that the CMS fs store skips. */
|
|
18
|
+
const SKIPPED_DIRS = new Set([
|
|
19
|
+
'node_modules',
|
|
20
|
+
'.nuxt',
|
|
21
|
+
'.output',
|
|
22
|
+
'.data',
|
|
23
|
+
'.nitro',
|
|
24
|
+
'.cache',
|
|
25
|
+
'dist',
|
|
26
|
+
])
|
|
27
|
+
const SEGMENT_PATTERN = /^(?:\d+\.)?[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
28
|
+
const MAX_SEGMENTS = 6
|
|
29
|
+
const MAX_ID_LENGTH = 255
|
|
30
|
+
|
|
31
|
+
/** The Git blob id of a text: sha1 of `blob <byteLength>\0` and the UTF-8 bytes. Core does the same. */
|
|
32
|
+
export function gitBlobSha(text: string): string {
|
|
33
|
+
const bytes = Buffer.from(text, 'utf8')
|
|
34
|
+
return createHash('sha1')
|
|
35
|
+
.update(`blob ${bytes.length}\0`)
|
|
36
|
+
.update(bytes)
|
|
37
|
+
.digest('hex')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The version of a file text for a storage.
|
|
42
|
+
*
|
|
43
|
+
* - `github`: the store reports the Git blob id. Git keeps LF in the blob, so a CRLF working file
|
|
44
|
+
* (Windows with `core.autocrlf`) is hashed with LF. A file that really has CRLF in the repository
|
|
45
|
+
* looks changed at run time. That is safe: the page then comes from the store.
|
|
46
|
+
* - `fs` (and `memory`): the store hashes the file as it is on the disk, so the manifest does too.
|
|
47
|
+
*/
|
|
48
|
+
export function versionOf(text: string, storage: string): string {
|
|
49
|
+
return gitBlobSha(storage === 'github' ? text.replace(/\r\n/g, '\n') : text)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether a content id is one that the CMS lists: 1 to 6 safe segments, and not excluded. */
|
|
53
|
+
export function isListedId(
|
|
54
|
+
id: string,
|
|
55
|
+
exclude: readonly string[] = []
|
|
56
|
+
): boolean {
|
|
57
|
+
if (id.length === 0 || id.length > MAX_ID_LENGTH) {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
const segments = id.split('/')
|
|
61
|
+
if (
|
|
62
|
+
segments.length > MAX_SEGMENTS ||
|
|
63
|
+
segments.some((segment) => !SEGMENT_PATTERN.test(segment))
|
|
64
|
+
) {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
return !exclude.some((entry) =>
|
|
68
|
+
entry.endsWith('/') ? id.startsWith(entry) : id === entry
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One file in memory: the path is relative to the type folder, with `/`. */
|
|
73
|
+
export interface ManifestFile {
|
|
74
|
+
path: string
|
|
75
|
+
text: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Builds the manifest of one type from its files. A file that is not a listed `.md` page is skipped. */
|
|
79
|
+
export function buildTypeManifest(
|
|
80
|
+
files: ManifestFile[],
|
|
81
|
+
options: { storage: string; exclude?: readonly string[] }
|
|
82
|
+
): TypeManifest {
|
|
83
|
+
const manifest: TypeManifest = {}
|
|
84
|
+
for (const file of [...files].sort((a, b) =>
|
|
85
|
+
a.path < b.path ? -1 : a.path > b.path ? 1 : 0
|
|
86
|
+
)) {
|
|
87
|
+
if (!file.path.endsWith('.md')) {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
const id = file.path.slice(0, -'.md'.length)
|
|
91
|
+
if (!isListedId(id, options.exclude)) {
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
manifest[id] = versionOf(file.text, options.storage)
|
|
95
|
+
}
|
|
96
|
+
return manifest
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function walkMarkdown(dir: string, out: string[]): void {
|
|
100
|
+
let entries
|
|
101
|
+
try {
|
|
102
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
103
|
+
} catch {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (entry.name.startsWith('.')) {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
const full = join(dir, entry.name)
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
if (!SKIPPED_DIRS.has(entry.name)) {
|
|
113
|
+
walkMarkdown(full, out)
|
|
114
|
+
}
|
|
115
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
116
|
+
out.push(full)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Reads the `.md` files under a type folder on the disk. A missing folder gives `[]`. */
|
|
122
|
+
export function readTypeFiles(folder: string): ManifestFile[] {
|
|
123
|
+
const found: string[] = []
|
|
124
|
+
walkMarkdown(folder, found)
|
|
125
|
+
return found.map((full) => ({
|
|
126
|
+
path: relative(folder, full).split(sep).join('/'),
|
|
127
|
+
text: readFileSync(full, 'utf8'),
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ManifestTypeInput {
|
|
132
|
+
name: string
|
|
133
|
+
/** The folder under the CMS content dir. `''` is the content dir itself. */
|
|
134
|
+
source: string
|
|
135
|
+
exclude?: string[]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Builds the manifest of every type. `contentDir` is the absolute path of the CMS content dir. */
|
|
139
|
+
export function buildContentManifest(
|
|
140
|
+
types: ManifestTypeInput[],
|
|
141
|
+
options: { contentDir: string; storage: string }
|
|
142
|
+
): ContentManifest {
|
|
143
|
+
const manifest: ContentManifest = {}
|
|
144
|
+
for (const type of types) {
|
|
145
|
+
const folder = type.source
|
|
146
|
+
? join(options.contentDir, type.source)
|
|
147
|
+
: options.contentDir
|
|
148
|
+
manifest[type.name] = buildTypeManifest(readTypeFiles(folder), {
|
|
149
|
+
storage: options.storage,
|
|
150
|
+
exclude: type.exclude,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
return manifest
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The source of the virtual module that the server imports. */
|
|
157
|
+
export function manifestModuleSource(manifest: ContentManifest): string {
|
|
158
|
+
return `export default ${JSON.stringify(manifest)}\n`
|
|
159
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/** One content type that the layer builds from a preset. */
|
|
2
|
+
export interface PlutoDocsTypeOptions {
|
|
3
|
+
/** The preset that makes the type. Only `docs` exists. */
|
|
4
|
+
preset: 'docs'
|
|
5
|
+
/** The content type name. It is a URL segment: `/admin/content/<name>`. */
|
|
6
|
+
name: string
|
|
7
|
+
/**
|
|
8
|
+
* The folder of the pages, under the CMS content dir. Use `''` for the content
|
|
9
|
+
* dir itself (the usual case for a Nuxt Content site).
|
|
10
|
+
*/
|
|
11
|
+
source: string
|
|
12
|
+
label?: string
|
|
13
|
+
labelPlural?: string
|
|
14
|
+
/** The Iconify name of the type in the admin (sidebar, New menu, dashboard). Default `lucide:book-open`. */
|
|
15
|
+
icon?: string
|
|
16
|
+
/** Ids that the type must not list or edit. An id (`index`), or a folder with a trailing slash (`drafts/`). */
|
|
17
|
+
exclude?: string[]
|
|
18
|
+
/** The route prefix of the pages on the public site, for the View link of the form. Default: none. */
|
|
19
|
+
routePrefix?: string
|
|
20
|
+
/**
|
|
21
|
+
* The nav group of the type in the admin sidebar and the admin bar. Default `'pages'` (core's
|
|
22
|
+
* group). A string is a group id. `false` gives the type its own sidebar item.
|
|
23
|
+
*/
|
|
24
|
+
navGroup?: string | false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** How the public pages get their content. See the docs-setup skill. */
|
|
28
|
+
export type PlutoDocsRender = 'hybrid' | 'build' | 'runtime'
|
|
29
|
+
|
|
30
|
+
export interface PlutoDocsOptions {
|
|
31
|
+
/** The content types to register. Default: none. */
|
|
32
|
+
types?: PlutoDocsTypeOptions[]
|
|
33
|
+
/**
|
|
34
|
+
* `'hybrid'` (default): the pages are built with the site, and the server reads from the content
|
|
35
|
+
* repository the pages that changed after the build. `'build'`: Nuxt Content builds the pages
|
|
36
|
+
* and nothing else. `'runtime'`: the server reads every page from the repository at request time.
|
|
37
|
+
*/
|
|
38
|
+
render?: PlutoDocsRender
|
|
39
|
+
/**
|
|
40
|
+
* Seconds that a CDN may cache the answers of the layer routes (`hybrid` and `runtime`). Default 10.
|
|
41
|
+
* It is short, so a save shows soon. `0` turns the cache header into `no-store`.
|
|
42
|
+
*/
|
|
43
|
+
runtimeMaxAge?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PlutoDocsResolveContext {
|
|
47
|
+
/** False when the app sets `plutoGithub.enabled` to false. */
|
|
48
|
+
cmsEnabled: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PlutoDocsResolveResult {
|
|
52
|
+
enabled: boolean
|
|
53
|
+
render: PlutoDocsRender
|
|
54
|
+
runtimeMaxAge: number
|
|
55
|
+
types: PlutoDocsTypeOptions[]
|
|
56
|
+
errors: string[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const PLUTO_DOCS_PRESETS = ['docs'] as const
|
|
60
|
+
export const PLUTO_DOCS_RENDER_MODES = ['hybrid', 'build', 'runtime'] as const
|
|
61
|
+
export const DEFAULT_RUNTIME_MAX_AGE = 10
|
|
62
|
+
|
|
63
|
+
export const CMS_OFF_ERROR =
|
|
64
|
+
'The docs layer needs the standalone CMS. Remove plutoGithub.enabled: false, or remove the layer.'
|
|
65
|
+
|
|
66
|
+
const NAME_PATTERN = /^[a-z][a-z0-9-]*$/
|
|
67
|
+
const SOURCE_SEGMENT = /^(?:\d+\.)?[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
68
|
+
|
|
69
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
70
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether `source` is `''` or a safe nested path (the same segment rule as the CMS). */
|
|
74
|
+
export function isValidSource(source: unknown): source is string {
|
|
75
|
+
if (typeof source !== 'string') {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
if (source === '') {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
const segments = source.split('/')
|
|
82
|
+
return (
|
|
83
|
+
segments.length <= 6 &&
|
|
84
|
+
segments.every((segment) => SOURCE_SEGMENT.test(segment))
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Normalizes a route prefix to `/a/b`, or `''` for none. */
|
|
89
|
+
export function normalizeRoutePrefix(prefix: unknown): string {
|
|
90
|
+
if (typeof prefix !== 'string') {
|
|
91
|
+
return ''
|
|
92
|
+
}
|
|
93
|
+
const segments = prefix.split('/').filter(Boolean)
|
|
94
|
+
return segments.length > 0 ? `/${segments.join('/')}` : ''
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Checks the module options. A type with a problem is dropped and gives one
|
|
99
|
+
* error. When the CMS is off, the layer is off and gives one error. This file
|
|
100
|
+
* has no Nuxt import, so plain Vitest tests it.
|
|
101
|
+
*/
|
|
102
|
+
export function resolvePlutoDocsOptions(
|
|
103
|
+
options: PlutoDocsOptions,
|
|
104
|
+
ctx: PlutoDocsResolveContext
|
|
105
|
+
): PlutoDocsResolveResult {
|
|
106
|
+
const errors: string[] = []
|
|
107
|
+
|
|
108
|
+
if (!ctx.cmsEnabled) {
|
|
109
|
+
return {
|
|
110
|
+
enabled: false,
|
|
111
|
+
render: 'build',
|
|
112
|
+
runtimeMaxAge: DEFAULT_RUNTIME_MAX_AGE,
|
|
113
|
+
types: [],
|
|
114
|
+
errors: [CMS_OFF_ERROR],
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let render: PlutoDocsRender = 'hybrid'
|
|
119
|
+
if (options.render !== undefined) {
|
|
120
|
+
if (
|
|
121
|
+
(PLUTO_DOCS_RENDER_MODES as readonly unknown[]).includes(options.render)
|
|
122
|
+
) {
|
|
123
|
+
render = options.render
|
|
124
|
+
} else {
|
|
125
|
+
errors.push(
|
|
126
|
+
`plutoDocs.render must be one of: ${PLUTO_DOCS_RENDER_MODES.join(', ')}. Using "hybrid".`
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let runtimeMaxAge = DEFAULT_RUNTIME_MAX_AGE
|
|
132
|
+
if (options.runtimeMaxAge !== undefined) {
|
|
133
|
+
if (
|
|
134
|
+
typeof options.runtimeMaxAge === 'number' &&
|
|
135
|
+
Number.isInteger(options.runtimeMaxAge) &&
|
|
136
|
+
options.runtimeMaxAge >= 0
|
|
137
|
+
) {
|
|
138
|
+
runtimeMaxAge = options.runtimeMaxAge
|
|
139
|
+
} else {
|
|
140
|
+
errors.push(
|
|
141
|
+
`plutoDocs.runtimeMaxAge must be a whole number of seconds, 0 or more. Using ${DEFAULT_RUNTIME_MAX_AGE}.`
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const types: PlutoDocsTypeOptions[] = []
|
|
147
|
+
const names = new Set<string>()
|
|
148
|
+
const entries: unknown[] = Array.isArray(options.types) ? options.types : []
|
|
149
|
+
|
|
150
|
+
for (const [index, entry] of entries.entries()) {
|
|
151
|
+
const where = `plutoDocs.types[${index}]`
|
|
152
|
+
if (!isRecord(entry)) {
|
|
153
|
+
errors.push(`${where} must be an object.`)
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
if (!(PLUTO_DOCS_PRESETS as readonly unknown[]).includes(entry.preset)) {
|
|
157
|
+
errors.push(
|
|
158
|
+
`${where}.preset must be one of: ${PLUTO_DOCS_PRESETS.join(', ')}.`
|
|
159
|
+
)
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (typeof entry.name !== 'string' || !NAME_PATTERN.test(entry.name)) {
|
|
163
|
+
errors.push(
|
|
164
|
+
`${where}.name must use lowercase letters, numbers and dashes, and start with a letter.`
|
|
165
|
+
)
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
if (names.has(entry.name)) {
|
|
169
|
+
errors.push(`${where}.name "${entry.name}" is used twice.`)
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (!isValidSource(entry.source)) {
|
|
173
|
+
errors.push(
|
|
174
|
+
`${where}.source must be '' or a folder path such as "docs" or "1.docs/api".`
|
|
175
|
+
)
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
entry.exclude !== undefined &&
|
|
180
|
+
!(
|
|
181
|
+
Array.isArray(entry.exclude) &&
|
|
182
|
+
entry.exclude.every((item) => typeof item === 'string')
|
|
183
|
+
)
|
|
184
|
+
) {
|
|
185
|
+
errors.push(`${where}.exclude must be a list of strings.`)
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
entry.navGroup !== undefined &&
|
|
190
|
+
entry.navGroup !== false &&
|
|
191
|
+
!(typeof entry.navGroup === 'string' && entry.navGroup !== '')
|
|
192
|
+
) {
|
|
193
|
+
errors.push(`${where}.navGroup must be a group id, or false.`)
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
entry.icon !== undefined &&
|
|
198
|
+
!(typeof entry.icon === 'string' && entry.icon !== '')
|
|
199
|
+
) {
|
|
200
|
+
errors.push(`${where}.icon must be an icon name.`)
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
names.add(entry.name)
|
|
204
|
+
types.push({
|
|
205
|
+
preset: 'docs',
|
|
206
|
+
name: entry.name,
|
|
207
|
+
source: entry.source,
|
|
208
|
+
...(typeof entry.label === 'string' && { label: entry.label }),
|
|
209
|
+
...(typeof entry.labelPlural === 'string' && {
|
|
210
|
+
labelPlural: entry.labelPlural,
|
|
211
|
+
}),
|
|
212
|
+
...(typeof entry.icon === 'string' && { icon: entry.icon }),
|
|
213
|
+
...(Array.isArray(entry.exclude) && {
|
|
214
|
+
exclude: entry.exclude as string[],
|
|
215
|
+
}),
|
|
216
|
+
routePrefix: normalizeRoutePrefix(entry.routePrefix),
|
|
217
|
+
...((typeof entry.navGroup === 'string' || entry.navGroup === false) && {
|
|
218
|
+
navGroup: entry.navGroup,
|
|
219
|
+
}),
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { enabled: true, render, runtimeMaxAge, types, errors }
|
|
224
|
+
}
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Point PLUTO_CORE_PATH at a local checkout (e.g. `../pluto/packages/core`) to test
|
|
2
|
+
// unpublished changes; unset, it resolves to the published npm package.
|
|
3
|
+
const coreLayer = process.env.PLUTO_CORE_PATH || '@plutocms/core'
|
|
4
|
+
|
|
5
|
+
export default defineNuxtConfig({
|
|
6
|
+
extends: [coreLayer],
|
|
7
|
+
|
|
8
|
+
modules: ['@nuxt/content'],
|
|
9
|
+
|
|
10
|
+
$meta: {
|
|
11
|
+
name: 'docs',
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
// Nuxt Content stores its data in SQLite. The native connector needs Node 22.5
|
|
15
|
+
// or newer and no extra package. An app can set another connector.
|
|
16
|
+
content: {
|
|
17
|
+
experimental: {
|
|
18
|
+
sqliteConnector: 'native',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// No `css` entry: the app owns the one Tailwind build, and Nuxt UI scans
|
|
23
|
+
// this layer's app/ dir. The tokens of this layer are in
|
|
24
|
+
// app/assets/css/pluto-layer.css. See the single-tailwind-build skill in pluto.
|
|
25
|
+
|
|
26
|
+
// The pages are Markdown files, so the fixed blog post type is off. A docs
|
|
27
|
+
// edit must redeploy the site, so the commit message has no `[skip ci]`
|
|
28
|
+
// marker by default. An app can change both. The other `plutoGithub` options
|
|
29
|
+
// have defaults in core (see the github-cms skill in pluto).
|
|
30
|
+
plutoGithub: {
|
|
31
|
+
postType: false,
|
|
32
|
+
commit: {
|
|
33
|
+
skipDeploy: false,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plutocms/docs",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.15.0",
|
|
5
|
+
"description": "Docs layer for Pluto: edit the Markdown pages of a Nuxt Content site in the Pluto admin",
|
|
6
|
+
"author": "Victor Ribeiro",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/plutocms/docs#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/plutocms/docs.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/plutocms/docs/issues"
|
|
15
|
+
},
|
|
16
|
+
"main": "./nuxt.config.ts",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"prepare": "nuxt prepare",
|
|
19
|
+
"prettier": "prettier --write .",
|
|
20
|
+
"lint": "eslint .",
|
|
21
|
+
"lint:fix": "eslint . --fix",
|
|
22
|
+
"typecheck": "nuxi typecheck",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@nuxt/content": "^3.16.2",
|
|
27
|
+
"@nuxtjs/mdc": "^0.23.2",
|
|
28
|
+
"@plutocms/core": ">=0.39.0 <1.0.0",
|
|
29
|
+
"@types/sortablejs": "^1.15.9",
|
|
30
|
+
"reka-ui": "^2.11.0",
|
|
31
|
+
"remark-gfm": "4.0.1",
|
|
32
|
+
"remark-mdc": "3.11.1",
|
|
33
|
+
"remark-parse": "11.0.0",
|
|
34
|
+
"sortablejs": "^1.15.7",
|
|
35
|
+
"unified": "11.0.5",
|
|
36
|
+
"yaml": "2.9.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@antfu/eslint-config": "^9.5.1",
|
|
40
|
+
"@nuxt/eslint-config": "^1.17.0",
|
|
41
|
+
"@nuxt/ui": "^4.12.0",
|
|
42
|
+
"@types/node": "^26.6.5",
|
|
43
|
+
"@vueuse/nuxt": "^15.0.0",
|
|
44
|
+
"eslint": "^10.12.0",
|
|
45
|
+
"eslint-config-prettier": "^10.1.8",
|
|
46
|
+
"eslint-plugin-better-tailwindcss": "^4.10.0",
|
|
47
|
+
"nuxt": "^4.6.1",
|
|
48
|
+
"prettier": "^3.9.10",
|
|
49
|
+
"vitest": "^5.0.5",
|
|
50
|
+
"vue": "3.5.44",
|
|
51
|
+
"vue-router": "5.4.0",
|
|
52
|
+
"vue-tsc": "^3.3.13"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineEventHandler } from 'h3'
|
|
2
|
+
|
|
3
|
+
// Added by the module only when `plutoDocs.render` is `'hybrid'` or `'runtime'`. A route of the
|
|
4
|
+
// public API (`/api/pluto/v1/docs/:type/navigation`): the answer is `{ data }`, like core's routes.
|
|
5
|
+
export default defineEventHandler(async (event) => ({
|
|
6
|
+
data: await plutoRuntimeNavigation(event),
|
|
7
|
+
}))
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineEventHandler } from 'h3'
|
|
2
|
+
|
|
3
|
+
// Added by the module only when `plutoDocs.render` is `'hybrid'` or `'runtime'`. A route of the
|
|
4
|
+
// public API (`/api/pluto/v1/docs/:type/page`): the answer is `{ data }`, like core's routes.
|
|
5
|
+
export default defineEventHandler(async (event) => ({
|
|
6
|
+
data: await plutoRuntimePage(event),
|
|
7
|
+
}))
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineEventHandler } from 'h3'
|
|
2
|
+
|
|
3
|
+
// Added by the module only when `plutoDocs.render` is `'hybrid'` or `'runtime'`. A route of the
|
|
4
|
+
// public API (`/api/pluto/v1/docs/:type/surround`): the answer is `{ data }`, like core's routes.
|
|
5
|
+
export default defineEventHandler(async (event) => ({
|
|
6
|
+
data: await plutoRuntimeSurround(event),
|
|
7
|
+
}))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PlutoDocsTypeOptions } from '../../modules/pluto-docs/options'
|
|
2
|
+
|
|
3
|
+
// Registers the content types of the layer with the core content registry. The
|
|
4
|
+
// CMS Nitro plugin registers the adapter. The types are looked up per request,
|
|
5
|
+
// so the order of the two plugins does not matter. When the layer is off (the
|
|
6
|
+
// CMS is off), the module sets no config and this plugin does nothing.
|
|
7
|
+
export default defineNitroPlugin(() => {
|
|
8
|
+
const config = useRuntimeConfig().public.plutoDocs as
|
|
9
|
+
| {
|
|
10
|
+
types: PlutoDocsTypeOptions[]
|
|
11
|
+
render?: string
|
|
12
|
+
}
|
|
13
|
+
| undefined
|
|
14
|
+
if (!config) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
for (const entry of config.types) {
|
|
18
|
+
registerContentType(
|
|
19
|
+
createDocsType({
|
|
20
|
+
...entry,
|
|
21
|
+
liveOnSave: config.render === 'hybrid' || config.render === 'runtime',
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
})
|