@cms-lab/core 1.1.0 → 1.2.1

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
@@ -5,6 +5,7 @@ type CMSDocument = {
5
5
  uid?: string;
6
6
  url?: string;
7
7
  routable?: boolean;
8
+ entryKind?: "collection" | "single";
8
9
  status: CMSDocumentStatus;
9
10
  data: unknown;
10
11
  };
@@ -50,18 +51,22 @@ type CmsFieldMappingConfig = {
50
51
  uidField?: string;
51
52
  urlField?: string;
52
53
  };
54
+ type StrapiLocaleConfig = {
55
+ locale?: string;
56
+ };
53
57
  type StrapiCollectionConfig = {
54
58
  type: string;
55
59
  endpoint: string;
56
- } & CmsFieldMappingConfig;
60
+ } & CmsFieldMappingConfig & StrapiLocaleConfig;
57
61
  type StrapiSingleTypeConfig = {
58
62
  type: string;
59
63
  endpoint: string;
60
- } & CmsFieldMappingConfig;
64
+ } & CmsFieldMappingConfig & StrapiLocaleConfig;
61
65
  type StrapiCmsProviderConfig = {
62
66
  provider: "strapi";
63
67
  url: string;
64
68
  token?: string;
69
+ locale?: string;
65
70
  collections?: StrapiCollectionConfig[];
66
71
  singleTypes?: StrapiSingleTypeConfig[];
67
72
  };
@@ -114,6 +119,8 @@ type CmsProviderConfig = PrismicCmsProviderConfig | StrapiCmsProviderConfig | Di
114
119
  type CmsLabConfig = {
115
120
  site: {
116
121
  url: string;
122
+ healthPath?: string;
123
+ healthUrl?: string;
117
124
  };
118
125
  framework: {
119
126
  type: "next";
@@ -187,6 +194,9 @@ declare class SiteUnreachableError extends CmsLabError {
187
194
  constructor(message: string);
188
195
  }
189
196
 
197
+ declare function strapiRelationSlug(data: unknown, path: string): string | undefined;
198
+ declare function strapiRelationValue(data: unknown, path: string, field: string): string | undefined;
199
+
190
200
  type ScanDocumentsOptions = {
191
201
  config: CmsLabConfig;
192
202
  project: ProjectInfo;
@@ -198,5 +208,6 @@ type ScanDocumentsOptions = {
198
208
  filters?: ScanFilters;
199
209
  };
200
210
  declare function scanDocuments(options: ScanDocumentsOptions): Promise<ScanResult>;
211
+ declare function resolveSiteHealthUrl(site: CmsLabConfig["site"]): URL;
201
212
 
202
- 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 StrapiSingleTypeConfig, 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,15 +53,20 @@ 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
+ };
56
59
  var strapiContentShape = z.object({
57
60
  type: z.string().min(1),
58
61
  endpoint: z.string().min(1),
59
- ...cmsFieldMappingShape
62
+ ...cmsFieldMappingShape,
63
+ ...strapiLocaleShape
60
64
  }).strict();
61
65
  var strapiConfigSchema = z.object({
62
66
  provider: z.literal("strapi"),
63
67
  url: z.string().url(),
64
68
  token: z.string().optional(),
69
+ ...strapiLocaleShape,
65
70
  collections: z.array(strapiContentShape).min(1).optional(),
66
71
  singleTypes: z.array(strapiContentShape).min(1).optional()
67
72
  }).refine(
@@ -160,7 +165,12 @@ var checksSchema = z.object({
160
165
  }).strict().optional();
161
166
  var configSchema = z.object({
162
167
  site: z.object({
163
- 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()
164
174
  }).strict(),
165
175
  framework: z.object({
166
176
  type: z.literal("next"),
@@ -330,6 +340,42 @@ function listDiagnosticExplanations() {
330
340
  return [...explanations];
331
341
  }
332
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
+
333
379
  // src/scan.ts
334
380
  async function scanDocuments(options) {
335
381
  const fetchImpl = options.fetch ?? fetch;
@@ -339,7 +385,7 @@ async function scanDocuments(options) {
339
385
  const documents = filterDocuments(options.documents, options.filters);
340
386
  const diagnostics = [];
341
387
  await assertSiteReachable(
342
- options.config.site.url,
388
+ resolveSiteHealthUrl(options.config.site).toString(),
343
389
  fetchImpl,
344
390
  timeoutMs,
345
391
  retries
@@ -511,6 +557,16 @@ async function checkRouteReachability(config, candidates, fetchImpl, timeoutMs,
511
557
  });
512
558
  return results.flat();
513
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
+ }
514
570
  function resolveSiteRouteUrl(siteUrl, path) {
515
571
  const url = new URL(path, siteUrl);
516
572
  if (!url.search && siteUrl.search) {
@@ -573,7 +629,7 @@ function checkSeoFields(config, documents) {
573
629
  const checkMetaTitle = typeof seo === "object" ? seo.metaTitle !== false : true;
574
630
  const checkMetaDescription = typeof seo === "object" ? seo.metaDescription !== false : true;
575
631
  for (const document of documents) {
576
- const data = asRecord2(document.data);
632
+ const data = asRecord3(document.data);
577
633
  if (!data) {
578
634
  continue;
579
635
  }
@@ -701,7 +757,7 @@ function collectImagesMissingAlt(value, provider, path = "data") {
701
757
  (item, index) => collectImagesMissingAlt(item, provider, `${path}[${index}]`)
702
758
  );
703
759
  }
704
- const record = asRecord2(value);
760
+ const record = asRecord3(value);
705
761
  if (!record) {
706
762
  return [];
707
763
  }
@@ -729,7 +785,7 @@ function imageAltCandidate(provider, record) {
729
785
  if (provider === "contentful" && isContentfulImageRecord(record)) {
730
786
  return { isImage: true, value: record.description ?? record.title };
731
787
  }
732
- 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)) {
733
789
  return { isImage: true, value: record.alt };
734
790
  }
735
791
  return { isImage: false, value: void 0 };
@@ -741,7 +797,7 @@ function hasImageExtension(value) {
741
797
  return typeof value === "string" && /\.(?:avif|gif|jpe?g|png|svg|webp)$/i.test(value);
742
798
  }
743
799
  function isContentfulImageRecord(record) {
744
- const file = asRecord2(record.file);
800
+ const file = asRecord3(record.file);
745
801
  return typeof file?.url === "string" && (isContentfulImageHost(file.url) || hasImageExtension(file.url)) || hasImageExtension(file?.fileName);
746
802
  }
747
803
  function isContentfulImageHost(value) {
@@ -892,7 +948,7 @@ function isMissingFieldValue(value) {
892
948
  function isBlankOrPlaceholderAlt(value) {
893
949
  return isBlank(value) || typeof value === "string" && ["image", "photo", "picture"].includes(value.trim().toLowerCase());
894
950
  }
895
- function asRecord2(value) {
951
+ function asRecord3(value) {
896
952
  if (value && typeof value === "object" && !Array.isArray(value)) {
897
953
  return value;
898
954
  }
@@ -920,7 +976,10 @@ export {
920
976
  listDiagnosticExplanations,
921
977
  loadCmsLabConfig,
922
978
  readCmsDataPath,
979
+ resolveSiteHealthUrl,
923
980
  scanDocuments,
981
+ strapiRelationSlug,
982
+ strapiRelationValue,
924
983
  summarizeDiagnostics,
925
984
  validateConfig
926
985
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cms-lab/core",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "description": "Core config, scan, diagnostics, and checks for cms-lab.",
6
6
  "license": "MIT",