@cms0/cms0 0.2.7 → 0.2.9

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.
@@ -8,6 +8,7 @@ exports.buildDescriptorAlt = buildDescriptorAlt;
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const ts_morph_1 = require("ts-morph");
11
+ const shared_1 = require("@cms0/shared");
11
12
  const config_loader_js_1 = require("./config-loader.cjs");
12
13
  const registry_js_1 = require("../../custom-types/registry.cjs");
13
14
  function resolveCustomTypeName(type) {
@@ -127,6 +128,39 @@ function descriptorFromType(type, modelNames, warnings, ctx, opts) {
127
128
  const { base, optional, nullable } = unwrapOptional(type);
128
129
  const isOptional = !!opts?.optional || optional;
129
130
  const isNullable = !!opts?.nullable || nullable;
131
+ if (base.isUnion()) {
132
+ const unionDescriptor = descriptorFromUnionType(base, modelNames, warnings, ctx);
133
+ if (unionDescriptor) {
134
+ return applyOptionalityToDescriptor(unionDescriptor, isOptional, isNullable);
135
+ }
136
+ }
137
+ if (base.isStringLiteral()) {
138
+ return {
139
+ kind: "enum",
140
+ valueType: "string",
141
+ values: [String(base.getLiteralValue())],
142
+ optional: isOptional,
143
+ nullable: isNullable,
144
+ };
145
+ }
146
+ if (base.isNumberLiteral()) {
147
+ return {
148
+ kind: "enum",
149
+ valueType: "number",
150
+ values: [Number(base.getLiteralValue())],
151
+ optional: isOptional,
152
+ nullable: isNullable,
153
+ };
154
+ }
155
+ if (base.isBooleanLiteral()) {
156
+ return {
157
+ kind: "enum",
158
+ valueType: "boolean",
159
+ values: [base.getText() === "true"],
160
+ optional: isOptional,
161
+ nullable: isNullable,
162
+ };
163
+ }
130
164
  if (base.isString()) {
131
165
  return {
132
166
  kind: "primitive",
@@ -195,6 +229,203 @@ function descriptorFromType(type, modelNames, warnings, ctx, opts) {
195
229
  nullable: isNullable,
196
230
  };
197
231
  }
232
+ function descriptorFromUnionType(unionType, modelNames, warnings, ctx) {
233
+ const unionTypes = unionType.getUnionTypes();
234
+ if (!unionTypes.length)
235
+ return undefined;
236
+ const hasBroadString = unionTypes.some((entry) => entry.isString());
237
+ const hasBroadNumber = unionTypes.some((entry) => entry.isNumber());
238
+ const hasBroadBoolean = unionTypes.some((entry) => entry.isBoolean());
239
+ const filtered = unionTypes.filter((entry) => {
240
+ if (entry.isStringLiteral() && hasBroadString)
241
+ return false;
242
+ if (entry.isNumberLiteral() && hasBroadNumber)
243
+ return false;
244
+ if (entry.isBooleanLiteral() && hasBroadBoolean)
245
+ return false;
246
+ return true;
247
+ });
248
+ if (!filtered.length) {
249
+ if (hasBroadString) {
250
+ return { kind: "primitive", type: "string" };
251
+ }
252
+ if (hasBroadNumber) {
253
+ return { kind: "primitive", type: "number" };
254
+ }
255
+ if (hasBroadBoolean) {
256
+ return { kind: "primitive", type: "boolean" };
257
+ }
258
+ return undefined;
259
+ }
260
+ const literalEnum = resolveLiteralUnionAsEnum(filtered);
261
+ if (literalEnum) {
262
+ return literalEnum;
263
+ }
264
+ const branches = filtered
265
+ .map((entry, index) => descriptorFromType(entry, modelNames, warnings, `${ctx}|${index}`, {
266
+ optional: false,
267
+ nullable: false,
268
+ }))
269
+ .map((entry) => stripOptionality(entry));
270
+ const deduped = dedupeDescriptors(branches);
271
+ if (!deduped.length)
272
+ return undefined;
273
+ if (deduped.length === 1)
274
+ return deduped[0];
275
+ const discriminator = inferUnionDiscriminator(deduped);
276
+ const branchKeys = (0, shared_1.computeUnionBranchKeys)(deduped);
277
+ return {
278
+ kind: "union",
279
+ anyOf: deduped,
280
+ branchKeys,
281
+ ...(discriminator ? { discriminator: { key: discriminator } } : {}),
282
+ };
283
+ }
284
+ function stripOptionality(descriptor) {
285
+ const clone = JSON.parse(JSON.stringify(descriptor));
286
+ delete clone.optional;
287
+ delete clone.nullable;
288
+ return clone;
289
+ }
290
+ function dedupeDescriptors(descriptors) {
291
+ const seen = new Set();
292
+ const deduped = [];
293
+ for (const descriptor of descriptors) {
294
+ const key = JSON.stringify(descriptor);
295
+ if (seen.has(key))
296
+ continue;
297
+ seen.add(key);
298
+ deduped.push(descriptor);
299
+ }
300
+ return deduped;
301
+ }
302
+ function resolveLiteralUnionAsEnum(types) {
303
+ if (!types.length)
304
+ return undefined;
305
+ const literals = [];
306
+ let kind;
307
+ for (const entry of types) {
308
+ if (entry.isStringLiteral()) {
309
+ if (kind && kind !== "string")
310
+ return undefined;
311
+ kind = "string";
312
+ literals.push(String(entry.getLiteralValue()));
313
+ continue;
314
+ }
315
+ if (entry.isNumberLiteral()) {
316
+ if (kind && kind !== "number")
317
+ return undefined;
318
+ kind = "number";
319
+ literals.push(Number(entry.getLiteralValue()));
320
+ continue;
321
+ }
322
+ if (entry.isBooleanLiteral()) {
323
+ if (kind && kind !== "boolean")
324
+ return undefined;
325
+ kind = "boolean";
326
+ literals.push(entry.getText() === "true");
327
+ continue;
328
+ }
329
+ return undefined;
330
+ }
331
+ if (!kind)
332
+ return undefined;
333
+ const unique = Array.from(new Set(literals));
334
+ return {
335
+ kind: "enum",
336
+ valueType: kind,
337
+ values: unique,
338
+ };
339
+ }
340
+ function inferUnionDiscriminator(descriptors) {
341
+ if (!descriptors.length ||
342
+ descriptors.some((entry) => entry?.type !== "object")) {
343
+ return undefined;
344
+ }
345
+ const objects = descriptors;
346
+ const sharedKeys = Object.keys(objects[0]?.properties ?? {}).filter((key) => objects.every((obj) => Object.prototype.hasOwnProperty.call(obj.properties ?? {}, key)));
347
+ for (const key of sharedKeys) {
348
+ const enumLikeValues = objects.map((obj) => {
349
+ const prop = (obj.properties ?? {})[key];
350
+ if (!prop)
351
+ return undefined;
352
+ if (prop.kind !== "enum" || !Array.isArray(prop.values) || prop.values.length !== 1) {
353
+ return undefined;
354
+ }
355
+ return prop.values[0];
356
+ });
357
+ if (enumLikeValues.some((value) => value === undefined)) {
358
+ continue;
359
+ }
360
+ const unique = new Set(enumLikeValues);
361
+ if (unique.size === enumLikeValues.length) {
362
+ return key;
363
+ }
364
+ }
365
+ return undefined;
366
+ }
367
+ function normalizePropertyOrder(keys, explicitOrder) {
368
+ const normalizedExplicit = Array.isArray(explicitOrder)
369
+ ? explicitOrder
370
+ .filter((value) => typeof value === "string")
371
+ .map((value) => value.trim())
372
+ .filter(Boolean)
373
+ : [];
374
+ const dedupedExplicit = Array.from(new Set(normalizedExplicit)).filter((key) => keys.includes(key));
375
+ const remaining = keys.filter((key) => !dedupedExplicit.includes(key));
376
+ return [...dedupedExplicit, ...remaining];
377
+ }
378
+ function applyFieldPropertyOrder(descriptor) {
379
+ if (!descriptor || typeof descriptor !== "object")
380
+ return descriptor;
381
+ if (descriptor.type === "object") {
382
+ const properties = (descriptor.properties ?? {});
383
+ const keys = Object.keys(properties);
384
+ for (const key of keys) {
385
+ properties[key] = applyFieldPropertyOrder(properties[key]);
386
+ }
387
+ descriptor.propertyOrder = normalizePropertyOrder(keys, descriptor.propertyOrder);
388
+ return descriptor;
389
+ }
390
+ if (descriptor.type === "array" && descriptor.items) {
391
+ descriptor.items = applyFieldPropertyOrder(descriptor.items);
392
+ return descriptor;
393
+ }
394
+ if (descriptor.kind === "union" && Array.isArray(descriptor.anyOf)) {
395
+ descriptor.anyOf = descriptor.anyOf.map((branch) => applyFieldPropertyOrder(branch));
396
+ return descriptor;
397
+ }
398
+ return descriptor;
399
+ }
400
+ function applyDescriptorOrderingMetadata(descriptor) {
401
+ const rootsOrder = Object.keys(descriptor.roots ?? {});
402
+ const modelsOrder = Object.keys(descriptor.models ?? {});
403
+ for (const modelName of modelsOrder) {
404
+ const model = descriptor.models?.[modelName];
405
+ if (!model || typeof model !== "object")
406
+ continue;
407
+ const keys = Object.keys(model.properties ?? {});
408
+ for (const key of keys) {
409
+ model.properties[key] = applyFieldPropertyOrder(model.properties[key]);
410
+ }
411
+ model.propertyOrder = normalizePropertyOrder(keys, model.propertyOrder);
412
+ }
413
+ for (const rootName of rootsOrder) {
414
+ const rootDescriptor = descriptor.roots?.[rootName];
415
+ if (!rootDescriptor)
416
+ continue;
417
+ descriptor.roots[rootName] = applyFieldPropertyOrder(rootDescriptor);
418
+ }
419
+ descriptor.metadata = {
420
+ ...(descriptor.metadata ?? {}),
421
+ ordering: {
422
+ ...(descriptor.metadata?.ordering ?? {}),
423
+ roots: rootsOrder,
424
+ models: modelsOrder,
425
+ },
426
+ };
427
+ return descriptor;
428
+ }
198
429
  function collectModels(sourceFiles, modelNames, modelMap, warnings) {
199
430
  const handleShape = (name, type, ctx) => {
200
431
  if (modelMap[name])
@@ -296,15 +527,15 @@ function buildDescriptorAlt(resolved) {
296
527
  const referencedModelNames = new Set();
297
528
  collectReferencedModelNames(rootType, referencedModelNames, new Set(), "root");
298
529
  const referencedCustomTypeNames = new Set(Array.from(referencedModelNames).filter((n) => registry_js_1.customTypeNames.has(n)));
299
- const referencedCustomModelNames = new Set(Array.from(referencedCustomTypeNames).filter((n) => registry_js_1.customModelTypeNames.has(n)));
300
- const resolvedCustomTypeNames = (0, registry_js_1.resolveCustomTypeDependencies)(referencedCustomModelNames);
530
+ const resolvedCustomTypeNames = (0, registry_js_1.resolveCustomTypeDependencies)(referencedCustomTypeNames);
531
+ const resolvedCustomModelNames = new Set(Array.from(resolvedCustomTypeNames).filter((n) => registry_js_1.customModelTypeNames.has(n)));
301
532
  const modelNames = new Set([
302
533
  ...Array.from(referencedModelNames).filter((n) => exportedModelNames.has(n)),
303
- ...Array.from(resolvedCustomTypeNames),
534
+ ...Array.from(resolvedCustomModelNames),
304
535
  ]);
305
536
  const modelMap = {};
306
537
  collectModels(sourceFiles, modelNames, modelMap, warnings);
307
- resolvedCustomTypeNames.forEach((name) => {
538
+ resolvedCustomModelNames.forEach((name) => {
308
539
  if (modelMap[name]) {
309
540
  return;
310
541
  }
@@ -341,7 +572,7 @@ function buildDescriptorAlt(resolved) {
341
572
  warnings.forEach((w) => console.warn(w));
342
573
  const locales = invocation.locales;
343
574
  const defaultLocale = invocation.defaultLocale;
344
- return {
575
+ const descriptor = {
345
576
  models: modelMap,
346
577
  roots,
347
578
  metadata: locales || defaultLocale
@@ -351,4 +582,5 @@ function buildDescriptorAlt(resolved) {
351
582
  }
352
583
  : undefined,
353
584
  };
585
+ return applyDescriptorOrderingMetadata(descriptor);
354
586
  }
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveLocalized = resolveLocalized;
4
+ exports.toOpenGraph = toOpenGraph;
5
+ exports.toTwitter = toTwitter;
6
+ exports.toNextMetadata = toNextMetadata;
7
+ function isRecord(value) {
8
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+ function normalizeText(value) {
11
+ if (typeof value !== "string")
12
+ return undefined;
13
+ const trimmed = value.trim();
14
+ return trimmed.length > 0 ? trimmed : undefined;
15
+ }
16
+ function uniqueNonEmpty(values) {
17
+ const result = [];
18
+ for (const value of values) {
19
+ const trimmed = normalizeText(value);
20
+ if (!trimmed || result.includes(trimmed))
21
+ continue;
22
+ result.push(trimmed);
23
+ }
24
+ return result;
25
+ }
26
+ function isLocalizedLike(value) {
27
+ if (!isRecord(value))
28
+ return false;
29
+ return "locales" in value && isRecord(value.locales);
30
+ }
31
+ function localizedValueForLocale(locales, locale) {
32
+ const entry = locales[locale];
33
+ return normalizeText(entry);
34
+ }
35
+ function resolveLocalized(value, options = {}) {
36
+ if (value == null)
37
+ return undefined;
38
+ if (typeof value === "string") {
39
+ return normalizeText(value);
40
+ }
41
+ if (!isLocalizedLike(value)) {
42
+ return undefined;
43
+ }
44
+ const locales = value.locales;
45
+ const localeKeys = Object.keys(locales);
46
+ const preferredLocales = uniqueNonEmpty([
47
+ options.locale,
48
+ typeof value.defaultLocale === "string" ? value.defaultLocale : undefined,
49
+ options.defaultLocale,
50
+ options.fallbackLocale,
51
+ ...localeKeys,
52
+ ]);
53
+ for (const locale of preferredLocales) {
54
+ const resolved = localizedValueForLocale(locales, locale);
55
+ if (resolved)
56
+ return resolved;
57
+ }
58
+ return undefined;
59
+ }
60
+ function resolveKeywords(value, options) {
61
+ if (Array.isArray(value)) {
62
+ const list = value.map((entry) => normalizeText(entry)).filter(Boolean);
63
+ return list.length ? list : undefined;
64
+ }
65
+ const localized = resolveLocalized(value, options);
66
+ if (!localized)
67
+ return undefined;
68
+ const parts = localized
69
+ .split(/[;,]/g)
70
+ .map((entry) => entry.trim())
71
+ .filter((entry) => entry.length > 0);
72
+ if (parts.length > 0) {
73
+ return parts;
74
+ }
75
+ return [localized];
76
+ }
77
+ function resolveImageUrl(image) {
78
+ if (typeof image === "string") {
79
+ return normalizeText(image);
80
+ }
81
+ if (!isRecord(image))
82
+ return undefined;
83
+ return normalizeText(image.url);
84
+ }
85
+ function normalizeRobots(robots) {
86
+ if (typeof robots === "boolean") {
87
+ return { index: robots, follow: robots };
88
+ }
89
+ if (!isRecord(robots))
90
+ return undefined;
91
+ const resolved = {};
92
+ if (typeof robots.index === "boolean")
93
+ resolved.index = robots.index;
94
+ if (typeof robots.follow === "boolean")
95
+ resolved.follow = robots.follow;
96
+ if (typeof robots.nocache === "boolean")
97
+ resolved.nocache = robots.nocache;
98
+ return Object.keys(resolved).length ? resolved : undefined;
99
+ }
100
+ function hasAnyKeys(value) {
101
+ return Object.keys(value).length > 0;
102
+ }
103
+ function toOpenGraph(value, options = {}) {
104
+ if (!isRecord(value))
105
+ return undefined;
106
+ const openGraph = {};
107
+ const type = normalizeText(value.type);
108
+ if (type)
109
+ openGraph.type = type;
110
+ const url = normalizeText(value.url);
111
+ if (url)
112
+ openGraph.url = url;
113
+ const siteName = normalizeText(value.siteName);
114
+ if (siteName)
115
+ openGraph.siteName = siteName;
116
+ const title = resolveLocalized(value.title, options);
117
+ if (title)
118
+ openGraph.title = title;
119
+ const description = resolveLocalized(value.description, options);
120
+ if (description)
121
+ openGraph.description = description;
122
+ if (Array.isArray(value.images)) {
123
+ const images = value.images
124
+ .map((image) => resolveImageUrl(image))
125
+ .filter((entry) => typeof entry === "string" && entry.length > 0);
126
+ if (images.length) {
127
+ openGraph.images = images;
128
+ }
129
+ }
130
+ const locale = normalizeText(value.locale);
131
+ if (locale)
132
+ openGraph.locale = locale;
133
+ if (Array.isArray(value.alternateLocale)) {
134
+ const alternateLocale = value.alternateLocale
135
+ .map((entry) => normalizeText(entry))
136
+ .filter((entry) => Boolean(entry));
137
+ if (alternateLocale.length) {
138
+ openGraph.alternateLocale = alternateLocale;
139
+ }
140
+ }
141
+ return hasAnyKeys(openGraph) ? openGraph : undefined;
142
+ }
143
+ function toTwitter(value, options = {}) {
144
+ if (!isRecord(value))
145
+ return undefined;
146
+ const twitter = {};
147
+ const card = normalizeText(value.card);
148
+ if (card)
149
+ twitter.card = card;
150
+ const site = normalizeText(value.site);
151
+ if (site)
152
+ twitter.site = site;
153
+ const creator = normalizeText(value.creator);
154
+ if (creator)
155
+ twitter.creator = creator;
156
+ const title = resolveLocalized(value.title, options);
157
+ if (title)
158
+ twitter.title = title;
159
+ const description = resolveLocalized(value.description, options);
160
+ if (description)
161
+ twitter.description = description;
162
+ if (Array.isArray(value.images)) {
163
+ const images = value.images
164
+ .map((image) => resolveImageUrl(image))
165
+ .filter((entry) => typeof entry === "string" && entry.length > 0);
166
+ if (images.length) {
167
+ twitter.images = images;
168
+ }
169
+ }
170
+ return hasAnyKeys(twitter) ? twitter : undefined;
171
+ }
172
+ function resolveAlternates(seo) {
173
+ const alternates = {};
174
+ const canonical = normalizeText(seo.alternates?.canonical) ?? normalizeText(seo.canonical);
175
+ if (canonical) {
176
+ alternates.canonical = canonical;
177
+ }
178
+ if (isRecord(seo.alternates?.languages)) {
179
+ const languages = {};
180
+ for (const [locale, rawValue] of Object.entries(seo.alternates.languages)) {
181
+ const normalizedLocale = normalizeText(locale);
182
+ const normalizedUrl = normalizeText(rawValue);
183
+ if (!normalizedLocale || !normalizedUrl)
184
+ continue;
185
+ languages[normalizedLocale] = normalizedUrl;
186
+ }
187
+ if (Object.keys(languages).length) {
188
+ alternates.languages = languages;
189
+ }
190
+ }
191
+ return hasAnyKeys(alternates) ? alternates : undefined;
192
+ }
193
+ /**
194
+ * Maps a CMS0 `Seo` object to a Next.js-compatible metadata shape.
195
+ *
196
+ * The return type is generic so consumers can pass Next's `Metadata` type:
197
+ * `toNextMetadata<Metadata>(seo, { locale })`.
198
+ */
199
+ function toNextMetadata(seo, options = {}) {
200
+ if (!isRecord(seo)) {
201
+ return {};
202
+ }
203
+ const metadata = {};
204
+ const title = resolveLocalized(seo.title, options);
205
+ if (title)
206
+ metadata.title = title;
207
+ const description = resolveLocalized(seo.description, options);
208
+ if (description)
209
+ metadata.description = description;
210
+ const keywords = resolveKeywords(seo.keywords, options);
211
+ if (keywords?.length)
212
+ metadata.keywords = keywords;
213
+ const robots = normalizeRobots(seo.robots);
214
+ if (robots)
215
+ metadata.robots = robots;
216
+ const alternates = resolveAlternates(seo);
217
+ if (alternates)
218
+ metadata.alternates = alternates;
219
+ const openGraph = toOpenGraph(seo.openGraph, options);
220
+ if (openGraph)
221
+ metadata.openGraph = openGraph;
222
+ const twitter = toTwitter(seo.twitter, options);
223
+ if (twitter)
224
+ metadata.twitter = twitter;
225
+ return metadata;
226
+ }