@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 +22 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.js +67 -8
- package/package.json +1 -1
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.
|
|
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 =
|
|
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 =
|
|
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 && (
|
|
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 =
|
|
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
|
|
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
|
};
|