@xleddyl/nuxt-cms 0.2.2 → 0.3.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.
Files changed (92) hide show
  1. package/README.md +4 -2
  2. package/dist/module.d.mts +4 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +329 -32
  5. package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +45 -0
  6. package/dist/runtime/app/blocks/CmsBlocks.vue +36 -0
  7. package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +45 -0
  8. package/dist/runtime/app/blocks/resolve.d.ts +21 -0
  9. package/dist/runtime/app/blocks/resolve.js +37 -0
  10. package/dist/runtime/app/components/cms/BlockOutline.d.vue.ts +17 -0
  11. package/dist/runtime/app/components/cms/BlockOutline.vue +99 -0
  12. package/dist/runtime/app/components/cms/BlockOutline.vue.d.ts +17 -0
  13. package/dist/runtime/app/components/cms/BlockPalette.d.vue.ts +12 -0
  14. package/dist/runtime/app/components/cms/BlockPalette.vue +29 -0
  15. package/dist/runtime/app/components/cms/BlockPalette.vue.d.ts +12 -0
  16. package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +2 -0
  17. package/dist/runtime/app/components/cms/BlocksField.vue +83 -3
  18. package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +2 -0
  19. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +2 -0
  20. package/dist/runtime/app/components/cms/EntryForm.vue +43 -7
  21. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +2 -0
  22. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +2 -0
  23. package/dist/runtime/app/components/cms/FieldInput.vue +18 -1
  24. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +2 -0
  25. package/dist/runtime/app/components/cms/MediaDetail.vue +25 -13
  26. package/dist/runtime/app/components/cms/Search.d.vue.ts +7 -0
  27. package/dist/runtime/app/components/cms/Search.vue +347 -0
  28. package/dist/runtime/app/components/cms/Search.vue.d.ts +7 -0
  29. package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
  30. package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
  31. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
  32. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
  33. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
  34. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +39 -0
  35. package/dist/runtime/app/components/cms/VisualEditor.vue +553 -0
  36. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +39 -0
  37. package/dist/runtime/app/composables/cms-entry-disabled.d.ts +7 -1
  38. package/dist/runtime/app/composables/cms-entry-disabled.js +15 -0
  39. package/dist/runtime/app/composables/cms-entry.d.ts +7 -1
  40. package/dist/runtime/app/composables/cms-entry.js +51 -1
  41. package/dist/runtime/app/composables/cms-field-focus.d.ts +4 -0
  42. package/dist/runtime/app/composables/cms-field-focus.js +20 -0
  43. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  44. package/dist/runtime/app/composables/cms-settings.js +8 -2
  45. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  46. package/dist/runtime/app/layouts/cms-admin.vue +7 -0
  47. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  48. package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
  49. package/dist/runtime/app/layouts/cms-editor.vue +14 -0
  50. package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
  51. package/dist/runtime/app/pages/admin-collection.vue +36 -4
  52. package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
  53. package/dist/runtime/app/pages/admin-content.vue +287 -0
  54. package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
  55. package/dist/runtime/app/pages/admin-entry.vue +3 -0
  56. package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
  57. package/dist/runtime/app/pages/admin-preview.vue +132 -0
  58. package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
  59. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
  60. package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
  61. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
  62. package/dist/runtime/app/utils/ui.js +1 -0
  63. package/dist/runtime/assets/main.css +1 -1
  64. package/dist/runtime/server/api/collection.get.js +4 -5
  65. package/dist/runtime/server/api/collection.post.js +4 -1
  66. package/dist/runtime/server/api/item.delete.js +2 -1
  67. package/dist/runtime/server/api/item.put.js +2 -1
  68. package/dist/runtime/server/api/media-download.get.d.ts +2 -0
  69. package/dist/runtime/server/api/media-download.get.js +90 -0
  70. package/dist/runtime/server/api/search.get.d.ts +3 -0
  71. package/dist/runtime/server/api/search.get.js +24 -0
  72. package/dist/runtime/server/utils/graphql.js +23 -8
  73. package/dist/runtime/server/utils/media-download.d.ts +1 -0
  74. package/dist/runtime/server/utils/media-download.js +8 -0
  75. package/dist/runtime/server/utils/media.d.ts +1 -0
  76. package/dist/runtime/server/utils/media.js +18 -3
  77. package/dist/runtime/server/utils/registry.d.ts +1 -0
  78. package/dist/runtime/server/utils/registry.js +6 -0
  79. package/dist/runtime/server/utils/search.d.ts +20 -0
  80. package/dist/runtime/server/utils/search.js +233 -0
  81. package/dist/runtime/server/utils/visibility.d.ts +4 -0
  82. package/dist/runtime/server/utils/visibility.js +12 -0
  83. package/dist/runtime/shared/graphql-sdl.js +4 -1
  84. package/dist/runtime/shared/index.d.ts +59 -5
  85. package/dist/runtime/shared/index.js +133 -0
  86. package/dist/runtime/shared/preview.d.ts +85 -0
  87. package/dist/runtime/shared/preview.js +219 -0
  88. package/dist/runtime/shared/search.d.ts +52 -0
  89. package/dist/runtime/shared/search.js +207 -0
  90. package/dist/runtime/shared/validation.d.ts +2 -0
  91. package/dist/runtime/shared/validation.js +15 -2
  92. package/package.json +2 -2
