@xleddyl/nuxt-cms 0.1.41 → 0.1.43
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 +5 -4
- package/dist/module.json +1 -1
- package/dist/module.mjs +424 -104
- package/dist/runtime/app/composables/cms-entry-disabled.d.ts +23 -0
- package/dist/runtime/app/composables/cms-entry-disabled.js +23 -0
- package/dist/runtime/app/composables/cms-entry.d.ts +23 -0
- package/dist/runtime/app/composables/cms-entry.js +67 -0
- package/dist/runtime/app/layouts/cms-admin.vue +5 -0
- package/dist/runtime/app/pages/admin-collection.vue +44 -1
- package/dist/runtime/app/pages/admin-entry.vue +14 -6
- package/dist/runtime/server/api/collection.get.js +19 -0
- package/dist/runtime/server/api/item.delete.js +4 -1
- package/dist/runtime/server/api/item.get.js +13 -2
- package/dist/runtime/server/api/item.put.js +14 -1
- package/dist/runtime/server/utils/graphql.js +21 -4
- package/dist/runtime/server/utils/registry.d.ts +3 -2
- package/dist/runtime/server/utils/registry.js +14 -3
- package/dist/runtime/server/utils/relations.js +4 -3
- package/dist/runtime/shared/graphql-sdl.js +13 -2
- package/dist/runtime/shared/index.d.ts +43 -6
- package/dist/runtime/shared/index.js +49 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,8 +57,9 @@ Then declare your content types in a `cms.config.ts` at the project root with `d
|
|
|
57
57
|
### Disabling the CMS
|
|
58
58
|
|
|
59
59
|
Keep the module in `modules[]` at all times and turn it off with the `enabled` option or the
|
|
60
|
-
`NUXT_CMS_ENABLED` env var. When disabled the module registers no-op
|
|
61
|
-
nothing else, so components can call them unconditionally and simply render
|
|
60
|
+
`NUXT_CMS_ENABLED` env var. When disabled the module registers no-op query composables and keeps the
|
|
61
|
+
generated types, and nothing else, so components can call them unconditionally and simply render
|
|
62
|
+
their empty states:
|
|
62
63
|
|
|
63
64
|
```ts
|
|
64
65
|
export default defineNuxtConfig({
|
|
@@ -96,8 +97,8 @@ Full documentation lives in [`docs/`](docs/README.md):
|
|
|
96
97
|
- [Getting started](docs/getting-started.md) — install, configure, first content type, run.
|
|
97
98
|
- [Configuration](docs/configuration.md) — every `cms.*` option and the `NUXT_CMS_*` env vars.
|
|
98
99
|
- [Database](docs/database.md) — SQLite, Postgres, libSQL/Turso and D1 drivers, migrations, studio.
|
|
99
|
-
- [Schema](docs/schema.md) — `defineCmsConfig`, entries, field types, relations, blocks, i18n.
|
|
100
|
-
- [Querying content](docs/querying.md) — GraphQL API, `useCms` / `$cmsQuery`, filters, sorting, pagination.
|
|
100
|
+
- [Schema](docs/schema.md) — `defineCmsConfig`, entries, pages, field types, relations, blocks, i18n.
|
|
101
|
+
- [Querying content](docs/querying.md) — GraphQL API, `useCmsSingle` / `useCmsCollection` / `useCmsPage` / `useCms` / `$cmsQuery`, filters, sorting, pagination.
|
|
101
102
|
- [Admin panel & security](docs/admin.md) — pages, authentication, sessions, admin REST API.
|
|
102
103
|
- [Media](docs/media.md) — S3-compatible storage or local mode backed by your `public/` folder, upload flow, allowed file types.
|
|
103
104
|
- [Deployment](docs/deployment.md) — host/driver matrix, migrations on serverless, horizontal scaling, Cloudflare Workers.
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readdirSync } 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,
|
|
6
|
+
import { defineNuxtModule, createResolver, useLogger, addImports, addTemplate, addServerPlugin, addTypeTemplate, addVitePlugin, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler, resolvePath } 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, blocksFieldTypeName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
|
|
13
|
-
import { isMultiSelect,
|
|
13
|
+
import { isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, PAGE_PATH_FIELD, pageRoutes, fieldConditions, isRequiredField, pageFields, isPrivateField, entryFieldsFor, pageLabelFromPath, pageKeyFromPath, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
|
|
14
14
|
import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
16
|
import { readFile } from 'node:fs/promises';
|
|
@@ -105,6 +105,7 @@ function validateConfig(config, i18n) {
|
|
|
105
105
|
)}]`
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
|
+
let pageEntry;
|
|
108
109
|
const entryIds = /* @__PURE__ */ new Map();
|
|
109
110
|
const typeNames = /* @__PURE__ */ new Map();
|
|
110
111
|
for (const [name, entry] of Object.entries(config)) {
|
|
@@ -130,8 +131,51 @@ function validateConfig(config, i18n) {
|
|
|
130
131
|
} else {
|
|
131
132
|
entryIds.set(entry.id, name);
|
|
132
133
|
}
|
|
133
|
-
if (entry.kind !== "collection" && entry.kind !== "single")
|
|
134
|
-
errors.push(`${at}: kind must be 'collection' or '
|
|
134
|
+
if (entry.kind !== "collection" && entry.kind !== "single" && entry.kind !== "page")
|
|
135
|
+
errors.push(`${at}: kind must be 'collection', 'single' or 'page'`);
|
|
136
|
+
if (entry.kind === "page") {
|
|
137
|
+
if (pageEntry) {
|
|
138
|
+
errors.push(
|
|
139
|
+
`${at}: only one page entry is allowed (already declared as '${pageEntry}')`
|
|
140
|
+
);
|
|
141
|
+
} else {
|
|
142
|
+
pageEntry = name;
|
|
143
|
+
}
|
|
144
|
+
if (entry.titleField) errors.push(`${at}: pages have no titleField`);
|
|
145
|
+
if (Object.hasOwn(entry.fields ?? {}, PAGE_PATH_FIELD))
|
|
146
|
+
errors.push(`${at}: '${PAGE_PATH_FIELD}' is a reserved field name on pages`);
|
|
147
|
+
const known = new Set(pageRoutes(entry).map((route) => route.path));
|
|
148
|
+
const keys = /* @__PURE__ */ new Map();
|
|
149
|
+
for (const route of pageRoutes(entry)) {
|
|
150
|
+
const clash = keys.get(route.key);
|
|
151
|
+
if (clash) errors.push(`${at}: paths '${clash}' and '${route.path}' give the same key`);
|
|
152
|
+
else keys.set(route.key, route.path);
|
|
153
|
+
}
|
|
154
|
+
for (const [path, override] of Object.entries(entry.overrides ?? {})) {
|
|
155
|
+
const oat = `${at}, override '${path}'`;
|
|
156
|
+
if (known.size && !known.has(path)) errors.push(`${oat}: '${path}' is not a page path`);
|
|
157
|
+
for (const key of Object.keys(override)) {
|
|
158
|
+
if (Object.hasOwn(entry.fields ?? {}, key))
|
|
159
|
+
errors.push(`${oat}: field '${key}' is already declared for every page`);
|
|
160
|
+
if (key === PAGE_PATH_FIELD)
|
|
161
|
+
errors.push(`${oat}: '${PAGE_PATH_FIELD}' is a reserved field name on pages`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const declared = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const [path, override] of Object.entries(entry.overrides ?? {})) {
|
|
166
|
+
for (const [key, field] of Object.entries(override)) {
|
|
167
|
+
const seen = declared.get(key);
|
|
168
|
+
if (seen && seen !== field.type) {
|
|
169
|
+
errors.push(
|
|
170
|
+
`${at}: field '${key}' is declared as '${seen}' and '${field.type}' by different pages`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
declared.set(key, field.type);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else if (entry.overrides || entry.routes || entry.include || entry.exclude) {
|
|
177
|
+
errors.push(`${at}: routes, include, exclude and overrides need kind 'page'`);
|
|
178
|
+
}
|
|
135
179
|
if (entry.drafts && entry.kind !== "collection")
|
|
136
180
|
errors.push(`${at}: drafts are only supported on collections`);
|
|
137
181
|
if (!entry.fields || !Object.keys(entry.fields).length)
|
|
@@ -150,8 +194,9 @@ function validateConfig(config, i18n) {
|
|
|
150
194
|
);
|
|
151
195
|
}
|
|
152
196
|
}
|
|
197
|
+
const allFields = entry.kind === "page" ? pageAllFields(entry) : entry.fields ?? {};
|
|
153
198
|
const columnNames = /* @__PURE__ */ new Set();
|
|
154
|
-
for (const [key, field] of Object.entries(
|
|
199
|
+
for (const [key, field] of Object.entries(allFields)) {
|
|
155
200
|
const fat = `${at}, field '${key}'`;
|
|
156
201
|
if (!IDENTIFIER.test(key)) errors.push(`${fat}: key must be a valid identifier`);
|
|
157
202
|
const column = snakeCase(key);
|
|
@@ -181,7 +226,7 @@ function validateConfig(config, i18n) {
|
|
|
181
226
|
errors.push(`${fat}: the titleField cannot be conditional`);
|
|
182
227
|
for (const condition of fieldConditions(field)) {
|
|
183
228
|
const cat = `${fat}, showIf on '${condition.field}'`;
|
|
184
|
-
const target =
|
|
229
|
+
const target = allFields[condition.field];
|
|
185
230
|
if (!condition.field || !target) {
|
|
186
231
|
errors.push(`${cat}: '${condition.field}' is not a declared field`);
|
|
187
232
|
continue;
|
|
@@ -380,7 +425,10 @@ function tableExpr(name, entry, dialect) {
|
|
|
380
425
|
const tableFn = pg ? "pgTable" : "sqliteTable";
|
|
381
426
|
const lines = [];
|
|
382
427
|
lines.push(` id: text('id').primaryKey(),`);
|
|
383
|
-
|
|
428
|
+
if (entry.kind === "page") lines.push(` path: text('path').notNull().unique(),`);
|
|
429
|
+
for (const [key, field] of Object.entries(
|
|
430
|
+
entry.kind === "page" ? pageAllFields(entry) : entry.fields
|
|
431
|
+
)) {
|
|
384
432
|
if (isManyToMany(field)) continue;
|
|
385
433
|
lines.push(columnExpr(key, field, dialect));
|
|
386
434
|
}
|
|
@@ -440,7 +488,7 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
|
|
|
440
488
|
];
|
|
441
489
|
const tables = derived.map(([name, entry]) => tableExpr(name, entry, dialect));
|
|
442
490
|
const joins = derived.flatMap(
|
|
443
|
-
([name, entry]) => Object.entries(entry.fields).filter(([, field]) => isManyToMany(field)).map(([key, field]) => joinTableExpr(name, key, field, dialect))
|
|
491
|
+
([name, entry]) => Object.entries(entry.kind === "page" ? pageAllFields(entry) : entry.fields).filter(([, field]) => isManyToMany(field)).map(([key, field]) => joinTableExpr(name, key, field, dialect))
|
|
444
492
|
);
|
|
445
493
|
return `${imports.join("\n")}
|
|
446
494
|
|
|
@@ -450,6 +498,139 @@ ${[mediaTableExpr(dialect), ...tables, ...joins].join(
|
|
|
450
498
|
`;
|
|
451
499
|
}
|
|
452
500
|
|
|
501
|
+
const MEDIA_SELECTION = "key url type alt folder mime size width height";
|
|
502
|
+
function blocksSelection(entryName, key, field) {
|
|
503
|
+
const parts = ["type"];
|
|
504
|
+
for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
|
|
505
|
+
const fields = Object.entries(block.fields).map(
|
|
506
|
+
([blockFieldKey, blockField]) => blockField.type === "media" ? `${blockFieldKey} { ${MEDIA_SELECTION} }` : blockFieldKey
|
|
507
|
+
);
|
|
508
|
+
if (!fields.length) continue;
|
|
509
|
+
parts.push(`... on ${blockTypeName(entryName, key, blockName)} { ${fields.join(" ")} }`);
|
|
510
|
+
}
|
|
511
|
+
return parts.join(" ");
|
|
512
|
+
}
|
|
513
|
+
function entrySelection(config, name, entry, withRelations, fields = entryFieldsFor(entry)) {
|
|
514
|
+
const parts = ["id"];
|
|
515
|
+
if (entry.kind === "page") parts.push("path");
|
|
516
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
517
|
+
if (isPrivateField(field)) continue;
|
|
518
|
+
if (field.type === "relation") {
|
|
519
|
+
const target = config[field.to];
|
|
520
|
+
if (!withRelations || !target) continue;
|
|
521
|
+
parts.push(`${key} { ${entrySelection(config, field.to, target, false)} }`);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (field.type === "media") {
|
|
525
|
+
parts.push(`${key} { ${MEDIA_SELECTION} }`);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (field.type === "blocks") {
|
|
529
|
+
parts.push(`${key} { ${blocksSelection(name, key, field)} }`);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
parts.push(key);
|
|
533
|
+
}
|
|
534
|
+
if (entry.kind === "collection") parts.push("createdAt");
|
|
535
|
+
parts.push("updatedAt");
|
|
536
|
+
return parts.join(" ");
|
|
537
|
+
}
|
|
538
|
+
function singleQuery(config, name, entry) {
|
|
539
|
+
const selection = entrySelection(config, name, entry, true);
|
|
540
|
+
return `query CmsSingle($locale: String) { ${name}(locale: $locale) { ${selection} } }`;
|
|
541
|
+
}
|
|
542
|
+
function collectionQuery(config, name, entry) {
|
|
543
|
+
const selection = entrySelection(config, name, entry, true);
|
|
544
|
+
const gqlType = typeName(name);
|
|
545
|
+
const args = [
|
|
546
|
+
"$locale: String",
|
|
547
|
+
`$filters: ${gqlType}Filters`,
|
|
548
|
+
`$sort: [${gqlType}Sort!]`,
|
|
549
|
+
"$limit: Int",
|
|
550
|
+
"$offset: Int"
|
|
551
|
+
].join(", ");
|
|
552
|
+
return `query CmsCollection(${args}) { ${name}(locale: $locale, filters: $filters, sort: $sort, limit: $limit, offset: $offset) { ${selection} } }`;
|
|
553
|
+
}
|
|
554
|
+
function pageQuery(config, name, entry, path) {
|
|
555
|
+
const selection = entrySelection(config, name, entry, true, pageFields(entry, path));
|
|
556
|
+
return `query CmsPage($path: String!, $locale: String) { page: ${name}ByPath(path: $path, locale: $locale) { ${selection} } }`;
|
|
557
|
+
}
|
|
558
|
+
function renderQueriesFile(config) {
|
|
559
|
+
const singles = [];
|
|
560
|
+
const collections = [];
|
|
561
|
+
const pages = [];
|
|
562
|
+
for (const [name, entry] of Object.entries(config)) {
|
|
563
|
+
if (entry.kind === "page") {
|
|
564
|
+
for (const route of pageRoutes(entry)) {
|
|
565
|
+
pages.push(
|
|
566
|
+
` ${JSON.stringify(route.path)}: ${JSON.stringify(
|
|
567
|
+
pageQuery(config, name, entry, route.path)
|
|
568
|
+
)},`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const line = ` ${JSON.stringify(name)}: ${JSON.stringify(
|
|
574
|
+
entry.kind === "single" ? singleQuery(config, name, entry) : collectionQuery(config, name, entry)
|
|
575
|
+
)},`;
|
|
576
|
+
if (entry.kind === "single") singles.push(line);
|
|
577
|
+
else collections.push(line);
|
|
578
|
+
}
|
|
579
|
+
return [
|
|
580
|
+
`export const cmsSingleQueries: Record<string, string> = {`,
|
|
581
|
+
...singles,
|
|
582
|
+
`}`,
|
|
583
|
+
``,
|
|
584
|
+
`export const cmsCollectionQueries: Record<string, string> = {`,
|
|
585
|
+
...collections,
|
|
586
|
+
`}`,
|
|
587
|
+
``,
|
|
588
|
+
`export const cmsPageQueries: Record<string, string> = {`,
|
|
589
|
+
...pages,
|
|
590
|
+
`}`,
|
|
591
|
+
``
|
|
592
|
+
].join("\n");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const DYNAMIC = /[[\]]/;
|
|
596
|
+
function vueFiles(dir, prefix = "") {
|
|
597
|
+
if (!existsSync(dir)) return [];
|
|
598
|
+
const files = [];
|
|
599
|
+
for (const item of readdirSync(dir, { withFileTypes: true })) {
|
|
600
|
+
const name = `${prefix}${item.name}`;
|
|
601
|
+
if (item.isDirectory()) files.push(...vueFiles(join(dir, item.name), `${name}/`));
|
|
602
|
+
else if (item.name.endsWith(".vue")) files.push(name);
|
|
603
|
+
}
|
|
604
|
+
return files;
|
|
605
|
+
}
|
|
606
|
+
function routePathFromFile(file) {
|
|
607
|
+
if (DYNAMIC.test(file)) return null;
|
|
608
|
+
const segments = file.replace(/\.vue$/, "").split("/").filter((segment) => !/^\(.+\)$/.test(segment));
|
|
609
|
+
if (segments.at(-1) === "index") segments.pop();
|
|
610
|
+
const path = `/${segments.join("/")}`.replace(/\/$/, "");
|
|
611
|
+
return path || "/";
|
|
612
|
+
}
|
|
613
|
+
function routePathsFromDir(pagesDir) {
|
|
614
|
+
const paths = /* @__PURE__ */ new Set();
|
|
615
|
+
for (const file of vueFiles(pagesDir)) {
|
|
616
|
+
const path = routePathFromFile(file);
|
|
617
|
+
if (path) paths.add(path);
|
|
618
|
+
}
|
|
619
|
+
return [...paths];
|
|
620
|
+
}
|
|
621
|
+
function resolvePageRoutes(entry, discovered) {
|
|
622
|
+
const declared = Array.isArray(entry.routes) ? entry.routes : discovered;
|
|
623
|
+
const paths = /* @__PURE__ */ new Set([...declared, ...entry.include ?? []]);
|
|
624
|
+
for (const path of entry.exclude ?? []) paths.delete(path);
|
|
625
|
+
const rank = new Map((entry.order ?? []).map((path, index) => [path, index]));
|
|
626
|
+
const order = entry.order?.length ?? 0;
|
|
627
|
+
return [...paths].sort((a, b) => (rank.get(a) ?? order) - (rank.get(b) ?? order) || a.localeCompare(b)).map((path) => ({
|
|
628
|
+
path,
|
|
629
|
+
key: pageKeyFromPath(path),
|
|
630
|
+
label: entry.labels?.[path] ?? pageLabelFromPath(path)
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
|
|
453
634
|
function mediaTsType(field) {
|
|
454
635
|
const types = mediaTypeFilter(field.mediaType);
|
|
455
636
|
return types ? `CmsMedia<${types.map((type) => `'${type}'`).join(" | ")}>` : "CmsMedia";
|
|
@@ -512,7 +693,8 @@ ${lines.join("\n")}
|
|
|
512
693
|
}
|
|
513
694
|
function entryTs(config, name, entry) {
|
|
514
695
|
const lines = [" id: string"];
|
|
515
|
-
|
|
696
|
+
if (entry.kind === "page") lines.push(" path: string");
|
|
697
|
+
for (const [key, field] of Object.entries(entryFieldsFor(entry))) {
|
|
516
698
|
if (isPrivateField(field)) continue;
|
|
517
699
|
lines.push(` ${key}: ${fieldTsType(config, name, key, field)}`);
|
|
518
700
|
}
|
|
@@ -522,6 +704,78 @@ function entryTs(config, name, entry) {
|
|
|
522
704
|
${lines.join("\n")}
|
|
523
705
|
}`;
|
|
524
706
|
}
|
|
707
|
+
function relationKeys(entry) {
|
|
708
|
+
return Object.entries(entryFieldsFor(entry)).filter(([, field]) => field.type === "relation" && !isPrivateField(field)).map(([key]) => key);
|
|
709
|
+
}
|
|
710
|
+
function quotedKeys(keys) {
|
|
711
|
+
return keys.map((key) => `'${key}'`).join(" | ");
|
|
712
|
+
}
|
|
713
|
+
function shallowTypeTs(config, name) {
|
|
714
|
+
const target = typeName(name);
|
|
715
|
+
const entry = config[name];
|
|
716
|
+
const keys = entry ? relationKeys(entry) : [];
|
|
717
|
+
return keys.length ? `Omit<${target}, ${quotedKeys(keys)}>` : target;
|
|
718
|
+
}
|
|
719
|
+
function autoTypeTs(config, name, entry) {
|
|
720
|
+
const auto = `${typeName(name)}Auto`;
|
|
721
|
+
const keys = relationKeys(entry);
|
|
722
|
+
if (!keys.length) return `export type ${auto} = ${typeName(name)}`;
|
|
723
|
+
const lines = keys.map((key) => {
|
|
724
|
+
const field = entry.fields[key];
|
|
725
|
+
const shallow = shallowTypeTs(config, field.to);
|
|
726
|
+
if (field.cardinality === "many-to-many") return ` ${key}: ${shallow}[]`;
|
|
727
|
+
const nonNull = isRequiredField(field) && !config[field.to]?.drafts;
|
|
728
|
+
return ` ${key}: ${shallow}${nonNull ? "" : " | null"}`;
|
|
729
|
+
});
|
|
730
|
+
return `export type ${auto} = Omit<${typeName(name)}, ${quotedKeys(keys)}> & {
|
|
731
|
+
${lines.join(
|
|
732
|
+
"\n"
|
|
733
|
+
)}
|
|
734
|
+
}`;
|
|
735
|
+
}
|
|
736
|
+
function pageTypesTs(name, entry) {
|
|
737
|
+
const auto = `${typeName(name)}Auto`;
|
|
738
|
+
const lines = pageRoutes(entry).map((route) => {
|
|
739
|
+
const keys = ["id", "path", "updatedAt", ...Object.keys(pageFields(entry, route.path))];
|
|
740
|
+
return ` ${JSON.stringify(route.path)}: Pick<${auto}, ${quotedKeys(keys)}>`;
|
|
741
|
+
});
|
|
742
|
+
return [
|
|
743
|
+
`export interface CmsPageTypes {
|
|
744
|
+
${lines.join("\n")}
|
|
745
|
+
}`,
|
|
746
|
+
`export type CmsPagePath = keyof CmsPageTypes`
|
|
747
|
+
];
|
|
748
|
+
}
|
|
749
|
+
function entryMapsTs(config) {
|
|
750
|
+
const singles = [];
|
|
751
|
+
const collections = [];
|
|
752
|
+
const pages = [];
|
|
753
|
+
for (const [name, entry] of Object.entries(config)) {
|
|
754
|
+
const line = ` ${JSON.stringify(name)}: ${typeName(name)}Auto`;
|
|
755
|
+
if (entry.kind === "single") singles.push(line);
|
|
756
|
+
else if (entry.kind === "page") pages.push(...pageTypesTs(name, entry));
|
|
757
|
+
else collections.push(line);
|
|
758
|
+
}
|
|
759
|
+
if (!pages.length) {
|
|
760
|
+
pages.push(
|
|
761
|
+
`export interface CmsPageTypes {
|
|
762
|
+
[path: string]: never
|
|
763
|
+
}`,
|
|
764
|
+
`export type CmsPagePath = keyof CmsPageTypes`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return [
|
|
768
|
+
`export interface CmsSingleTypes {
|
|
769
|
+
${singles.join("\n")}
|
|
770
|
+
}`,
|
|
771
|
+
`export interface CmsCollectionTypes {
|
|
772
|
+
${collections.join("\n")}
|
|
773
|
+
}`,
|
|
774
|
+
`export type CmsSingleName = keyof CmsSingleTypes`,
|
|
775
|
+
`export type CmsCollectionName = keyof CmsCollectionTypes`,
|
|
776
|
+
...pages
|
|
777
|
+
];
|
|
778
|
+
}
|
|
525
779
|
function renderTypesFile(config) {
|
|
526
780
|
const parts = [
|
|
527
781
|
`export type CmsMediaType = 'image' | 'video' | 'file'`,
|
|
@@ -538,12 +792,14 @@ function renderTypesFile(config) {
|
|
|
538
792
|
}`
|
|
539
793
|
];
|
|
540
794
|
for (const [name, entry] of Object.entries(config)) {
|
|
541
|
-
for (const [key, field] of Object.entries(entry
|
|
795
|
+
for (const [key, field] of Object.entries(entryFieldsFor(entry))) {
|
|
542
796
|
if (field.type === "blocks" && !isPrivateField(field))
|
|
543
797
|
parts.push(...blockTypesTs(name, key, field));
|
|
544
798
|
}
|
|
545
799
|
parts.push(entryTs(config, name, entry));
|
|
546
800
|
}
|
|
801
|
+
for (const [name, entry] of Object.entries(config)) parts.push(autoTypeTs(config, name, entry));
|
|
802
|
+
parts.push(...entryMapsTs(config));
|
|
547
803
|
return `${parts.join("\n\n")}
|
|
548
804
|
`;
|
|
549
805
|
}
|
|
@@ -632,6 +888,136 @@ function resolveModuleOptions(options) {
|
|
|
632
888
|
}
|
|
633
889
|
};
|
|
634
890
|
}
|
|
891
|
+
const moduleRequire = createRequire(import.meta.url);
|
|
892
|
+
function resolveImport(specifier) {
|
|
893
|
+
try {
|
|
894
|
+
return fileURLToPath(import.meta.resolve(specifier)).replace(/\\/g, "/");
|
|
895
|
+
} catch {
|
|
896
|
+
return moduleRequire.resolve(specifier).replace(/\\/g, "/");
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function loadCmsConfig(nuxt, resolver, configPathOption, i18n, logger) {
|
|
900
|
+
const configPath = await resolvePath(configPathOption, { cwd: nuxt.options.rootDir });
|
|
901
|
+
nuxt.options.alias["#nuxt-cms"] = resolver.resolve("./runtime/shared/index");
|
|
902
|
+
nuxt.options.watch.push(configPath);
|
|
903
|
+
let cmsConfig = {};
|
|
904
|
+
if (existsSync(configPath)) {
|
|
905
|
+
nuxt.options.alias["#cms-config"] = configPath;
|
|
906
|
+
const jiti = createJiti(import.meta.url, {
|
|
907
|
+
moduleCache: false,
|
|
908
|
+
alias: { "#nuxt-cms": resolver.resolve("./runtime/shared/index") }
|
|
909
|
+
});
|
|
910
|
+
cmsConfig = await jiti.import(configPath, { default: true });
|
|
911
|
+
} else {
|
|
912
|
+
logger.warn(
|
|
913
|
+
`[nuxt-cms] Config file not found: ${configPath}. Using an empty registry \u2014 create a ${configPathOption}.ts with defineCmsConfig().`
|
|
914
|
+
);
|
|
915
|
+
nuxt.options.alias["#cms-config"] = resolver.resolve("./runtime/shared/empty-config");
|
|
916
|
+
}
|
|
917
|
+
const pagesDir = join(
|
|
918
|
+
nuxt.options.srcDir ?? nuxt.options.rootDir,
|
|
919
|
+
nuxt.options.dir?.pages ?? "pages"
|
|
920
|
+
);
|
|
921
|
+
const discoveredRoutes = routePathsFromDir(pagesDir);
|
|
922
|
+
const routesByEntry = {};
|
|
923
|
+
for (const [name, entry] of Object.entries(cmsConfig)) {
|
|
924
|
+
if (entry.kind !== "page") continue;
|
|
925
|
+
entry.pages = resolvePageRoutes(entry, discoveredRoutes);
|
|
926
|
+
routesByEntry[name] = entry.pages;
|
|
927
|
+
}
|
|
928
|
+
if (Object.keys(routesByEntry).length) {
|
|
929
|
+
const source = (nuxt.options.alias["#cms-config"] ?? "").replace(/\\/g, "/").replace(/\.[cm]?[jt]s$/, "");
|
|
930
|
+
const wrapper = addTemplate({
|
|
931
|
+
filename: "cms/config.ts",
|
|
932
|
+
write: true,
|
|
933
|
+
getContents: () => [
|
|
934
|
+
`import config from '${source}'`,
|
|
935
|
+
``,
|
|
936
|
+
`const pageRoutes = ${JSON.stringify(routesByEntry, null, 3)}`,
|
|
937
|
+
``,
|
|
938
|
+
`for (const [name, routes] of Object.entries(pageRoutes)) {`,
|
|
939
|
+
` const entry = (config as Record<string, { pages?: unknown }>)[name]`,
|
|
940
|
+
` if (entry) entry.pages = routes`,
|
|
941
|
+
`}`,
|
|
942
|
+
``,
|
|
943
|
+
`export default config`,
|
|
944
|
+
``
|
|
945
|
+
].join("\n")
|
|
946
|
+
});
|
|
947
|
+
nuxt.options.alias["#cms-config"] = wrapper.dst;
|
|
948
|
+
}
|
|
949
|
+
const configErrors = validateConfig(cmsConfig, i18n);
|
|
950
|
+
if (configErrors.length) {
|
|
951
|
+
for (const error of configErrors) logger.error(error);
|
|
952
|
+
throw new Error(
|
|
953
|
+
`[nuxt-cms] Invalid cms config (${configErrors.length} error${configErrors.length > 1 ? "s" : ""})`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
return cmsConfig;
|
|
957
|
+
}
|
|
958
|
+
function addCmsTypeTemplates(nuxt, cmsConfig) {
|
|
959
|
+
addTemplate({
|
|
960
|
+
filename: "cms/schema.graphql",
|
|
961
|
+
write: true,
|
|
962
|
+
getContents: () => renderGraphqlSdl(cmsConfig)
|
|
963
|
+
});
|
|
964
|
+
const typesTemplate = addTemplate({
|
|
965
|
+
filename: "cms/types.ts",
|
|
966
|
+
write: true,
|
|
967
|
+
getContents: () => renderTypesFile(cmsConfig)
|
|
968
|
+
});
|
|
969
|
+
nuxt.options.alias["#cms-types"] = typesTemplate.dst;
|
|
970
|
+
const queriesTemplate = addTemplate({
|
|
971
|
+
filename: "cms/queries.ts",
|
|
972
|
+
write: true,
|
|
973
|
+
getContents: () => renderQueriesFile(cmsConfig)
|
|
974
|
+
});
|
|
975
|
+
nuxt.options.alias["#cms-queries"] = queriesTemplate.dst;
|
|
976
|
+
addTemplate({
|
|
977
|
+
filename: "cms/graphql-env.d.ts",
|
|
978
|
+
write: true,
|
|
979
|
+
getContents: () => {
|
|
980
|
+
const introspection = minifyIntrospection(
|
|
981
|
+
introspectionFromSchema(buildSchema(renderGraphqlSdl(cmsConfig)))
|
|
982
|
+
);
|
|
983
|
+
return outputIntrospectionFile(introspection, {
|
|
984
|
+
fileType: ".d.ts",
|
|
985
|
+
shouldPreprocess: true
|
|
986
|
+
}).split("import * as gqlTada from 'gql.tada';")[0];
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
const gqlTadaTypesPath = resolveImport("gql.tada").replace(/\.[mc]?js$/, "");
|
|
990
|
+
const graphqlTemplate = addTemplate({
|
|
991
|
+
filename: "cms/graphql.ts",
|
|
992
|
+
write: true,
|
|
993
|
+
getContents: () => [
|
|
994
|
+
`import type { initGraphQLTada, ResultOf, VariablesOf } from '${gqlTadaTypesPath}'`,
|
|
995
|
+
`import type { introspection } from './graphql-env'`,
|
|
996
|
+
``,
|
|
997
|
+
`export type CmsGraphql = initGraphQLTada<{`,
|
|
998
|
+
` introspection: introspection`,
|
|
999
|
+
` scalars: {`,
|
|
1000
|
+
` JSON: unknown`,
|
|
1001
|
+
` }`,
|
|
1002
|
+
`}>`,
|
|
1003
|
+
``,
|
|
1004
|
+
`declare const graphql: CmsGraphql`,
|
|
1005
|
+
``,
|
|
1006
|
+
`// @ts-ignore: gql.tada's cache overload rejects this instantiation, but the parse overload still resolves`,
|
|
1007
|
+
`export type CmsDocument<Query extends string> = ReturnType<typeof graphql<Query, []>>`,
|
|
1008
|
+
``,
|
|
1009
|
+
`export type CmsResult<Query extends string> = string extends Query`,
|
|
1010
|
+
` ? Record<string, unknown>`,
|
|
1011
|
+
` : ResultOf<CmsDocument<Query>>`,
|
|
1012
|
+
``,
|
|
1013
|
+
`export type CmsVariables<Query extends string> = string extends Query`,
|
|
1014
|
+
` ? Record<string, unknown>`,
|
|
1015
|
+
` : VariablesOf<CmsDocument<Query>>`,
|
|
1016
|
+
``
|
|
1017
|
+
].join("\n")
|
|
1018
|
+
});
|
|
1019
|
+
nuxt.options.alias["#cms-graphql"] = graphqlTemplate.dst;
|
|
1020
|
+
}
|
|
635
1021
|
const module$1 = defineNuxtModule({
|
|
636
1022
|
meta: {
|
|
637
1023
|
name: "@xleddyl/nuxt-cms",
|
|
@@ -676,11 +1062,19 @@ const module$1 = defineNuxtModule({
|
|
|
676
1062
|
const logger = useLogger("nuxt-cms");
|
|
677
1063
|
const resolved = resolveModuleOptions(options);
|
|
678
1064
|
if (!resolveCmsEnabled(options.enabled, process.env[CMS_ENABLED_ENV])) {
|
|
679
|
-
const
|
|
1065
|
+
const queryStub = resolver.resolve("./runtime/app/composables/cms-query-disabled");
|
|
1066
|
+
const entryStub = resolver.resolve("./runtime/app/composables/cms-entry-disabled");
|
|
680
1067
|
addImports([
|
|
681
|
-
{ name: "useCms", from:
|
|
682
|
-
{ name: "$cmsQuery", from:
|
|
1068
|
+
{ name: "useCms", from: queryStub },
|
|
1069
|
+
{ name: "$cmsQuery", from: queryStub },
|
|
1070
|
+
{ name: "useCmsSingle", from: entryStub },
|
|
1071
|
+
{ name: "useCmsCollection", from: entryStub },
|
|
1072
|
+
{ name: "useCmsPage", from: entryStub }
|
|
683
1073
|
]);
|
|
1074
|
+
addCmsTypeTemplates(
|
|
1075
|
+
nuxt,
|
|
1076
|
+
await loadCmsConfig(nuxt, resolver, resolved.configPath, resolved.i18n, logger)
|
|
1077
|
+
);
|
|
684
1078
|
nuxt.options.runtimeConfig.public.cms = {
|
|
685
1079
|
mediaBaseUrl: resolved.media.publicBaseUrl,
|
|
686
1080
|
mediaStorage: resolved.media.storage,
|
|
@@ -688,7 +1082,7 @@ const module$1 = defineNuxtModule({
|
|
|
688
1082
|
i18n: resolved.i18n
|
|
689
1083
|
};
|
|
690
1084
|
logger.info(
|
|
691
|
-
"[nuxt-cms] disabled: registering no-op
|
|
1085
|
+
"[nuxt-cms] disabled: registering no-op query composables and generated types, skipping admin, server and database setup"
|
|
692
1086
|
);
|
|
693
1087
|
return;
|
|
694
1088
|
}
|
|
@@ -713,38 +1107,13 @@ const module$1 = defineNuxtModule({
|
|
|
713
1107
|
"[nuxt-cms] media.storage is 's3' with credentials configured but no publicBaseUrl (media.publicBaseUrl or NUXT_PUBLIC_CMS_MEDIA_BASE_URL); uploaded media URLs will be null."
|
|
714
1108
|
);
|
|
715
1109
|
}
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
moduleCache: false,
|
|
724
|
-
alias: { "#nuxt-cms": resolver.resolve("./runtime/shared/index") }
|
|
725
|
-
});
|
|
726
|
-
cmsConfig = await jiti.import(configPath, { default: true });
|
|
727
|
-
} else {
|
|
728
|
-
logger.warn(
|
|
729
|
-
`[nuxt-cms] Config file not found: ${configPath}. Using an empty registry \u2014 create a ${resolved.configPath}.ts with defineCmsConfig().`
|
|
730
|
-
);
|
|
731
|
-
nuxt.options.alias["#cms-config"] = resolver.resolve("./runtime/shared/empty-config");
|
|
732
|
-
}
|
|
733
|
-
const configErrors = validateConfig(cmsConfig, resolved.i18n);
|
|
734
|
-
if (configErrors.length) {
|
|
735
|
-
for (const error of configErrors) logger.error(error);
|
|
736
|
-
throw new Error(
|
|
737
|
-
`[nuxt-cms] Invalid cms config (${configErrors.length} error${configErrors.length > 1 ? "s" : ""})`
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
const moduleRequire = createRequire(import.meta.url);
|
|
741
|
-
const resolveImport = (specifier) => {
|
|
742
|
-
try {
|
|
743
|
-
return fileURLToPath(import.meta.resolve(specifier)).replace(/\\/g, "/");
|
|
744
|
-
} catch {
|
|
745
|
-
return moduleRequire.resolve(specifier).replace(/\\/g, "/");
|
|
746
|
-
}
|
|
747
|
-
};
|
|
1110
|
+
const cmsConfig = await loadCmsConfig(
|
|
1111
|
+
nuxt,
|
|
1112
|
+
resolver,
|
|
1113
|
+
resolved.configPath,
|
|
1114
|
+
resolved.i18n,
|
|
1115
|
+
logger
|
|
1116
|
+
);
|
|
748
1117
|
const schemaTemplate = addTemplate({
|
|
749
1118
|
filename: "cms/schema.ts",
|
|
750
1119
|
write: true,
|
|
@@ -755,64 +1124,15 @@ const module$1 = defineNuxtModule({
|
|
|
755
1124
|
)
|
|
756
1125
|
});
|
|
757
1126
|
nuxt.options.alias["#cms-tables"] = schemaTemplate.dst;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
getContents: () => renderGraphqlSdl(cmsConfig)
|
|
762
|
-
});
|
|
763
|
-
const typesTemplate = addTemplate({
|
|
764
|
-
filename: "cms/types.ts",
|
|
765
|
-
write: true,
|
|
766
|
-
getContents: () => renderTypesFile(cmsConfig)
|
|
767
|
-
});
|
|
768
|
-
nuxt.options.alias["#cms-types"] = typesTemplate.dst;
|
|
769
|
-
addTemplate({
|
|
770
|
-
filename: "cms/graphql-env.d.ts",
|
|
771
|
-
write: true,
|
|
772
|
-
getContents: () => {
|
|
773
|
-
const introspection = minifyIntrospection(
|
|
774
|
-
introspectionFromSchema(buildSchema(renderGraphqlSdl(cmsConfig)))
|
|
775
|
-
);
|
|
776
|
-
return outputIntrospectionFile(introspection, {
|
|
777
|
-
fileType: ".d.ts",
|
|
778
|
-
shouldPreprocess: true
|
|
779
|
-
}).split("import * as gqlTada from 'gql.tada';")[0];
|
|
780
|
-
}
|
|
781
|
-
});
|
|
782
|
-
const gqlTadaTypesPath = resolveImport("gql.tada").replace(/\.[mc]?js$/, "");
|
|
783
|
-
const graphqlTemplate = addTemplate({
|
|
784
|
-
filename: "cms/graphql.ts",
|
|
785
|
-
write: true,
|
|
786
|
-
getContents: () => [
|
|
787
|
-
`import type { initGraphQLTada, ResultOf, VariablesOf } from '${gqlTadaTypesPath}'`,
|
|
788
|
-
`import type { introspection } from './graphql-env'`,
|
|
789
|
-
``,
|
|
790
|
-
`export type CmsGraphql = initGraphQLTada<{`,
|
|
791
|
-
` introspection: introspection`,
|
|
792
|
-
` scalars: {`,
|
|
793
|
-
` JSON: unknown`,
|
|
794
|
-
` }`,
|
|
795
|
-
`}>`,
|
|
796
|
-
``,
|
|
797
|
-
`declare const graphql: CmsGraphql`,
|
|
798
|
-
``,
|
|
799
|
-
`// @ts-ignore: gql.tada's cache overload rejects this instantiation, but the parse overload still resolves`,
|
|
800
|
-
`export type CmsDocument<Query extends string> = ReturnType<typeof graphql<Query, []>>`,
|
|
801
|
-
``,
|
|
802
|
-
`export type CmsResult<Query extends string> = string extends Query`,
|
|
803
|
-
` ? Record<string, unknown>`,
|
|
804
|
-
` : ResultOf<CmsDocument<Query>>`,
|
|
805
|
-
``,
|
|
806
|
-
`export type CmsVariables<Query extends string> = string extends Query`,
|
|
807
|
-
` ? Record<string, unknown>`,
|
|
808
|
-
` : VariablesOf<CmsDocument<Query>>`,
|
|
809
|
-
``
|
|
810
|
-
].join("\n")
|
|
811
|
-
});
|
|
812
|
-
nuxt.options.alias["#cms-graphql"] = graphqlTemplate.dst;
|
|
1127
|
+
addCmsTypeTemplates(nuxt, cmsConfig);
|
|
1128
|
+
const queryComposables = resolver.resolve("./runtime/app/composables/cms-query");
|
|
1129
|
+
const entryComposables = resolver.resolve("./runtime/app/composables/cms-entry");
|
|
813
1130
|
addImports([
|
|
814
|
-
{ name: "useCms", from:
|
|
815
|
-
{ name: "$cmsQuery", from:
|
|
1131
|
+
{ name: "useCms", from: queryComposables },
|
|
1132
|
+
{ name: "$cmsQuery", from: queryComposables },
|
|
1133
|
+
{ name: "useCmsSingle", from: entryComposables },
|
|
1134
|
+
{ name: "useCmsCollection", from: entryComposables },
|
|
1135
|
+
{ name: "useCmsPage", from: entryComposables }
|
|
816
1136
|
]);
|
|
817
1137
|
const {
|
|
818
1138
|
driver,
|