@cms-lab/core 1.0.10 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Core config, types, diagnostics, route resolution, and scan orchestration for cms-lab.
4
4
 
5
5
  ```ts
6
- import { defineConfig, scanDocuments } from "@cms-lab/core";
6
+ import { defineConfig, scanDocuments, strapiRelationSlug } from "@cms-lab/core";
7
7
  ```
8
8
 
9
9
  Most users should install `cms-lab` and use the CLI. This package is public for
@@ -19,6 +19,27 @@ Sanity image `alt` fields.
19
19
  `readCmsDataPath` is exported for adapters that need to read dotted paths from
20
20
  normalized CMS payloads, for example custom `uidField` and `urlField` mapping.
21
21
 
22
+ `strapiRelationSlug` and `strapiRelationValue` help Strapi route mappings read
23
+ relation values from both Strapi v4 `data.attributes` payloads and newer flat
24
+ REST payloads:
25
+
26
+ ```ts
27
+ routes: [
28
+ {
29
+ type: "article",
30
+ pattern: "/blog/:topic/:slug",
31
+ getPath: (doc) => {
32
+ const topic = strapiRelationSlug(doc.data, "topic") ?? "uncategorized";
33
+ return `/blog/${topic}/${doc.uid}`;
34
+ },
35
+ },
36
+ ];
37
+ ```
38
+
39
+ `site.healthPath` and `site.healthUrl` let `doctor` and the initial scan health
40
+ probe hit a known-good page or endpoint while normal route probes still use
41
+ `site.url`.
42
+
22
43
  ## Release History
23
44
 
24
45
  See the repository [changelog](https://github.com/i-afaqrashid/cms-lab/blob/main/CHANGELOG.md)
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ type CMSDocument = {
4
4
  type: string;
5
5
  uid?: string;
6
6
  url?: string;
7
+ routable?: boolean;
8
+ entryKind?: "collection" | "single";
7
9
  status: CMSDocumentStatus;
8
10
  data: unknown;
9
11
  };
@@ -49,15 +51,24 @@ type CmsFieldMappingConfig = {
49
51
  uidField?: string;
50
52
  urlField?: string;
51
53
  };
54
+ type StrapiLocaleConfig = {
55
+ locale?: string;
56
+ };
52
57
  type StrapiCollectionConfig = {
53
58
  type: string;
54
59
  endpoint: string;
55
- } & CmsFieldMappingConfig;
60
+ } & CmsFieldMappingConfig & StrapiLocaleConfig;
61
+ type StrapiSingleTypeConfig = {
62
+ type: string;
63
+ endpoint: string;
64
+ } & CmsFieldMappingConfig & StrapiLocaleConfig;
56
65
  type StrapiCmsProviderConfig = {
57
66
  provider: "strapi";
58
67
  url: string;
59
68
  token?: string;
60
- collections: StrapiCollectionConfig[];
69
+ locale?: string;
70
+ collections?: StrapiCollectionConfig[];
71
+ singleTypes?: StrapiSingleTypeConfig[];
61
72
  };
