@xleddyl/nuxt-cms 0.1.18 → 0.1.21

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/README.md CHANGED
@@ -34,12 +34,28 @@ export default defineNuxtConfig({
34
34
 
35
35
  Then declare your content types in a `cms.config.ts` at the project root with `defineCmsConfig()`.
36
36
 
37
+ ### Disabling the CMS
38
+
39
+ Keep the module in `modules[]` at all times and turn it off with the `enabled` option or the
40
+ `NUXT_CMS_ENABLED` env var. When disabled the module registers no-op `useCms` / `$cmsQuery` stubs and
41
+ nothing else, so components can call them unconditionally and simply render their empty states:
42
+
43
+ ```ts
44
+ export default defineNuxtConfig({
45
+ modules: ['@xleddyl/nuxt-cms'],
46
+ cms: { enabled: false },
47
+ })
48
+ ```
49
+
50
+ See [Configuration](docs/configuration.md#enabled) for the resolution order.
51
+
37
52
  ### Environment variables
38
53
 
39
54
  Every secret maps to runtime config, so it can be set as an env var instead of in `nuxt.config.ts`:
40
55
 
41
56
  | Variable | Required | Purpose |
42
57
  | --- | --- | --- |
58
+ | `NUXT_CMS_ENABLED` | no | set to `0` / `false` to disable the CMS (default enabled) |
43
59
  | `NUXT_CMS_ADMIN_EMAIL` | yes | admin login email |
44
60
  | `NUXT_CMS_ADMIN_PASSWORD` | yes | admin login password |
45
61
  | `NUXT_SESSION_PASSWORD` | in production | session encryption key (32+ chars) |
package/dist/module.d.mts CHANGED
@@ -4,6 +4,7 @@ type Dialect = 'sqlite' | 'postgres';
4
4
  type Driver = Dialect | 'libsql';
5
5
 
6
6
  interface ModuleOptions {
7
+ enabled?: boolean;
7
8
  configPath: string;
8
9
  admin: {
9
10
  email: string;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
3
  "configKey": "cms",
4
- "version": "0.1.18",
4
+ "version": "0.1.21",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -3,14 +3,14 @@ import { existsSync } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import { isAbsolute, resolve, relative, join, dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { defineNuxtModule, createResolver, useLogger, resolvePath, addTemplate, addImports, addServerPlugin, addTypeTemplate, addVitePlugin, addLayout, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler } from '@nuxt/kit';
6
+ import { defineNuxtModule, createResolver, useLogger, addImports, resolvePath, addTemplate, addServerPlugin, addTypeTemplate, addVitePlugin, addLayout, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler } 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
12
  import { typeName, blockTypeName, blockUnionName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
13
- import { isTranslatableField } from '../dist/runtime/shared/index.js';
13
+ import { isTranslatableField, isMultiSelect } from '../dist/runtime/shared/index.js';
14
14
 
15
15
  const IDENTIFIER = /^[a-z_]\w*$/i;
16
16
  const RESERVED_ENTRY_KEYS = ["admin", "auth", "login", "media", "graphql", "cms_media"];
@@ -129,6 +129,9 @@ function validateConfig(config, i18n) {
129
129
  if (blockField.type === "select" && !blockField.options?.length) {
130
130
  errors.push(`${bfat}: select requires a non-empty options array`);
131
131
  }
132
+ if (blockField.type === "select" && blockField.multiple) {
133
+ errors.push(`${bfat}: multiple select is not supported inside blocks`);
134
+ }
132
135
  }
133
136
  }
134
137
  }
@@ -209,6 +212,9 @@ function columnExpr(key, field, dialect) {
209
212
  case "blocks":
210
213
  expr = jsonExpr(col, dialect);
211
214
  break;
215
+ case "select":
216
+ expr = field.multiple ? jsonExpr(col, dialect) : `text('${col}')`;
217
+ break;
212
218
  case "slug":
213
219
  expr = `text('${col}').unique()`;
214
220
  break;
@@ -284,7 +290,9 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
284
290
  core.add(pg ? "doublePrecision" : "real");
285
291
  if (pg && fields.some((f) => f.type === "date")) core.add("date");
286
292
  if (pg && fields.some((f) => f.type === "boolean")) core.add("boolean");
287
- if (pg && fields.some((f) => f.type === "json" || f.type === "blocks" || isTranslatableField(f)))
293
+ if (pg && fields.some(
294
+ (f) => f.type === "json" || f.type === "blocks" || isTranslatableField(f) || isMultiSelect(f)
295
+ ))
288
296
  core.add("jsonb");
289
297
  if (pg) core.add("timestamp");
290
298
  if (fields.some(isManyToMany)) core.add("primaryKey");
@@ -327,6 +335,10 @@ function fieldTsType(config, entryName, key, field) {
327
335
  return field.required && !config[field.to]?.drafts ? target : `${target} | null`;
328
336
  }
329
337
  if (field.type === "media") return "CmsMedia | null";
338
+ if (field.type === "select" && field.multiple) {
339
+ const union = field.options.map((o) => JSON.stringify(o)).join(" | ");
340
+ return `(${union})[]`;
341
+ }
330
342
  if (field.type === "blocks") {
331
343
  const union = blockUnionName(entryName, key);
332
344
  return field.required ? `${union}[]` : `${union}[] | null`;
@@ -393,14 +405,25 @@ function renderTypesFile(config) {
393
405
  `;
394
406
  }
395
407
 
408
+ const CMS_ENABLED_ENV = "NUXT_CMS_ENABLED";
409
+ function resolveCmsEnabled(explicit, envValue) {
410
+ if (typeof explicit === "boolean") return explicit;
411
+ if (typeof envValue === "string") {
412
+ const normalized = envValue.trim().toLowerCase();
413
+ return normalized === "1" || normalized === "true";
414
+ }
415
+ return true;
416
+ }
417
+
396
418
  const module$1 = defineNuxtModule({
397
419
  meta: {
398
420
  name: "@xleddyl/nuxt-cms",
399
421
  configKey: "cms"
400
422
  },
401
- moduleDependencies: {
402
- "nuxt-auth-utils": {}
403
- },
423
+ moduleDependencies: (nuxt) => resolveCmsEnabled(
424
+ nuxt.options.cms?.enabled,
425
+ process.env[CMS_ENABLED_ENV]
426
+ ) ? { "nuxt-auth-utils": {} } : {},
404
427
  defaults: {
405
428
  configPath: "cms.config",
406
429
  admin: {
@@ -433,6 +456,21 @@ const module$1 = defineNuxtModule({
433
456
  async setup(options, nuxt) {
434
457
  const resolver = createResolver(import.meta.url);
435
458
  const logger = useLogger("nuxt-cms");
459
+ if (!resolveCmsEnabled(options.enabled, process.env[CMS_ENABLED_ENV])) {
460
+ const stub = resolver.resolve("./runtime/app/composables/cms-query-disabled");
461
+ addImports([
462
+ { name: "useCms", from: stub },
463
+ { name: "$cmsQuery", from: stub }
464
+ ]);
465
+ nuxt.options.runtimeConfig.public.cms = {
466
+ mediaBaseUrl: options.media.publicBaseUrl,
467
+ i18n: options.i18n
468
+ };
469
+ logger.info(
470
+ "[nuxt-cms] disabled: registering no-op useCms/$cmsQuery stubs, skipping admin, server and database setup"
471
+ );
472
+ return;
473
+ }
436
474
  const configPath = await resolvePath(options.configPath, { cwd: nuxt.options.rootDir });
437
475
  nuxt.options.alias["#nuxt-cms"] = resolver.resolve("./runtime/shared/index");
438
476
  nuxt.options.watch.push(configPath);
@@ -556,7 +594,7 @@ const module$1 = defineNuxtModule({
556
594
  `export default {`,
557
595
  ` dialect: '${dialect}',`,
558
596
  ` schema: '${toPosix(schemaTemplate.dst)}',`,
559
- ` out: '${toPosix(migrationsDir)}',`,
597
+ ` out: '${toPosix(relativeMigrationsDir)}',`,
560
598
  driver === "postgres" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? '${databaseUrl}' },` : driver === "libsql" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? '${databaseUrl || `file:${toPosix(resolvedDbPath)}`}', authToken: (process.env.NUXT_CMS_DATABASE_AUTH_TOKEN ?? '${databaseAuthToken}') || undefined },` : ` dbCredentials: { url: '${toPosix(resolvedDbPath)}' },`,
561
599
  `}`,
562
600
  ``
@@ -644,7 +682,7 @@ const module$1 = defineNuxtModule({
644
682
  addVitePlugin(tailwindcss());
645
683
  addVitePlugin(svgLoader({ defaultImport: "url", svgoConfig: { plugins: ["prefixIds"] } }));
646
684
  nuxt.options.css.push(resolver.resolve("./runtime/assets/main.css"));
647
- addLayout({ src: resolver.resolve("./runtime/app/layouts/cms-admin.vue") }, "cms-admin");
685
+ addLayout({ src: resolver.resolve("./runtime/app/layouts/cms-admin.vue"), write: true }, "cms-admin");
648
686
  addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
649
687
  addRouteMiddleware({
650
688
  name: "cms-auth",
@@ -21,6 +21,7 @@
21
21
  <CmsSelectMenu
22
22
  v-else-if="field.type === 'select'"
23
23
  v-model="selValue"
24
+ :multiple="field.multiple"
24
25
  :items="selectItems"
25
26
  placeholder="Select…"
26
27
  size="lg"
@@ -63,7 +64,7 @@ const props = defineProps({
63
64
  });
64
65
  const model = defineModel({ type: null, ...{ required: true } });
65
66
  const selectItems = computed(() => [
66
- ...props.field.required ? [] : [{ label: "\u2014", value: null }],
67
+ ...props.field.required || props.field.multiple ? [] : [{ label: "\u2014", value: null }],
67
68
  ...(props.field.options ?? []).map((option) => ({ label: option, value: option }))
68
69
  ]);
69
70
  function proxy(fromModel, toModel = (v) => v) {
@@ -79,7 +80,9 @@ const str = proxy(
79
80
  (v) => v === "" ? null : v
80
81
  );
81
82
  const strOrNull = proxy((v) => v);
82
- const selValue = proxy((v) => v ?? null);
83
+ const selValue = proxy(
84
+ (v) => props.field.multiple ? v ?? [] : v ?? null
85
+ );
83
86
  const num = proxy(
84
87
  (v) => v ?? void 0,
85
88
  (v) => typeof v === "number" && !Number.isNaN(v) ? v : null
@@ -0,0 +1,6 @@
1
+ import type { AsyncData } from 'nuxt/app';
2
+ type CmsDisabledResult = Record<string, any>;
3
+ type CmsDisabledVariables = Record<string, any>;
4
+ export declare function $cmsQuery<const Q extends string>(query: Q, variables?: CmsDisabledVariables): Promise<CmsDisabledResult>;
5
+ export declare function useCms<const Q extends string>(query: Q, variables?: CmsDisabledVariables): AsyncData<CmsDisabledResult | undefined, Error | undefined>;
6
+ export {};
@@ -0,0 +1,10 @@
1
+ import { useAsyncData } from "#imports";
2
+ export async function $cmsQuery(query, variables) {
3
+ return {};
4
+ }
5
+ export function useCms(query, variables) {
6
+ return useAsyncData(
7
+ `cms-gql:${query}:${JSON.stringify(variables ?? {})}`,
8
+ async () => null
9
+ );
10
+ }
@@ -27,6 +27,8 @@ function filterScalarFor(field) {
27
27
  return null;
28
28
  case "blocks":
29
29
  return null;
30
+ case "select":
31
+ return field.multiple ? null : "StringFilter";
30
32
  case "relation":
31
33
  return field.cardinality === "many-to-many" ? null : "StringFilter";
32
34
  case "number":
@@ -54,6 +56,7 @@ function fieldSdl(config, entryName, key, field) {
54
56
  return ` ${key}: ${target}${nonNull ? "!" : ""}`;
55
57
  }
56
58
  if (field.type === "media") return ` ${key}: CmsMedia`;
59
+ if (field.type === "select" && field.multiple) return ` ${key}: [String!]!`;
57
60
  if (field.type === "blocks")
58
61
  return ` ${key}: [${blockUnionName(entryName, key)}!]${field.required ? "!" : ""}`;
59
62
  return ` ${key}: ${scalarFor(field)}${field.required ? "!" : ""}`;
@@ -39,6 +39,7 @@ export interface FieldConfig {
39
39
  integer?: boolean;
40
40
  translatable?: boolean;
41
41
  options?: string[];
42
+ multiple?: boolean;
42
43
  from?: string;
43
44
  blocks?: Record<string, BlockConfig>;
44
45
  mediaType?: MediaType;
@@ -48,6 +49,7 @@ export interface FieldConfig {
48
49
  onDelete?: 'set null' | 'cascade' | 'restrict';
49
50
  }
50
51
  export declare function isTranslatableField(field: FieldConfig): boolean;
52
+ export declare function isMultiSelect(field: FieldConfig): boolean;
51
53
  export declare function translatableFieldKeys(entry: CmsEntry): string[];
52
54
  export interface CmsEntry {
53
55
  id: string;
@@ -92,6 +94,7 @@ export interface SlugFieldInput extends FieldInputBase {
92
94
  export interface SelectFieldInput extends FieldInputBase {
93
95
  type: 'select';
94
96
  options: string[];
97
+ multiple?: boolean;
95
98
  }
96
99
  export interface JsonFieldInput extends FieldInputBase {
97
100
  type: 'json';
@@ -47,6 +47,9 @@ export function slugify(value) {
47
47
  export function isTranslatableField(field) {
48
48
  return !!field.translatable && (field.type === "text" || field.type === "richtext");
49
49
  }
50
+ export function isMultiSelect(field) {
51
+ return field.type === "select" && !!field.multiple;
52
+ }
50
53
  export function translatableFieldKeys(entry) {
51
54
  return Object.entries(entry.fields).filter(([, field]) => isTranslatableField(field)).map(([key]) => key);
52
55
  }
@@ -58,6 +58,11 @@ export function buildEntrySchema(entry, i18n, messages) {
58
58
  shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? []);
59
59
  continue;
60
60
  }
61
+ if (field.type === "select" && field.multiple) {
62
+ const list = z.array(z.enum(field.options));
63
+ shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? []);
64
+ continue;
65
+ }
61
66
  if (field.type === "blocks") {
62
67
  const list = blocksSchema(field, m);
63
68
  shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
- "version": "0.1.18",
3
+ "version": "0.1.21",
4
4
  "description": "Lightweight CMS that ships with your Nuxt app: runs on the Nitro server, content types defined in code, /cms admin panel, GraphQL API, SQLite or Postgres. No external CMS needed!",
5
5
  "license": "MIT",
6
6
  "author": "Edoardo Alberti (https://github.com/xleddyl)",
@@ -103,6 +103,6 @@
103
103
  "test": "vitest run",
104
104
  "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
105
105
  "format": "prettier --write \"**/*.{ts,tsx,js,jsx,md,html,json,sql,vue}\" --log-level error",
106
- "release": "changelogen --release --push"
106
+ "release": "changelogen --release && git push origin main && git push origin v$(node -p \"require('./package.json').version\")"
107
107
  }
108
108
  }