package/dist/module.mjs CHANGED
@@ -3,20 +3,86 @@ import { existsSync } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import { join, isAbsolute, resolve, relative, dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { defineNuxtModule, createResolver, useLogger, addImports, addTemplate, addServerPlugin, addTypeTemplate, addVitePlugin, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler, resolvePath } from '@nuxt/kit';
6
+ import { defineNuxtModule, createResolver, useLogger, addImports, addTemplate, addServerPlugin, addTypeTemplate, addVitePlugin, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler, resolvePath, addComponent } from '@nuxt/kit';
7
7
  import tailwindcss from '@tailwindcss/vite';
8
8
  import svgLoader from 'vite-svg-loader';
9
9
  import { introspectionFromSchema, buildSchema } from 'graphql';
10
10
  import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal';
11
11
  import { createJiti } from 'jiti';
12
- import { typeName, blockTypeName, blocksFieldTypeName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
12
+ import { typeName, blockTypeName as blockTypeName$1, blocksFieldTypeName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
13
13
  import { migrationsDirFor } from '../dist/runtime/shared/migrations-dir.js';
14
- import { pageColumnFields, isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, PAGE_PATH_FIELD, pageRoutes, layoutErrors, listErrors, mobileMediaKey, fieldConditions, pageFieldsTableName, pageMediaTableName, isRequiredField, pageFields, isPrivateField, entryFieldsFor, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, expandMobileMedia } from '../dist/runtime/shared/index.js';
14
+ import { entryFieldsFor, isPrivateField, blockTypeName, pageColumnFields, isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, normalizeContentEntries, PAGE_PATH_FIELD, pageRoutes, isCollectionKind, layoutErrors, listErrors, mobileMediaKey, fieldConditions, CONTENT_SLUG_FIELD, pageFieldsTableName, pageMediaTableName, CONTENT_SYSTEM_FIELDS, isRequiredField, pageFields, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, normalizeCmsConfig } from '../dist/runtime/shared/index.js';
15
+ import { previewPathErrors, DEFAULT_CMS_PREVIEW_PATH } from '../dist/runtime/shared/preview.js';
15
16
  import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
16
17
  import { createHash } from 'node:crypto';
17
18
  import { readFile } from 'node:fs/promises';
18
19
  import { routePathsFromDir, resolvePageRoutes } from '../dist/runtime/shared/page-routes.js';
19
20
 
21
+ function componentPascalName(name) {
22
+ return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
23
+ }
24
+ function collectBlockComponents(config) {
25
+ const refs = [];
26
+ for (const [entryName, entry] of Object.entries(config)) {
27
+ for (const [fieldKey, field] of Object.entries(entryFieldsFor(entry))) {
28
+ if (field.type !== "blocks" || isPrivateField(field)) continue;
29
+ for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
30
+ if (typeof block.component !== "string" || !block.component.trim()) continue;
31
+ refs.push({
32
+ entry: entryName,
33
+ field: fieldKey,
34
+ block: blockName,
35
+ component: componentPascalName(block.component.trim()),
36
+ typeName: blockTypeName(entryName, fieldKey, blockName)
37
+ });
38
+ }
39
+ }
40
+ }
41
+ return refs;
42
+ }
43
+ function collectPreviewComponents(config, fallback) {
44
+ const refs = [];
45
+ for (const [entryName, entry] of Object.entries(config)) {
46
+ if (entry.kind !== "content") continue;
47
+ const name = entry.preview?.component ?? fallback;
48
+ if (typeof name !== "string" || !name.trim()) continue;
49
+ refs.push({ entry: entryName, component: componentPascalName(name.trim()) });
50
+ }
51
+ return refs;
52
+ }
53
+ function missingPreviewComponents(refs, available) {
54
+ const known = new Set(available);
55
+ return refs.filter((ref) => !known.has(ref.component)).map(
56
+ (ref) => `cms.config entry '${ref.entry}': preview component '${ref.component}' is not a registered Nuxt component`
57
+ );
58
+ }
59
+ function missingBlockComponents(refs, available) {
60
+ const known = new Set(available);
61
+ return refs.filter((ref) => !known.has(ref.component)).map(
62
+ (ref) => `cms.config entry '${ref.entry}', field '${ref.field}', block '${ref.block}': component '${ref.component}' is not a registered Nuxt component`
63
+ );
64
+ }
65
+ function renderBlocksFile(refs, previews = []) {
66
+ const components = [...new Set([...refs, ...previews].map((ref) => ref.component))].sort();
67
+ const lines = [`import type { Component } from 'vue'`];
68
+ if (components.length)
69
+ lines.push(
70
+ `import { ${components.map((name) => `Lazy${name}`).join(", ")} } from '#components'`
71
+ );
72
+ lines.push(
73
+ ``,
74
+ `export const cmsBlockComponents: Record<string, Component> = {`,
75
+ ...refs.map((ref) => ` ${JSON.stringify(ref.typeName)}: Lazy${ref.component},`),
76
+ `}`,
77
+ ``,
78
+ `export const cmsPreviewComponents: Record<string, Component> = {`,
79
+ ...previews.map((ref) => ` ${JSON.stringify(ref.entry)}: Lazy${ref.component},`),
80
+ `}`,
81
+ ``
82
+ );
83
+ return lines.join("\n");
84
+ }
85
+
20
86
  async function collectMediaManifest(root) {
21
87
  const files = await scanMediaDirectory(root);
22
88
  return Promise.all(files.map((file) => readMediaFileMeta(root, file)));
@@ -92,9 +158,19 @@ const RESERVED_ENTRY_KEYS = [
92
158
  "cms_users"
93
159
  ];
94
160
  const RESERVED_COLUMNS = ["id", "status", "created_at", "updated_at"];
95
- const TITLE_FIELD_TYPES = ["text", "slug", "email", "number", "date", "select"];
161
+ const TITLE_FIELD_TYPES = ["text", "slug", "email", "number", "date", "datetime", "select"];
96
162
  const TRANSLATABLE_FIELD_TYPES = ["text", "richtext", "media"];
97
- const CONDITION_FIELD_TYPES = ["select", "boolean", "text", "number", "date", "email", "slug"];
163
+ const CONDITION_FIELD_TYPES = [
164
+ "select",
165
+ "boolean",
166
+ "text",
167
+ "number",
168
+ "date",
169
+ "datetime",
170
+ "email",
171
+ "slug"
172
+ ];
173
+ const COMPONENT_RESERVED_KEYS = ["key", "ref", "class", "style"];
98
174
  const RESERVED_TYPE_NAMES = [
99
175
  "Query",
100
176
  "Mutation",
@@ -110,6 +186,7 @@ const RESERVED_TYPE_NAMES = [
110
186
  function validateConfig(config, i18n) {
111
187
  const errors = [];
112
188
  const locales = i18n?.locales ?? [];
189
+ normalizeContentEntries(config, { locales });
113
190
  if (locales.length && !locales.includes(i18n.defaultLocale)) {
114
191
  errors.push(
115
192
  `cms.i18n: defaultLocale '${i18n.defaultLocale}' is not in locales [${locales.join(
@@ -143,8 +220,10 @@ function validateConfig(config, i18n) {
143
220
  } else {
144
221
  entryIds.set(entry.id, name);
145
222
  }
146
- if (entry.kind !== "collection" && entry.kind !== "single" && entry.kind !== "page")
147
- errors.push(`${at}: kind must be 'collection', 'single' or 'page'`);
223
+ if (!["collection", "single", "page", "content"].includes(entry.kind))
224
+ errors.push(`${at}: kind must be 'collection', 'single', 'page' or 'content'`);
225
+ if (entry.kind === "content") errors.push(...contentErrors(at, entry));
226
+ else if (entry.preview !== void 0) errors.push(`${at}: preview needs kind 'content'`);
148
227
  if (entry.kind === "page") {
149
228
  if (pageEntry) {
150
229
  errors.push(
@@ -196,7 +275,7 @@ function validateConfig(config, i18n) {
196
275
  errors.push(`${at}: routes, include, exclude and overrides need kind 'page'`);
197
276
  }
198
277
  if (entry.kind !== "page" && entry.columns) errors.push(`${at}: columns need kind 'page'`);
199
- if (entry.drafts && entry.kind !== "collection")
278
+ if (entry.drafts && !isCollectionKind(entry))
200
279
  errors.push(`${at}: drafts are only supported on collections`);
201
280
  const tabIds = /* @__PURE__ */ new Set();
202
281
  for (const tab of entry.tabs ?? []) {
@@ -237,8 +316,8 @@ function validateConfig(config, i18n) {
237
316
  const allFields = entry.kind === "page" ? pageAllFields(entry) : entry.fields ?? {};
238
317
  errors.push(...layoutErrors(at, allFields, entry.layout));
239
318
  errors.push(...listErrors(at, allFields, entry.list, entry.drafts ? ["status"] : []));
240
- if (entry.list && entry.kind !== "collection")
241
- errors.push(`${at}: list needs kind 'collection'`);
319
+ if (entry.list && !isCollectionKind(entry))
320
+ errors.push(`${at}: list needs kind 'collection' or 'content'`);
242
321
  if (entry.icon !== void 0 && (typeof entry.icon !== "string" || !entry.icon))
243
322
  errors.push(`${at}: icon must be a non-empty string`);
244
323
  const columnNames = /* @__PURE__ */ new Set();
@@ -321,7 +400,7 @@ function validateConfig(config, i18n) {
321
400
  if (!source) errors.push(`${fat}: slug source '${field.from}' is not a declared field`);
322
401
  else if (source.type !== "text")
323
402
  errors.push(`${fat}: slug source '${field.from}' must be a text field`);
324
- else if (isTranslatableField(source))
403
+ else if (isTranslatableField(source) && !(entry.kind === "content" && key === CONTENT_SLUG_FIELD))
325
404
  errors.push(`${fat}: slug source '${field.from}' cannot be translatable`);
326
405
  }
327
406
  if (field.type === "blocks") {
@@ -335,6 +414,7 @@ function validateConfig(config, i18n) {
335
414
  errors.push(`${bat}: key must be a valid identifier`);
336
415
  if (!block.fields || !Object.keys(block.fields).length)
337
416
  errors.push(`${bat}: fields must not be empty`);
417
+ errors.push(...blockMetaErrors(bat, block));
338
418
  for (const [blockFieldKey, blockField] of Object.entries(block.fields ?? {})) {
339
419
  const bfat = `${bat}, field '${blockFieldKey}'`;
340
420
  if (!IDENTIFIER.test(blockFieldKey))
@@ -376,8 +456,10 @@ function validateConfig(config, i18n) {
376
456
  if (!field.to || !target) {
377
457
  errors.push(`${fat}: relation target '${field.to}' is not in the registry`);
378
458
  } else {
379
- if (target.kind !== "collection")
380
- errors.push(`${fat}: relation target '${field.to}' must be a collection`);
459
+ if (!isCollectionKind(target))
460
+ errors.push(
461
+ `${fat}: relation target '${field.to}' must be a collection or a content entry`
462
+ );
381
463
  if (!entry.table && target.table)
382
464
  errors.push(
383
465
  `${fat}: relation target '${field.to}' uses a custom table: derived relations can only target derived entries`
@@ -428,6 +510,51 @@ function validateConfig(config, i18n) {
428
510
  }
429
511
  return errors;
430
512
  }
513
+ function contentErrors(at, entry) {
514
+ const errors = [];
515
+ const system = CONTENT_SYSTEM_FIELDS;
516
+ for (const key of entry.declared?.fields ?? []) {
517
+ if (system.includes(key))
518
+ errors.push(`${at}: '${key}' is a system field of content entries, remove it from fields`);
519
+ }
520
+ if (entry.declared?.titleField !== void 0)
521
+ errors.push(`${at}: content entries use 'title' as titleField, remove titleField`);
522
+ if (entry.declared?.drafts !== void 0)
523
+ errors.push(`${at}: content entries always have drafts, remove the drafts option`);
524
+ if (!entry.blocks || typeof entry.blocks !== "object" || !Object.keys(entry.blocks).length)
525
+ errors.push(`${at}: content entries need a non-empty blocks map for the body`);
526
+ if (entry.table) errors.push(`${at}: content entries cannot use a custom table`);
527
+ if (entry.preview !== void 0) {
528
+ const component = entry.preview?.component;
529
+ if (!entry.preview || typeof entry.preview !== "object")
530
+ errors.push(`${at}: preview must be an object`);
531
+ else if (component !== void 0 && (typeof component !== "string" || !component.trim()))
532
+ errors.push(`${at}: preview.component must be a non-empty string`);
533
+ }
534
+ for (const key of Object.keys(entry.labels ?? {})) {
535
+ if (!system.includes(key))
536
+ errors.push(`${at}: labels names '${key}', which is not a system field`);
537
+ }
538
+ return errors;
539
+ }
540
+ function blockMetaErrors(at, block) {
541
+ const errors = [];
542
+ if (block.component !== void 0) {
543
+ if (typeof block.component !== "string" || !block.component.trim())
544
+ errors.push(`${at}: component must be a non-empty string`);
545
+ for (const key of Object.keys(block.fields ?? {})) {
546
+ if (COMPONENT_RESERVED_KEYS.includes(key))
547
+ errors.push(
548
+ `${at}, field '${key}': '${key}' cannot be a prop of the block component, rename it`
549
+ );
550
+ }
551
+ }
552
+ if (block.icon !== void 0 && (typeof block.icon !== "string" || !block.icon))
553
+ errors.push(`${at}: icon must be a non-empty string`);
554
+ if (block.description !== void 0 && typeof block.description !== "string")
555
+ errors.push(`${at}: description must be a string`);
556
+ return errors;
557
+ }
431
558
  function pageColumnsErrors(at, entry) {
432
559
  const errors = [];
433
560
  if (entry.table) errors.push(`${at}: page entries cannot use a custom table`);
@@ -525,7 +652,7 @@ function tableExpr(name, entry, dialect) {
525
652
  lines.push(columnExpr(key, field, dialect));
526
653
  }
527
654
  if (entry.drafts) lines.push(` status: text('status').notNull().default('draft'),`);
528
- if (entry.kind === "collection") lines.push(` createdAt: ${nowExpr(pg, "created_at")},`);
655
+ if (isCollectionKind(entry)) lines.push(` createdAt: ${nowExpr(pg, "created_at")},`);
529
656
  lines.push(` updatedAt: ${nowExpr(pg, "updated_at")},`);
530
657
  return `export const ${name} = ${tableFn}('${name}', {
531
658
  ${lines.join("\n")}
@@ -638,13 +765,13 @@ ${[
638
765
 
639
766
  const MEDIA_SELECTION = "key url type alt folder mime size width height";
640
767
  function blocksSelection(entryName, key, field) {
641
- const parts = ["type"];
768
+ const parts = ["__typename type"];
642
769
  for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
643
770
  const fields = Object.entries(block.fields).map(
644
771
  ([blockFieldKey, blockField]) => blockField.type === "media" ? `${blockFieldKey} { ${MEDIA_SELECTION} }` : blockFieldKey
645
772
  );
646
773
  if (!fields.length) continue;
647
- parts.push(`... on ${blockTypeName(entryName, key, blockName)} { ${fields.join(" ")} }`);
774
+ parts.push(`... on ${blockTypeName$1(entryName, key, blockName)} { ${fields.join(" ")} }`);
648
775
  }
649
776
  return parts.join(" ");
650
777
  }
@@ -669,7 +796,7 @@ function entrySelection(config, name, entry, withRelations, fields = entryFields
669
796
  }
670
797
  parts.push(key);
671
798
  }
672
- if (entry.kind === "collection") parts.push("createdAt");
799
+ if (isCollectionKind(entry)) parts.push("createdAt");
673
800
  parts.push("updatedAt");
674
801
  return parts.join(" ");
675
802
  }
@@ -677,7 +804,7 @@ function singleQuery(config, name, entry) {
677
804
  const selection = entrySelection(config, name, entry, true);
678
805
  return `query CmsSingle($locale: String) { ${name}(locale: $locale) { ${selection} } }`;
679
806
  }
680
- function collectionQuery(config, name, entry) {
807
+ function collectionQuery(config, name, entry, operation = "CmsCollection") {
681
808
  const selection = entrySelection(config, name, entry, true);
682
809
  const gqlType = typeName(name);
683
810
  const args = [
@@ -687,7 +814,14 @@ function collectionQuery(config, name, entry) {
687
814
  "$limit: Int",
688
815
  "$offset: Int"
689
816
  ].join(", ");
690
- return `query CmsCollection(${args}) { ${name}(locale: $locale, filters: $filters, sort: $sort, limit: $limit, offset: $offset) { ${selection} } }`;
817
+ return `query ${operation}(${args}) { ${name}(locale: $locale, filters: $filters, sort: $sort, limit: $limit, offset: $offset) { ${selection} } }`;
818
+ }
819
+ function contentListQuery(config, name, entry) {
820
+ return collectionQuery(config, name, entry, "CmsContents");
821
+ }
822
+ function contentItemQuery(config, name, entry) {
823
+ const selection = entrySelection(config, name, entry, true);
824
+ return `query CmsContent($slug: String!, $locale: String) { item: ${name}BySlug(slug: $slug, locale: $locale) { ${selection} } }`;
691
825
  }
692
826
  function pageQuery(config, name, entry, path) {
693
827
  const selection = entrySelection(config, name, entry, true, pageFields(entry, path));
@@ -697,7 +831,18 @@ function renderQueriesFile(config) {
697
831
  const singles = [];
698
832
  const collections = [];
699
833
  const pages = [];
834
+ const contents = [];
835
+ const contentItems = [];
700
836
  for (const [name, entry] of Object.entries(config)) {
837
+ if (entry.kind === "content") {
838
+ contents.push(
839
+ ` ${JSON.stringify(name)}: ${JSON.stringify(contentListQuery(config, name, entry))},`
840
+ );
841
+ contentItems.push(
842
+ ` ${JSON.stringify(name)}: ${JSON.stringify(contentItemQuery(config, name, entry))},`
843
+ );
844
+ continue;
845
+ }
701
846
  if (entry.kind === "page") {
702
847
  for (const route of pageRoutes(entry)) {
703
848
  pages.push(
@@ -726,6 +871,14 @@ function renderQueriesFile(config) {
726
871
  `export const cmsPageQueries: Record<string, string> = {`,
727
872
  ...pages,
728
873
  `}`,
874
+ ``,
875
+ `export const cmsContentQueries: Record<string, string> = {`,
876
+ ...contents,
877
+ `}`,
878
+ ``,
879
+ `export const cmsContentItemQueries: Record<string, string> = {`,
880
+ ...contentItems,
881
+ `}`,
729
882
  ``
730
883
  ].join("\n");
731
884
  }
@@ -767,13 +920,17 @@ function fieldTsType(config, entryName, key, field) {
767
920
  const base = scalarTsType(field);
768
921
  return isRequiredField(field) ? base : `${base} | null`;
769
922
  }
923
+ function blockPropsTypeName(entryName, fieldKey, blockName) {
924
+ return `${blockTypeName$1(entryName, fieldKey, blockName)}Props`;
925
+ }
770
926
  function blockTypesTs(entryName, key, field) {
771
927
  const defs = [];
772
928
  const members = [];
773
929
  for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
774
- const name = blockTypeName(entryName, key, blockName);
930
+ const name = blockTypeName$1(entryName, key, blockName);
931
+ const props = blockPropsTypeName(entryName, key, blockName);
775
932
  members.push(name);
776
- const lines = [` __typename?: '${name}'`, ` type: '${blockName}'`];
933
+ const lines = [];
777
934
  for (const [blockFieldKey, blockField] of Object.entries(block.fields)) {
778
935
  if (blockField.type === "media") {
779
936
  lines.push(` ${blockFieldKey}: ${mediaTsType(blockField)} | null`);
@@ -782,9 +939,15 @@ function blockTypesTs(entryName, key, field) {
782
939
  const base = scalarTsType(blockField);
783
940
  lines.push(` ${blockFieldKey}: ${blockField.required ? base : `${base} | null`}`);
784
941
  }
785
- defs.push(`export interface ${name} {
942
+ defs.push(`export interface ${props} {
786
943
  ${lines.join("\n")}
787
944
  }`);
945
+ defs.push(
946
+ `export interface ${name} extends ${props} {
947
+ __typename?: '${name}'
948
+ type: '${blockName}'
949
+ }`
950
+ );
788
951
  }
789
952
  if (members.length)
790
953
  defs.push(`export type ${blocksFieldTypeName(entryName, key)} = ${members.join(" | ")}`);
@@ -797,7 +960,7 @@ function entryTs(config, name, entry) {
797
960
  if (isPrivateField(field)) continue;
798
961
  lines.push(` ${key}: ${fieldTsType(config, name, key, field)}`);
799
962
  }
800
- if (entry.kind === "collection") lines.push(" createdAt: string");
963
+ if (isCollectionKind(entry)) lines.push(" createdAt: string");
801
964
  lines.push(" updatedAt: string");
802
965
  return `export interface ${typeName(name)} {
803
966
  ${lines.join("\n")}
@@ -853,11 +1016,13 @@ ${lines.join("\n")}
853
1016
  function entryMapsTs(config) {
854
1017
  const singles = [];
855
1018
  const collections = [];
1019
+ const contents = [];
856
1020
  const pages = [];
857
1021
  for (const [name, entry] of Object.entries(config)) {
858
1022
  const line = ` ${JSON.stringify(name)}: ${typeName(name)}Auto`;
859
1023
  if (entry.kind === "single") singles.push(line);
860
1024
  else if (entry.kind === "page") pages.push(...pageTypesTs(name, entry));
1025
+ else if (entry.kind === "content") contents.push(line);
861
1026
  else collections.push(line);
862
1027
  }
863
1028
  if (!pages.length) {
@@ -874,12 +1039,50 @@ ${singles.join("\n")}
874
1039
  }`,
875
1040
  `export interface CmsCollectionTypes {
876
1041
  ${collections.join("\n")}
1042
+ }`,
1043
+ `export interface CmsContentTypes {
1044
+ ${contents.join("\n")}
877
1045
  }`,
878
1046
  `export type CmsSingleName = keyof CmsSingleTypes`,
879
1047
  `export type CmsCollectionName = keyof CmsCollectionTypes`,
1048
+ `export type CmsContentName = keyof CmsContentTypes`,
880
1049
  ...pages
881
1050
  ];
882
1051
  }
1052
+ function blockPropsMapTs(config) {
1053
+ const entries = [];
1054
+ for (const [name, entry] of Object.entries(config)) {
1055
+ const fields = [];
1056
+ for (const [key, field] of Object.entries(entryFieldsFor(entry))) {
1057
+ if (field.type !== "blocks" || isPrivateField(field)) continue;
1058
+ const blocks = Object.keys(field.blocks ?? {}).map(
1059
+ (blockName) => ` ${JSON.stringify(blockName)}: ${blockPropsTypeName(name, key, blockName)}`
1060
+ );
1061
+ if (blocks.length)
1062
+ fields.push(` ${JSON.stringify(key)}: {
1063
+ ${blocks.join("\n")}
1064
+ }`);
1065
+ }
1066
+ if (fields.length) entries.push(` ${JSON.stringify(name)}: {
1067
+ ${fields.join("\n")}
1068
+ }`);
1069
+ }
1070
+ return [
1071
+ `export interface CmsBlockPropTypes {
1072
+ ${entries.join("\n")}
1073
+ }`,
1074
+ `export type CmsBlockEntryName = [keyof CmsBlockPropTypes] extends [never]
1075
+ ? string
1076
+ : keyof CmsBlockPropTypes`,
1077
+ [
1078
+ `export type CmsBlockProps<`,
1079
+ ` E extends keyof CmsBlockPropTypes,`,
1080
+ ` F extends keyof CmsBlockPropTypes[E],`,
1081
+ ` B extends keyof CmsBlockPropTypes[E][F],`,
1082
+ `> = CmsBlockPropTypes[E][F][B]`
1083
+ ].join("\n")
1084
+ ];
1085
+ }
883
1086
  function renderTypesFile(config) {
884
1087
  const parts = [
885
1088
  `export type CmsMediaType = 'image' | 'video' | 'file'`,
@@ -904,6 +1107,7 @@ function renderTypesFile(config) {
904
1107
  }
905
1108
  for (const [name, entry] of Object.entries(config)) parts.push(autoTypeTs(config, name, entry));
906
1109
  parts.push(...entryMapsTs(config));
1110
+ parts.push(...blockPropsMapTs(config));
907
1111
  return `${parts.join("\n\n")}
908
1112
  `;
909
1113
  }
@@ -1007,6 +1211,10 @@ function resolveModuleOptions(options) {
1007
1211
  },
1008
1212
  graphql: {
1009
1213
  maxDepth: options.graphql?.maxDepth ?? 8
1214
+ },
1215
+ preview: {
1216
+ path: options.preview?.path ?? DEFAULT_CMS_PREVIEW_PATH,
1217
+ component: options.preview?.component?.trim() ?? ""
1010
1218
  }
1011
1219
  };
1012
1220
  }
@@ -1029,7 +1237,10 @@ async function loadCmsConfig(nuxt, resolver, configPathOption, i18n, logger) {
1029
1237
  moduleCache: false,
1030
1238
  alias: { "#nuxt-cms": resolver.resolve("./runtime/shared/index") }
1031
1239
  });
1032
- cmsConfig = expandMobileMedia(await jiti.import(configPath, { default: true }));
1240
+ cmsConfig = normalizeCmsConfig(
1241
+ await jiti.import(configPath, { default: true }),
1242
+ i18n
1243
+ );
1033
1244
  } else {
1034
1245
  logger.warn(
1035
1246
  `[nuxt-cms] Config file not found: ${configPath}. Using an empty registry. Create a ${configPathOption}.ts with defineCmsConfig().`
@@ -1052,12 +1263,12 @@ async function loadCmsConfig(nuxt, resolver, configPathOption, i18n, logger) {
1052
1263
  filename: "cms/config.ts",
1053
1264
  write: true,
1054
1265
  getContents: () => [
1055
- `import { expandMobileMedia } from '#nuxt-cms'`,
1266
+ `import { normalizeCmsConfig } from '#nuxt-cms'`,
1056
1267
  `import config from '${source}'`,
1057
1268
  ``,
1058
1269
  `const pageRoutes = ${JSON.stringify(routesByEntry, null, 3)}`,
1059
1270
  ``,
1060
- `expandMobileMedia(config)`,
1271
+ `normalizeCmsConfig(config, ${JSON.stringify({ locales: i18n.locales })})`,
1061
1272
  ``,
1062
1273
  `for (const [name, routes] of Object.entries(pageRoutes)) {`,
1063
1274
  ` const entry = Object(config)[name]`,
@@ -1141,6 +1352,31 @@ function addCmsTypeTemplates(nuxt, cmsConfig) {
1141
1352
  });
1142
1353
  nuxt.options.alias["#cms-graphql"] = graphqlTemplate.dst;
1143
1354
  }
1355
+ function addCmsBlocks(nuxt, resolver, cmsConfig, previewComponent) {
1356
+ const refs = collectBlockComponents(cmsConfig);
1357
+ const previews = collectPreviewComponents(cmsConfig, previewComponent);
1358
+ const blocksTemplate = addTemplate({
1359
+ filename: "cms/blocks.ts",
1360
+ write: true,
1361
+ getContents: () => renderBlocksFile(refs, previews)
1362
+ });
1363
+ nuxt.options.alias["#cms-blocks"] = blocksTemplate.dst;
1364
+ addComponent({
1365
+ name: "CmsBlocks",
1366
+ filePath: resolver.resolve("./runtime/app/blocks/CmsBlocks.vue")
1367
+ });
1368
+ if (!refs.length && !previews.length) return;
1369
+ nuxt.hook("components:extend", (components) => {
1370
+ const names = components.map((component) => component.pascalName);
1371
+ const errors = [
1372
+ ...missingBlockComponents(refs, names),
1373
+ ...missingPreviewComponents(previews, names)
1374
+ ];
1375
+ if (errors.length)
1376
+ throw new Error(`[nuxt-cms] Invalid block components:
1377
+ ${errors.join("\n")}`);
1378
+ });
1379
+ }
1144
1380
  const module$1 = defineNuxtModule({
1145
1381
  meta: {
1146
1382
  name: "@xleddyl/nuxt-cms",
@@ -1192,17 +1428,25 @@ const module$1 = defineNuxtModule({
1192
1428
  { name: "$cmsQuery", from: queryStub },
1193
1429
  { name: "useCmsSingle", from: entryStub },
1194
1430
  { name: "useCmsCollection", from: entryStub },
1195
- { name: "useCmsPage", from: entryStub }
1431
+ { name: "useCmsPage", from: entryStub },
1432
+ { name: "useCmsContents", from: entryStub },
1433
+ { name: "useCmsContent", from: entryStub }
1196
1434
  ]);
1197
- addCmsTypeTemplates(
1435
+ const disabledConfig = await loadCmsConfig(
1198
1436
  nuxt,
1199
- await loadCmsConfig(nuxt, resolver, resolved.configPath, resolved.i18n, logger)
1437
+ resolver,
1438
+ resolved.configPath,
1439
+ resolved.i18n,
1440
+ logger
1200
1441
  );
1442
+ addCmsTypeTemplates(nuxt, disabledConfig);
1443
+ addCmsBlocks(nuxt, resolver, disabledConfig, resolved.preview.component);
1201
1444
  nuxt.options.runtimeConfig.public.cms = {
1202
1445
  mediaBaseUrl: resolved.media.publicBaseUrl,
1203
1446
  mediaStorage: resolved.media.storage,
1204
1447
  mediaMaxFileSize: resolved.media.maxFileSize,
1205
- i18n: resolved.i18n
1448
+ i18n: resolved.i18n,
1449
+ previewPath: resolved.preview.path
1206
1450
  };
1207
1451
  logger.info(
1208
1452
  "[nuxt-cms] disabled: registering no-op query composables and generated types, skipping admin, server and database setup"
@@ -1255,8 +1499,26 @@ const module$1 = defineNuxtModule({
1255
1499
  { name: "$cmsQuery", from: queryComposables },
1256
1500
  { name: "useCmsSingle", from: entryComposables },
1257
1501
  { name: "useCmsCollection", from: entryComposables },
1258
- { name: "useCmsPage", from: entryComposables }
1502
+ { name: "useCmsPage", from: entryComposables },
1503
+ { name: "useCmsContents", from: entryComposables },
1504
+ { name: "useCmsContent", from: entryComposables }
1259
1505
  ]);
1506
+ addCmsBlocks(nuxt, resolver, cmsConfig, resolved.preview.component);
1507
+ const previewErrors = previewPathErrors(resolved.preview.path, Object.keys(cmsConfig));
1508
+ if (previewErrors.length) throw new Error(`[nuxt-cms] ${previewErrors.join("\n")}`);
1509
+ const previewPath = resolved.preview.path;
1510
+ const previewRule = nuxt.options.routeRules?.[previewPath] ?? {};
1511
+ nuxt.options.routeRules = {
1512
+ ...nuxt.options.routeRules,
1513
+ [previewPath]: {
1514
+ ...previewRule,
1515
+ headers: {
1516
+ "x-robots-tag": "noindex, nofollow",
1517
+ "x-frame-options": "SAMEORIGIN",
1518
+ ...previewRule.headers
1519
+ }
1520
+ }
1521
+ };
1260
1522
  const {
1261
1523
  driver,
1262
1524
  path: dbPath,
@@ -1433,7 +1695,8 @@ const module$1 = defineNuxtModule({
1433
1695
  mediaBaseUrl: resolved.media.publicBaseUrl,
1434
1696
  mediaStorage: resolved.media.storage,
1435
1697
  mediaMaxFileSize: resolved.media.maxFileSize,
1436
- i18n: resolved.i18n
1698
+ i18n: resolved.i18n,
1699
+ previewPath
1437
1700
  };
1438
1701
  addTypeTemplate(
1439
1702
  {
@@ -1484,14 +1747,38 @@ const module$1 = defineNuxtModule({
1484
1747
  name: "cms-admin",
1485
1748
  file: resolver.resolve("./runtime/app/layouts/cms-admin.vue")
1486
1749
  };
1750
+ app.layouts["cms-editor"] = {
1751
+ name: "cms-editor",
1752
+ file: resolver.resolve("./runtime/app/layouts/cms-editor.vue")
1753
+ };
1487
1754
  });
1488
1755
  addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
1489
1756
  addRouteMiddleware({
1490
1757
  name: "cms-auth",
1491
1758
  path: resolver.resolve("./runtime/app/middleware/cms-auth")
1492
1759
  });
1760
+ const contentPages = Object.entries(cmsConfig).filter(([, entry]) => entry.kind === "content").flatMap(([name]) => [
1761
+ {
1762
+ name: `cms-admin-content-${name}-new`,
1763
+ path: `/cms/${name}/new`,
1764
+ file: resolver.resolve("./runtime/app/pages/admin-content.vue"),
1765
+ meta: { cmsEntry: name }
1766
+ },
1767
+ {
1768
+ name: `cms-admin-content-${name}`,
1769
+ path: `/cms/${name}/:id()`,
1770
+ file: resolver.resolve("./runtime/app/pages/admin-content.vue"),
1771
+ meta: { cmsEntry: name }
1772
+ }
1773
+ ]);
1493
1774
  extendPages((pages) => {
1494
1775
  pages.unshift(
1776
+ {
1777
+ name: "cms-preview",
1778
+ path: previewPath,
1779
+ file: resolver.resolve("./runtime/app/pages/admin-preview.vue")
1780
+ },
1781
+ ...contentPages,
1495
1782
  {
1496
1783
  name: "cms-admin",
1497
1784
  path: "/cms",
@@ -1584,6 +1871,11 @@ const module$1 = defineNuxtModule({
1584
1871
  method: "post",
1585
1872
  handler: resolver.resolve("./runtime/server/api/media-usage.post")
1586
1873
  });
1874
+ addServerHandler({
1875
+ route: "/api/cms/admin/media/download",
1876
+ method: "get",
1877
+ handler: resolver.resolve("./runtime/server/api/media-download.get")
1878
+ });
1587
1879
  if (mediaFilesystemBase) {
1588
1880
  addServerHandler({
1589
1881
  route: "/api/cms/admin/media/upload",
@@ -1595,6 +1887,11 @@ const module$1 = defineNuxtModule({
1595
1887
  handler: resolver.resolve("./runtime/server/routes/media-file")
1596
1888
  });
1597
1889
  }
1890
+ addServerHandler({
1891
+ route: "/api/cms/search",
1892
+ method: "get",
1893
+ handler: resolver.resolve("./runtime/server/api/search.get")
1894
+ });
1598
1895
  addServerHandler({
1599
1896
  route: "/api/cms/users",
1600
1897
  method: "get",
@@ -0,0 +1,45 @@
1
+ import type { PropType } from 'vue';
2
+ import type { CmsBlockEntryName } from '#cms-types';
3
+ import type { CmsBlockItem } from './resolve.js';
4
+ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
5
+ blocks: {
6
+ type: PropType<readonly CmsBlockItem[] | null>;
7
+ default: null;
8
+ };
9
+ entry: {
10
+ type: PropType<CmsBlockEntryName>;
11
+ default: undefined;
12
+ };
13
+ field: {
14
+ type: StringConstructor;
15
+ default: undefined;
16
+ };
17
+ annotate: {
18
+ type: BooleanConstructor;
19
+ default: boolean;
20
+ };
21
+ }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
22
+ blocks: {
23
+ type: PropType<readonly CmsBlockItem[] | null>;
24
+ default: null;
25
+ };
26
+ entry: {
27
+ type: PropType<CmsBlockEntryName>;
28
+ default: undefined;
29
+ };
30
+ field: {
31
+ type: StringConstructor;
32
+ default: undefined;
33
+ };
34
+ annotate: {
35
+ type: BooleanConstructor;
36
+ default: boolean;
37
+ };
38
+ }>> & Readonly<{}>, {
39
+ field: string;
40
+ blocks: readonly CmsBlockItem[] | null;
41
+ entry: string;
42
+ annotate: boolean;
43
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
44
+ declare const _default: typeof __VLS_export;
45
+ export default _default;