@contentrain/types 1.21.0 → 1.23.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 +29 -1
- package/dist/index.d.mts +338 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +168 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -38,7 +38,8 @@ const SOURCE_ACCESS_LADDER = [
|
|
|
38
38
|
const SEO_PROVIDERS = [
|
|
39
39
|
"yoast",
|
|
40
40
|
"rank_math",
|
|
41
|
-
"aioseo"
|
|
41
|
+
"aioseo",
|
|
42
|
+
"seopress"
|
|
42
43
|
];
|
|
43
44
|
/** Where a candidate was found. */
|
|
44
45
|
const TEXT_CANDIDATE_KINDS = [
|
|
@@ -701,6 +702,171 @@ function isMigrateStudioClaim(input) {
|
|
|
701
702
|
return validateMigrateStudioClaim(input).ok;
|
|
702
703
|
}
|
|
703
704
|
//#endregion
|
|
705
|
+
//#region src/project-plan.ts
|
|
706
|
+
const PROJECT_PLAN_FORMAT = "contentrain-project-plan@1";
|
|
707
|
+
/** Semantic design roles the kit reads — the starter's `@theme` names without the `--` prefix. */
|
|
708
|
+
const PLAN_TOKEN_ROLES = [
|
|
709
|
+
"color-surface",
|
|
710
|
+
"color-surface-muted",
|
|
711
|
+
"color-ink",
|
|
712
|
+
"color-ink-muted",
|
|
713
|
+
"color-line",
|
|
714
|
+
"color-accent",
|
|
715
|
+
"color-accent-ink",
|
|
716
|
+
"font-sans",
|
|
717
|
+
"font-serif",
|
|
718
|
+
"font-mono",
|
|
719
|
+
"container-prose",
|
|
720
|
+
"container-page",
|
|
721
|
+
"container-wide",
|
|
722
|
+
"radius-card"
|
|
723
|
+
];
|
|
724
|
+
const PLAN_VALUE_PATTERN = /^(?:field:[\w-]+|media:[\w-]+|ref:[\w-]+\.[\w-]+|href:(?:self|[\w-]+)|term:[\w-]+|ui:[\w.-]+|site:[\w-]+|menu:(?:primary|footer)|page:(?:base|current|total|breadcrumb)|const:(?:true|false|-?\d+(?:\.\d+)?|[a-z][a-z0-9_-]{0,31}))$/;
|
|
725
|
+
const PERMALINK = /^\/(?:[^/\s]+\/)*$/;
|
|
726
|
+
const IMPORTED_MODELS = new Set([
|
|
727
|
+
"posts",
|
|
728
|
+
"pages",
|
|
729
|
+
"categories",
|
|
730
|
+
"tags",
|
|
731
|
+
"authors",
|
|
732
|
+
"media",
|
|
733
|
+
"menus",
|
|
734
|
+
"menu-items",
|
|
735
|
+
"site",
|
|
736
|
+
"ui-strings"
|
|
737
|
+
]);
|
|
738
|
+
/**
|
|
739
|
+
* Structural checks a plan must pass before anything is written: every reference resolves (routes →
|
|
740
|
+
* components → models), permalinks are well-formed, kit components carry a kit id, plan models carry
|
|
741
|
+
* their fields. Kit catalog and fact checks (does the kit id exist, does the template exist) belong to
|
|
742
|
+
* the caller, which holds those documents.
|
|
743
|
+
*/
|
|
744
|
+
function validateProjectPlan(plan) {
|
|
745
|
+
const errors = [];
|
|
746
|
+
const warnings = [];
|
|
747
|
+
if (plan.format !== "contentrain-project-plan@1") errors.push(`format is not ${PROJECT_PLAN_FORMAT}`);
|
|
748
|
+
if (!/^https?:\/\/[^/]+$/.test(plan.source?.origin ?? "")) errors.push("source.origin is not an http(s) origin without a path");
|
|
749
|
+
const site = plan.site;
|
|
750
|
+
for (const [key, value] of Object.entries(site?.permalinks ?? {})) if (!PERMALINK.test(value)) errors.push(`site.permalinks.${key} must start and end with "/" (${value})`);
|
|
751
|
+
if (!Number.isSafeInteger(site?.postsPerPage) || site.postsPerPage < 1) errors.push("site.postsPerPage is not a positive integer");
|
|
752
|
+
for (const [from, to] of Object.entries(site?.redirects ?? {})) {
|
|
753
|
+
if (!from.startsWith("/")) errors.push(`redirect ${from} does not start with "/"`);
|
|
754
|
+
const status = typeof to === "string" ? 301 : to.status;
|
|
755
|
+
if (![
|
|
756
|
+
301,
|
|
757
|
+
302,
|
|
758
|
+
307,
|
|
759
|
+
308,
|
|
760
|
+
410
|
|
761
|
+
].includes(status)) errors.push(`redirect ${from} has status ${status}`);
|
|
762
|
+
}
|
|
763
|
+
for (const role of Object.keys(site?.tokens?.roles ?? {})) if (!PLAN_TOKEN_ROLES.includes(role)) errors.push(`tokens.roles.${role} is not a kit role`);
|
|
764
|
+
const models = /* @__PURE__ */ new Map();
|
|
765
|
+
for (const m of plan.models ?? []) {
|
|
766
|
+
if (models.has(m.id)) errors.push(`model ${m.id} is declared twice`);
|
|
767
|
+
models.set(m.id, m);
|
|
768
|
+
if (m.origin === "import" && !IMPORTED_MODELS.has(m.id)) warnings.push(`model ${m.id} is marked imported but wp-import does not write it`);
|
|
769
|
+
if (m.origin === "plan") {
|
|
770
|
+
if (!m.fields) errors.push(`plan model ${m.id} has no fields`);
|
|
771
|
+
if (!m.name || !m.domain) errors.push(`plan model ${m.id} needs name and domain`);
|
|
772
|
+
if (m.kind === "collection" || m.kind === "document") {
|
|
773
|
+
const title = m.title_field ? m.fields?.[m.title_field] : void 0;
|
|
774
|
+
if (!m.title_field) errors.push(`plan model ${m.id} has no title_field`);
|
|
775
|
+
else if (!title) errors.push(`plan model ${m.id}: title_field ${m.title_field} is not one of its fields`);
|
|
776
|
+
else if (!isTitleFieldType(title.type)) errors.push(`plan model ${m.id}: title_field ${m.title_field} is ${title.type}, not a text-like type`);
|
|
777
|
+
}
|
|
778
|
+
if (!m.extract?.length) warnings.push(`plan model ${m.id} has no extraction — its entries start empty`);
|
|
779
|
+
}
|
|
780
|
+
for (const x of m.extract ?? []) {
|
|
781
|
+
if (!x.rule && !x.fields) errors.push(`model ${m.id} extraction from ${x.from} has neither rule nor fields`);
|
|
782
|
+
if (x.rule && !/^(gutenberg|elementor|divi|classic):[\w./:-]+$/.test(x.rule)) errors.push(`model ${m.id} extraction rule ${x.rule} is not <builder>:<match>`);
|
|
783
|
+
for (const [field, path] of Object.entries(x.fields ?? {})) {
|
|
784
|
+
if (m.fields && !m.fields[field]) errors.push(`model ${m.id} extraction fills unknown field ${field}`);
|
|
785
|
+
if (!/^attr:[\w.-]+$|^[\d.]+\|(text|href|src|alt|datetime)$|^(?:dom|img|link|html):[^@]*(?:@[\w-]+)?$/.test(path)) errors.push(`model ${m.id} field ${field}: path ${path} is not "<path>|<prop>", "attr:<name>" or an element expression`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const knownModel = (id) => models.has(id) || IMPORTED_MODELS.has(id);
|
|
790
|
+
const components = /* @__PURE__ */ new Map();
|
|
791
|
+
for (const c of plan.components ?? []) {
|
|
792
|
+
if (components.has(c.id)) errors.push(`component ${c.id} is declared twice`);
|
|
793
|
+
components.set(c.id, c);
|
|
794
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(c.id)) errors.push(`component ${c.id} is not a PascalCase name`);
|
|
795
|
+
if (c.origin === "kit" && !c.kit?.id) errors.push(`kit component ${c.id} names no kit id`);
|
|
796
|
+
if (c.origin === "site" && !c.props) errors.push(`site component ${c.id} declares no props`);
|
|
797
|
+
if (c.origin === "site" && !c.brief) warnings.push(`site component ${c.id} has no brief for the writer`);
|
|
798
|
+
}
|
|
799
|
+
const placement = (s, at, route) => {
|
|
800
|
+
const c = components.get(s.component);
|
|
801
|
+
if (!c) {
|
|
802
|
+
errors.push(`${at}: component ${s.component} is not declared`);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const b = s.bind;
|
|
806
|
+
if ("model" in b && !knownModel(b.model)) errors.push(`${at}: model ${b.model} is not declared`);
|
|
807
|
+
if (b.kind === "entry" && !route?.source) errors.push(`${at}: binds the route entry but ${route ? "the route has no source" : "layout has no entry"}`);
|
|
808
|
+
const values = {
|
|
809
|
+
..."props" in b ? b.props : {},
|
|
810
|
+
...b.kind === "collection" && !b.into ? b.item : {}
|
|
811
|
+
};
|
|
812
|
+
for (const [prop, value] of Object.entries({
|
|
813
|
+
...values,
|
|
814
|
+
...b.kind === "collection" ? b.item : {}
|
|
815
|
+
})) if (!PLAN_VALUE_PATTERN.test(value)) errors.push(`${at}: ${prop} = ${value} is not a plan value (field: media: ref: href: term: ui: site: menu: page: const:)`);
|
|
816
|
+
if (c.props) {
|
|
817
|
+
const bound = new Set([
|
|
818
|
+
...Object.keys(values),
|
|
819
|
+
...Object.keys(s.labels ?? {}),
|
|
820
|
+
..."into" in b && b.into ? [b.into] : b.kind === "menu" ? ["items"] : []
|
|
821
|
+
]);
|
|
822
|
+
for (const prop of bound) if (!c.props[prop]) errors.push(`${at}: ${c.id} has no prop ${prop}`);
|
|
823
|
+
for (const [prop, def] of Object.entries(c.props)) if (def.required && !bound.has(prop)) errors.push(`${at}: required prop ${c.id}.${prop} is not bound`);
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
if (plan.layout?.header) placement(plan.layout.header, "layout header");
|
|
827
|
+
if (plan.layout?.footer) placement(plan.layout.footer, "layout footer");
|
|
828
|
+
const routeIds = /* @__PURE__ */ new Set();
|
|
829
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
830
|
+
for (const r of plan.routes ?? []) {
|
|
831
|
+
if (routeIds.has(r.id)) errors.push(`route ${r.id} is declared twice`);
|
|
832
|
+
routeIds.add(r.id);
|
|
833
|
+
if (!PERMALINK.test(r.pattern)) errors.push(`route ${r.id}: pattern ${r.pattern} must start and end with "/"`);
|
|
834
|
+
if (r.source) {
|
|
835
|
+
if (!knownModel(r.source.model)) errors.push(`route ${r.id}: source model ${r.source.model} is not declared`);
|
|
836
|
+
(byModel.get(r.source.model) ?? byModel.set(r.source.model, []).get(r.source.model)).push(r);
|
|
837
|
+
}
|
|
838
|
+
if (r.body === "composed" && !r.sections.length) warnings.push(`route ${r.id} is composed but has no sections`);
|
|
839
|
+
r.sections.forEach((s, i) => placement(s, `route ${r.id} section ${i}`, r));
|
|
840
|
+
}
|
|
841
|
+
const byPattern = /* @__PURE__ */ new Map();
|
|
842
|
+
for (const r of plan.routes ?? []) (byPattern.get(r.pattern) ?? byPattern.set(r.pattern, []).get(r.pattern)).push(r);
|
|
843
|
+
for (const [pattern, routes] of byPattern) {
|
|
844
|
+
if (routes.length < 2) continue;
|
|
845
|
+
const sources = new Set(routes.map((r) => r.source?.model ?? ""));
|
|
846
|
+
if (!(sources.size === 1 && !sources.has("") && routes.filter((r) => !r.source?.where).length <= 1)) warnings.push(`routes ${routes.map((r) => r.id).join(", ")} share the pattern ${pattern}`);
|
|
847
|
+
}
|
|
848
|
+
for (const [model, routes] of byModel) {
|
|
849
|
+
if (routes.length < 2) continue;
|
|
850
|
+
const catchAll = routes.filter((r) => !r.source?.where || Object.keys(r.source.where).length === 0);
|
|
851
|
+
if (catchAll.length > 1) errors.push(`model ${model}: routes ${catchAll.map((r) => r.id).join(", ")} are all catch-alls`);
|
|
852
|
+
const seen = /* @__PURE__ */ new Map();
|
|
853
|
+
for (const r of routes) {
|
|
854
|
+
const ids = r.source?.where?.wp_id;
|
|
855
|
+
if (r.source?.where && ids === void 0) warnings.push(`route ${r.id}: where without wp_id — overlap with other ${model} routes cannot be checked`);
|
|
856
|
+
for (const id of Array.isArray(ids) ? ids : ids === void 0 ? [] : [ids]) {
|
|
857
|
+
const other = seen.get(id);
|
|
858
|
+
if (other) errors.push(`model ${model}: entry ${String(id)} is in routes ${other} and ${r.id}`);
|
|
859
|
+
else seen.set(id, r.id);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (!plan.routes?.some((r) => r.kind === "home")) errors.push("no home route");
|
|
864
|
+
return {
|
|
865
|
+
errors,
|
|
866
|
+
warnings
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
//#endregion
|
|
704
870
|
//#region src/provider.ts
|
|
705
871
|
/** Capability set for the LocalProvider (simple-git + worktree). */
|
|
706
872
|
const LOCAL_CAPABILITIES = {
|
|
@@ -1725,6 +1891,6 @@ function serializeMarkdownFrontmatter(data, body) {
|
|
|
1725
1891
|
return lines.join("\n");
|
|
1726
1892
|
}
|
|
1727
1893
|
//#endregion
|
|
1728
|
-
export { APPROVAL_GATES, APPROVAL_MODES, CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_COMPONENT_CLOSE, CHROME_COMPONENT_OPEN, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, DEFAULT_APPROVAL_POLICY, DICTIONARY_TITLE_FIELD, ENTRY_ID_PATTERN, EXECUTION_CONTRACT_VERSION, INTEGRATION_CATEGORIES, INTEGRATION_EVIDENCE_KINDS, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, MIGRATE_STUDIO_CLAIM_ALG, MIGRATE_STUDIO_CLAIM_AUDIENCE, MIGRATE_STUDIO_CLAIM_CLOCK_SKEW_SECONDS, MIGRATE_STUDIO_CLAIM_ISSUER, MIGRATE_STUDIO_CLAIM_MAX_TRIAL_DAYS, MIGRATE_STUDIO_CLAIM_MAX_TTL_SECONDS, MIGRATE_STUDIO_CLAIM_VERSION, MIGRATE_STUDIO_PLANS, MIGRATION_CONTRACT_VERSION, MODEL_EXTENSION_KEYS, MODEL_FIELD_ORDER, PAGE_BODY_KINDS, PATH_PATTERNS, RAW_MARK_SUFFIX, RESERVED_PATHS, RISK_CLASSES, RUN_STATUSES, SCHEDULE_KINDS, SECRET_PATTERNS, SEO_PROVIDERS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, SOURCE_DELTA_OPS, TERMINAL_RUN_STATUSES, TEXT_CANDIDATE_KINDS, TEXT_EXCLUDE_REASONS, TITLE_FIELD_TYPES, approversFor, canonicalStringify, commentsExportSource, componentSlot, computePlanHash, conflictId, describeModelLocaleScope, detectSecrets, effectiveRisk, evaluateApproval, generateEntryId, highestRisk, isMediaType, isMigrateStudioClaim, isRepoRelativePath, isReservedPath, isStoredMediaPath, isTerminalRunStatus, isTitleFieldType, looksLikeCredential, orderedFieldNames, parseFrontmatterScalar, parseFrontmatterScalarString, parseMarkdownFrontmatter, planHashPayload, requiredApprovals, resolveFieldLabel, resolveModelLocales, rewriteEntryMedia, rewriteFieldMedia, rewriteMarkdownMedia, rewriteMediaUrl, riskRank, serializeMarkdownFrontmatter, sortKeys, splitFrontmatterList, stableHash, toDeliveryUrl, validateAccept, validateEntryId, validateFieldValue, validateHandoffCommentsExport, validateLocale, validateMigrateStudioClaim, validateModelLocales, validateSemanticType, validateSlug };
|
|
1894
|
+
export { APPROVAL_GATES, APPROVAL_MODES, CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_COMPONENT_CLOSE, CHROME_COMPONENT_OPEN, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, DEFAULT_APPROVAL_POLICY, DICTIONARY_TITLE_FIELD, ENTRY_ID_PATTERN, EXECUTION_CONTRACT_VERSION, INTEGRATION_CATEGORIES, INTEGRATION_EVIDENCE_KINDS, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, MIGRATE_STUDIO_CLAIM_ALG, MIGRATE_STUDIO_CLAIM_AUDIENCE, MIGRATE_STUDIO_CLAIM_CLOCK_SKEW_SECONDS, MIGRATE_STUDIO_CLAIM_ISSUER, MIGRATE_STUDIO_CLAIM_MAX_TRIAL_DAYS, MIGRATE_STUDIO_CLAIM_MAX_TTL_SECONDS, MIGRATE_STUDIO_CLAIM_VERSION, MIGRATE_STUDIO_PLANS, MIGRATION_CONTRACT_VERSION, MODEL_EXTENSION_KEYS, MODEL_FIELD_ORDER, PAGE_BODY_KINDS, PATH_PATTERNS, PLAN_TOKEN_ROLES, PLAN_VALUE_PATTERN, PROJECT_PLAN_FORMAT, RAW_MARK_SUFFIX, RESERVED_PATHS, RISK_CLASSES, RUN_STATUSES, SCHEDULE_KINDS, SECRET_PATTERNS, SEO_PROVIDERS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, SOURCE_DELTA_OPS, TERMINAL_RUN_STATUSES, TEXT_CANDIDATE_KINDS, TEXT_EXCLUDE_REASONS, TITLE_FIELD_TYPES, approversFor, canonicalStringify, commentsExportSource, componentSlot, computePlanHash, conflictId, describeModelLocaleScope, detectSecrets, effectiveRisk, evaluateApproval, generateEntryId, highestRisk, isMediaType, isMigrateStudioClaim, isRepoRelativePath, isReservedPath, isStoredMediaPath, isTerminalRunStatus, isTitleFieldType, looksLikeCredential, orderedFieldNames, parseFrontmatterScalar, parseFrontmatterScalarString, parseMarkdownFrontmatter, planHashPayload, requiredApprovals, resolveFieldLabel, resolveModelLocales, rewriteEntryMedia, rewriteFieldMedia, rewriteMarkdownMedia, rewriteMediaUrl, riskRank, serializeMarkdownFrontmatter, sortKeys, splitFrontmatterList, stableHash, toDeliveryUrl, validateAccept, validateEntryId, validateFieldValue, validateHandoffCommentsExport, validateLocale, validateMigrateStudioClaim, validateModelLocales, validateProjectPlan, validateSemanticType, validateSlug };
|
|
1729
1895
|
|
|
1730
1896
|
//# sourceMappingURL=index.mjs.map
|