62
73
  type DirectusCollectionConfig = {
63
74
  type: string;
@@ -108,6 +119,8 @@ type CmsProviderConfig = PrismicCmsProviderConfig | StrapiCmsProviderConfig | Di
108
119
  type CmsLabConfig = {
109
120
  site: {
110
121
  url: string;
122
+ healthPath?: string;
123
+ healthUrl?: string;
111
124
  };
112
125
  framework: {
113
126
  type: "next";
@@ -181,6 +194,9 @@ declare class SiteUnreachableError extends CmsLabError {
181
194
  constructor(message: string);
182
195
  }
183
196
 
197
+ declare function strapiRelationSlug(data: unknown, path: string): string | undefined;
198
+ declare function strapiRelationValue(data: unknown, path: string, field: string): string | undefined;
199
+
184
200
  type ScanDocumentsOptions = {
185
201
  config: CmsLabConfig;
186
202
  project: ProjectInfo;
@@ -192,5 +208,6 @@ type ScanDocumentsOptions = {
192
208
  filters?: ScanFilters;
193
209
  };
194
210
  declare function scanDocuments(options: ScanDocumentsOptions): Promise<ScanResult>;
211
+ declare function resolveSiteHealthUrl(site: CmsLabConfig["site"]): URL;
195
212
 
196
- export { type CMSDocument, type CMSDocumentStatus, type CheckGroup, CmsFetchError, type CmsFieldMappingConfig, type CmsLabConfig, CmsLabError, type CmsProviderConfig, ConfigLoadError, type ContentfulCmsProviderConfig, type ContentfulContentTypeConfig, type Diagnostic, type DiagnosticExplanation, type DiagnosticSeverity, type DirectusCmsProviderConfig, type DirectusCollectionConfig, type FetchLike, type LoadedCmsLabConfig, type PrismicCmsProviderConfig, type ProjectInfo, type RequiredFieldRule, type RouteDefinition, type SanityCmsProviderConfig, type SanityContentTypeConfig, type ScanDocumentsOptions, type ScanFilters, type ScanResult, type ScanSummary, SiteUnreachableError, type StrapiCmsProviderConfig, type StrapiCollectionConfig, type WordPressCmsProviderConfig, type WordPressContentTypeConfig, createDiagnostic, defineConfig, explainDiagnostic, listDiagnosticExplanations, loadCmsLabConfig, readCmsDataPath, scanDocuments, summarizeDiagnostics, validateConfig };
213
+ export { type CMSDocument, type CMSDocumentStatus, type CheckGroup, CmsFetchError, type CmsFieldMappingConfig, type CmsLabConfig, CmsLabError, type CmsProviderConfig, ConfigLoadError, type ContentfulCmsProviderConfig, type ContentfulContentTypeConfig, type Diagnostic, type DiagnosticExplanation, type DiagnosticSeverity, type DirectusCmsProviderConfig, type DirectusCollectionConfig, type FetchLike, type LoadedCmsLabConfig, type PrismicCmsProviderConfig, type ProjectInfo, type RequiredFieldRule, type RouteDefinition, type SanityCmsProviderConfig, type SanityContentTypeConfig, type ScanDocumentsOptions, type ScanFilters, type ScanResult, type ScanSummary, SiteUnreachableError, type StrapiCmsProviderConfig, type StrapiCollectionConfig, type StrapiLocaleConfig, type StrapiSingleTypeConfig, type WordPressCmsProviderConfig, type WordPressContentTypeConfig, createDiagnostic, defineConfig, explainDiagnostic, listDiagnosticExplanations, loadCmsLabConfig, readCmsDataPath, resolveSiteHealthUrl, scanDocuments, strapiRelationSlug, strapiRelationValue, summarizeDiagnostics, validateConfig };
package/dist/index.js CHANGED
@@ -53,18 +53,29 @@ var cmsFieldMappingShape = {
53
53
  uidField: z.string().min(1).optional(),
54
54
  urlField: z.string().min(1).optional()
55
55
  };
56
+ var strapiLocaleShape = {
57
+ locale: z.string().min(1).optional()
58
+ };
59
+ var strapiContentShape = z.object({
60
+ type: z.string().min(1),
61
+ endpoint: z.string().min(1),
62
+ ...cmsFieldMappingShape,
63
+ ...strapiLocaleShape
64
+ }).strict();
56
65
  var strapiConfigSchema = z.object({
57
66
  provider: z.literal("strapi"),
58
67
  url: z.string().url(),
59
68
  token: z.string().optional(),
60
- collections: z.array(
61
- z.object({
62
- type: z.string().min(1),
63
- endpoint: z.string().min(1),
64
- ...cmsFieldMappingShape
65
- }).strict()
66
- ).min(1)
67
- }).strict();
69
+ ...strapiLocaleShape,
70
+ collections: z.array(strapiContentShape).min(1).optional(),
71
+ singleTypes: z.array(strapiContentShape).min(1).optional()
72
+ }).refine(
73
+ (config) => (config.collections?.length ?? 0) > 0 || (config.singleTypes?.length ?? 0) > 0,
74
+ {
75
+ message: "Strapi config must include collections or singleTypes",
76
+ path: ["collections"]
77
+ }
78
+ ).strict();
68
79
  var directusConfigSchema = z.object({
69
80
  provider: z.literal("directus"),
70
81
  url: z.string().url(),
@@ -154,7 +165,12 @@ var checksSchema = z.object({
154
165
  }).strict().optional();
155
166
  var configSchema = z.object({
156
167
  site: z.object({
157
- url: z.string().url()
168
+ url: z.string().url(),
169
+ healthPath: z.string().min(1).refine(
170
+ (path) => path.startsWith("/") && !path.startsWith("//"),
171
+ "healthPath must be a same-origin path starting with a single /"
172
+ ).optional(),
173
+ healthUrl: z.string().url().optional()
158
174
  }).strict(),
159
175
  framework: z.object({
160
176
  type: z.literal("next"),
@@ -324,6 +340,42 @@ function listDiagnosticExplanations() {
324
340
  return [...explanations];
325
341
  }
326
342
 
343
+ // src/route-helpers.ts
344
+ function strapiRelationSlug(data, path) {
345
+ return strapiRelationValue(data, path, "slug");
346
+ }
347
+ function strapiRelationValue(data, path, field) {
348
+ const relation = unwrapStrapiRelation(readCmsDataPath(data, path));
349
+ const value = readCmsDataPath(relation, field);
350
+ if (typeof value === "string" && value.length > 0) {
351
+ return value;
352
+ }
353
+ if (typeof value === "number" && Number.isFinite(value)) {
354
+ return String(value);
355
+ }
356
+ return void 0;
357
+ }
358
+ function unwrapStrapiRelation(value) {
359
+ const record = asRecord2(value);
360
+ if (record && "data" in record) {
361
+ return unwrapStrapiRelation(record.data);
362
+ }
363
+ if (Array.isArray(value)) {
364
+ return unwrapStrapiRelation(value[0]);
365
+ }
366
+ if (record && "attributes" in record) {
367
+ const attributes = asRecord2(record.attributes);
368
+ return attributes ? { id: record.id, ...attributes } : record;
369
+ }
370
+ return value;
371
+ }
372
+ function asRecord2(value) {
373
+ if (value && typeof value === "object" && !Array.isArray(value)) {
374
+ return value;
375
+ }
376
+ return void 0;
377
+ }
378
+
327
379
  // src/scan.ts
328
380
  async function scanDocuments(options) {
329
381
  const fetchImpl = options.fetch ?? fetch;
@@ -333,7 +385,7 @@ async function scanDocuments(options) {
333
385
  const documents = filterDocuments(options.documents, options.filters);
334
386
  const diagnostics = [];
335
387
  await assertSiteReachable(
336
- options.config.site.url,
388
+ resolveSiteHealthUrl(options.config.site).toString(),
337
389
  fetchImpl,
338
390
  timeoutMs,
339
391
  retries
@@ -379,6 +431,9 @@ function resolveRouteCandidates(config, documents, diagnostics) {
379
431
  (candidate) => candidate.type === document.type
380
432
  );
381
433
  if (!route) {
434
+ if (document.routable === false) {
435
+ continue;
436
+ }
382
437
  diagnostics.push(
383
438
  createDiagnostic({
384
439
  severity: "info",
@@ -502,6 +557,16 @@ async function checkRouteReachability(config, candidates, fetchImpl, timeoutMs,
502
557
  });
503
558
  return results.flat();
504
559
  }
560
+ function resolveSiteHealthUrl(site) {
561
+ if (site.healthUrl) {
562
+ return new URL(site.healthUrl);
563
+ }
564
+ const siteUrl = new URL(site.url);
565
+ if (site.healthPath) {
566
+ return resolveSiteRouteUrl(siteUrl, site.healthPath);
567
+ }
568
+ return siteUrl;
569
+ }
505
570
  function resolveSiteRouteUrl(siteUrl, path) {
506
571
  const url = new URL(path, siteUrl);
507
572
  if (!url.search && siteUrl.search) {
@@ -564,7 +629,7 @@ function checkSeoFields(config, documents) {
564
629
  const checkMetaTitle = typeof seo === "object" ? seo.metaTitle !== false : true;
565
630
  const checkMetaDescription = typeof seo === "object" ? seo.metaDescription !== false : true;
566
631
  for (const document of documents) {
567
- const data = asRecord2(document.data);
632
+ const data = asRecord3(document.data);
568
633
  if (!data) {
569
634
  continue;
570
635
  }
@@ -692,7 +757,7 @@ function collectImagesMissingAlt(value, provider, path = "data") {
692
757
  (item, index) => collectImagesMissingAlt(item, provider, `${path}[${index}]`)
693
758
  );
694
759
  }
695
- const record = asRecord2(value);
760
+ const record = asRecord3(value);
696
761
  if (!record) {
697
762
  return [];
698
763
  }
@@ -720,7 +785,7 @@ function imageAltCandidate(provider, record) {
720
785
  if (provider === "contentful" && isContentfulImageRecord(record)) {
721
786
  return { isImage: true, value: record.description ?? record.title };
722
787
  }
723
- if (provider === "sanity" && "asset" in record && (asRecord2(record.asset)?._ref || asRecord2(record.asset)?._id)) {
788
+ if (provider === "sanity" && "asset" in record && (asRecord3(record.asset)?._ref || asRecord3(record.asset)?._id)) {
724
789
  return { isImage: true, value: record.alt };
725
790
  }
726
791
  return { isImage: false, value: void 0 };
@@ -732,7 +797,7 @@ function hasImageExtension(value) {
732
797
  return typeof value === "string" && /\.(?:avif|gif|jpe?g|png|svg|webp)$/i.test(value);
733
798
  }
734
799
  function isContentfulImageRecord(record) {
735
- const file = asRecord2(record.file);
800
+ const file = asRecord3(record.file);
736
801
  return typeof file?.url === "string" && (isContentfulImageHost(file.url) || hasImageExtension(file.url)) || hasImageExtension(file?.fileName);
737
802
  }
738
803
  function isContentfulImageHost(value) {
@@ -883,7 +948,7 @@ function isMissingFieldValue(value) {
883
948
  function isBlankOrPlaceholderAlt(value) {
884
949
  return isBlank(value) || typeof value === "string" && ["image", "photo", "picture"].includes(value.trim().toLowerCase());
885
950
  }
886
- function asRecord2(value) {
951
+ function asRecord3(value) {
887
952
  if (value && typeof value === "object" && !Array.isArray(value)) {
888
953
  return value;
889
954
  }
@@ -911,7 +976,10 @@ export {
911
976
  listDiagnosticExplanations,
912
977
  loadCmsLabConfig,
913
978
  readCmsDataPath,
979
+ resolveSiteHealthUrl,
914
980
  scanDocuments,
981
+ strapiRelationSlug,
982
+ strapiRelationValue,
915
983
  summarizeDiagnostics,
916
984
  validateConfig
917
985
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cms-lab/core",
3
- "version": "1.0.10",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Core config, scan, diagnostics, and checks for cms-lab.",
6
6
  "license": "MIT",