@pagefront/lint-commerce 0.12.0 → 0.13.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.
Files changed (72) hide show
  1. package/README.md +57 -13
  2. package/dist/catalogue.js +6 -2
  3. package/dist/collection-catalogue.d.ts +15 -0
  4. package/dist/collection-catalogue.js +30 -0
  5. package/dist/generated/release-manifest.d.ts +9 -0
  6. package/dist/generated/release-manifest.js +10 -0
  7. package/dist/generated/schema-ids.js +3 -0
  8. package/dist/generated/schemas.js +2744 -23
  9. package/dist/index.d.ts +3 -2
  10. package/dist/index.js +3 -2
  11. package/dist/lint-sheet.d.ts +4 -3
  12. package/dist/lint-sheet.js +9 -6
  13. package/dist/organization-catalogue.d.ts +4 -4
  14. package/dist/organization-catalogue.js +7 -6
  15. package/dist/overrides.d.ts +36 -0
  16. package/dist/overrides.js +5 -0
  17. package/dist/releases.d.ts +21 -6
  18. package/dist/releases.js +30 -8
  19. package/dist/rules/checkout-template.d.ts +69 -0
  20. package/dist/rules/checkout-template.js +165 -0
  21. package/dist/rules/col-e-001.d.ts +11 -0
  22. package/dist/rules/col-e-001.js +31 -0
  23. package/dist/rules/col-e-002.d.ts +2 -0
  24. package/dist/rules/col-e-002.js +37 -0
  25. package/dist/rules/col-e-003.d.ts +11 -0
  26. package/dist/rules/col-e-003.js +18 -0
  27. package/dist/rules/col-e-004.d.ts +12 -0
  28. package/dist/rules/col-e-004.js +19 -0
  29. package/dist/rules/col-e-005.d.ts +16 -0
  30. package/dist/rules/col-e-005.js +64 -0
  31. package/dist/rules/col-shared.d.ts +25 -0
  32. package/dist/rules/col-shared.js +43 -0
  33. package/dist/rules/col-w-001.d.ts +11 -0
  34. package/dist/rules/col-w-001.js +21 -0
  35. package/dist/rules/col-w-002.d.ts +12 -0
  36. package/dist/rules/col-w-002.js +31 -0
  37. package/dist/rules/col-w-004.d.ts +12 -0
  38. package/dist/rules/col-w-004.js +37 -0
  39. package/dist/rules/col-w-005.d.ts +12 -0
  40. package/dist/rules/col-w-005.js +41 -0
  41. package/dist/rules/col-w-006.d.ts +11 -0
  42. package/dist/rules/col-w-006.js +37 -0
  43. package/dist/rules/org-w-007.d.ts +15 -0
  44. package/dist/rules/org-w-007.js +47 -0
  45. package/dist/rules/pdp-e-005.d.ts +3 -1
  46. package/dist/rules/pdp-e-005.js +7 -5
  47. package/dist/rules/pdp-e-017.js +34 -6
  48. package/dist/rules/pdp-e-019.d.ts +2 -0
  49. package/dist/rules/pdp-e-019.js +50 -0
  50. package/dist/rules/pdp-e-020.d.ts +13 -0
  51. package/dist/rules/pdp-e-020.js +31 -0
  52. package/dist/rules/pdp-e-021.d.ts +11 -0
  53. package/dist/rules/pdp-e-021.js +32 -0
  54. package/dist/rules/pdp-w-021.js +1 -1
  55. package/dist/rules/pdp-w-031.d.ts +4 -4
  56. package/dist/rules/pdp-w-031.js +9 -7
  57. package/dist/rules/pdp-w-040.d.ts +12 -0
  58. package/dist/rules/pdp-w-040.js +37 -0
  59. package/dist/rules/release-schema.d.ts +3 -2
  60. package/dist/rules/release-schema.js +11 -11
  61. package/dist/schema-registry.d.ts +11 -0
  62. package/dist/schema-registry.js +24 -9
  63. package/dist/schema-validators.js +14 -5
  64. package/dist/sheet-type.d.ts +17 -12
  65. package/dist/sheet-type.js +11 -0
  66. package/dist/test-hooks.d.ts +15 -0
  67. package/dist/test-hooks.js +28 -0
  68. package/dist/validate-and-lint.d.ts +9 -5
  69. package/dist/validate-and-lint.js +33 -5
  70. package/dist/version.d.ts +1 -1
  71. package/dist/version.js +1 -1
  72. package/package.json +2 -2
@@ -0,0 +1,31 @@
1
+ import { asObject } from "./configurator-pricing.js";
2
+ import { declaresRegion } from "./checkout-template.js";
3
+ /**
4
+ * PDP-E-020 — Checkout template without a region.
5
+ * Transcribed from commerce/spec/rules.md (normative).
6
+ *
7
+ * An Offer that carries `checkoutPageURLTemplate` declares the market the
8
+ * checkout serves with `areaServed` or `eligibleRegion` ("Where to buy").
9
+ * A region property that is empty or whitespace-only counts as absent.
10
+ * The template's own validity is PDP-E-019's concern. Applies from
11
+ * release 0.5.0, whose Product Sheet spec 0.11.0 introduces the
12
+ * requirement.
13
+ */
14
+ export const pdpE020 = {
15
+ id: "PDP-E-020",
16
+ title: "Checkout template without a region",
17
+ severity: "error",
18
+ target: "$.data.offers[*]",
19
+ check: (match) => {
20
+ const offer = asObject(match.value);
21
+ if (offer === null || !Object.prototype.hasOwnProperty.call(offer, "checkoutPageURLTemplate"))
22
+ return null;
23
+ return declaresRegion(offer) ? null : { path: match.path, values: {} };
24
+ },
25
+ messageTemplate: "Offer at `{path}` carries `checkoutPageURLTemplate` and neither `areaServed` nor `eligibleRegion`.",
26
+ remediation: "Declare the region the checkout serves, or remove the template.",
27
+ introduced: "release 0.5.0",
28
+ // The requirement is introduced by release 0.5.0 (Product Sheet spec 0.11.0).
29
+ appliesFrom: "0.5.0",
30
+ specReference: "product.md#where-to-buy",
31
+ };
@@ -0,0 +1,11 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-E-021 — Offer `url` is not an absolute URL.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * An Offer's `url` is the page on which the shopper buys it ("Where to
7
+ * buy"); when present it must be a full absolute URL with the scheme
8
+ * http or https. Silent when the Offer carries no `url` (PDP-W-040's
9
+ * concern on a sheet with several offers). Applies from release 0.5.0.
10
+ */
11
+ export declare const pdpE021: Rule;
@@ -0,0 +1,32 @@
1
+ import { asObject, own } from "./configurator-pricing.js";
2
+ import { isAbsoluteHttpUrl } from "./checkout-template.js";
3
+ /**
4
+ * PDP-E-021 — Offer `url` is not an absolute URL.
5
+ * Transcribed from commerce/spec/rules.md (normative).
6
+ *
7
+ * An Offer's `url` is the page on which the shopper buys it ("Where to
8
+ * buy"); when present it must be a full absolute URL with the scheme
9
+ * http or https. Silent when the Offer carries no `url` (PDP-W-040's
10
+ * concern on a sheet with several offers). Applies from release 0.5.0.
11
+ */
12
+ export const pdpE021 = {
13
+ id: "PDP-E-021",
14
+ title: "Offer url is not an absolute URL",
15
+ severity: "error",
16
+ target: "$.data.offers[*]",
17
+ check: (match) => {
18
+ const offer = asObject(match.value);
19
+ if (offer === null || !Object.prototype.hasOwnProperty.call(offer, "url"))
20
+ return null;
21
+ const url = own(offer, "url");
22
+ if (isAbsoluteHttpUrl(url))
23
+ return null;
24
+ return { path: `${match.path}['url']`, values: { url: JSON.stringify(url) } };
25
+ },
26
+ messageTemplate: "Offer `url` at `{path}` is `{url}`, which is not an absolute `http` or `https` URL.",
27
+ remediation: "Give the offer the absolute URL of the page where it is bought.",
28
+ introduced: "release 0.5.0",
29
+ // The requirement is introduced by release 0.5.0 (Product Sheet spec 0.11.0).
30
+ appliesFrom: "0.5.0",
31
+ specReference: "product.md#where-to-buy",
32
+ };
@@ -46,7 +46,7 @@ export const pdpW021 = {
46
46
  }
47
47
  return hits;
48
48
  },
49
- messageTemplate: "Variant link `{name}` at `{path}` appears to reference a separate Sheet by `@id` but omits the recommended `sheet_url`; consumers must reach the target Sheet via `url` + `describedby`.",
49
+ messageTemplate: "Variant link `{name}` at `{path}` appears to reference a separate Sheet by `@id` but omits the recommended `sheet_url`; consumers must reach the target Sheet via `url` + `alternate`.",
50
50
  remediation: "Add `sheet_url` with the target's Sheet URL — the variant's Sheet URL on a parent `hasVariant` entry, or the parent's Sheet URL on a variant's `isVariantOf`. If you do not emit a separate Sheet for the target (the family is modeled inline in one ProductGroup Sheet), no cross-Sheet link exists and `sheet_url` does not apply.",
51
51
  introduced: "v0.7",
52
52
  appliesFrom: "0.3.0",
@@ -3,10 +3,10 @@ import type { Rule } from "@pagefront/lint-core";
3
3
  * PDP-W-031 — Seller reference not locatable.
4
4
  * Transcribed from commerce/spec/rules.md (normative).
5
5
  *
6
- * Fires when an offer's `seller` carries `@id` but no
7
- * `pagefront:sheetUrl`, and the offer carries neither
6
+ * Fires when an offer's `seller` carries `@id` but neither
7
+ * `pagefront:sheetUrl` nor `url`, and the offer carries neither
8
8
  * `hasMerchantReturnPolicy` nor `shippingDetails`: the terms it would
9
- * inherit sit on a seller sheet no consumer can locate. The finding
10
- * sits at the `seller` path.
9
+ * inherit sit on a seller sheet no consumer can reach, by the hint or
10
+ * through the seller's page. The finding sits at the `seller` path.
11
11
  */
12
12
  export declare const pdpW031: Rule;
@@ -2,11 +2,11 @@
2
2
  * PDP-W-031 — Seller reference not locatable.
3
3
  * Transcribed from commerce/spec/rules.md (normative).
4
4
  *
5
- * Fires when an offer's `seller` carries `@id` but no
6
- * `pagefront:sheetUrl`, and the offer carries neither
5
+ * Fires when an offer's `seller` carries `@id` but neither
6
+ * `pagefront:sheetUrl` nor `url`, and the offer carries neither
7
7
  * `hasMerchantReturnPolicy` nor `shippingDetails`: the terms it would
8
- * inherit sit on a seller sheet no consumer can locate. The finding
9
- * sits at the `seller` path.
8
+ * inherit sit on a seller sheet no consumer can reach, by the hint or
9
+ * through the seller's page. The finding sits at the `seller` path.
10
10
  */
11
11
  export const pdpW031 = {
12
12
  id: "PDP-W-031",
@@ -22,14 +22,16 @@ export const pdpW031 = {
22
22
  if (typeof seller !== "object" || seller === null || Array.isArray(seller))
23
23
  return null;
24
24
  const s = seller;
25
- if (s["@id"] === undefined || s["pagefront:sheetUrl"] !== undefined)
25
+ if (s["@id"] === undefined)
26
+ return null;
27
+ if (s["pagefront:sheetUrl"] !== undefined || s["url"] !== undefined)
26
28
  return null;
27
29
  if (o["hasMerchantReturnPolicy"] !== undefined || o["shippingDetails"] !== undefined)
28
30
  return null;
29
31
  return { path: `${match.path}['seller']` };
30
32
  },
31
- messageTemplate: "Seller at `{path}` has an `@id` but no `pagefront:sheetUrl`, and the Offer states no return or shipping terms of its own.",
32
- remediation: "Add `pagefront:sheetUrl` to the seller reference, or state the terms on the Offer.",
33
+ messageTemplate: "Seller at `{path}` has an `@id` but neither `pagefront:sheetUrl` nor `url`, and the Offer states no return or shipping terms of its own.",
34
+ remediation: "Add `url` or `pagefront:sheetUrl` to the seller reference, or state the terms on the Offer.",
33
35
  introduced: "v0.9",
34
36
  appliesFrom: "0.3.0",
35
37
  appliesToLegacy: true,
@@ -0,0 +1,12 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-W-040 — Offer without `url` among several offers.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * An Offer without `url` is bought on the product's page; that default
7
+ * serves a single offer. When `offers` is an array with more than one
8
+ * entry, each entry should state its own `url` ("Where to buy"). One
9
+ * finding per entry without it; silent on a single offer. Applies from
10
+ * release 0.5.0.
11
+ */
12
+ export declare const pdpW040: Rule;
@@ -0,0 +1,37 @@
1
+ import { asObject, own } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-W-040 — Offer without `url` among several offers.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * An Offer without `url` is bought on the product's page; that default
7
+ * serves a single offer. When `offers` is an array with more than one
8
+ * entry, each entry should state its own `url` ("Where to buy"). One
9
+ * finding per entry without it; silent on a single offer. Applies from
10
+ * release 0.5.0.
11
+ */
12
+ export const pdpW040 = {
13
+ id: "PDP-W-040",
14
+ title: "Offer without url among several offers",
15
+ severity: "warning",
16
+ target: "$.data",
17
+ check: (match) => {
18
+ const data = asObject(match.value);
19
+ const offers = own(data, "offers");
20
+ if (!Array.isArray(offers) || offers.length < 2)
21
+ return null;
22
+ const hits = [];
23
+ offers.forEach((entry, index) => {
24
+ const offer = asObject(entry);
25
+ if (offer !== null && !Object.prototype.hasOwnProperty.call(offer, "url")) {
26
+ hits.push({ path: `${match.path}['offers'][${index}]`, values: {} });
27
+ }
28
+ });
29
+ return hits.length > 0 ? hits : null;
30
+ },
31
+ messageTemplate: "Offer at `{path}` has no `url`, and the sheet carries several offers.",
32
+ remediation: "Give each offer the URL of the page where it is bought.",
33
+ introduced: "release 0.5.0",
34
+ // "Where to buy" is introduced by release 0.5.0 (Product Sheet spec 0.11.0).
35
+ appliesFrom: "0.5.0",
36
+ specReference: "product.md#where-to-buy",
37
+ };
@@ -1,5 +1,5 @@
1
1
  import type { CheckFn, Rule } from "@pagefront/lint-core";
2
- import type { SheetType } from "../sheet-type.js";
2
+ import { type SheetType } from "../sheet-type.js";
3
3
  /**
4
4
  * Release / schema mismatch. The rule reads `$schema` and is quiet
5
5
  * whenever it is absent, on both lines: whether `$schema` is required is
@@ -8,7 +8,8 @@ import type { SheetType } from "../sheet-type.js";
8
8
  * present `$schema` must be the URL of the sheet-spec schema the release
9
9
  * manifest maps the declared `release` and the sheet type to; a release
10
10
  * the manifest does not list has no mapping (the unknown-release rule
11
- * reports it). On the legacy line the `$schema` version segment must
11
+ * reports it), and neither has a listed release that bundles no schema
12
+ * for the sheet type (Layer 1 reports that). On the legacy line the `$schema` version segment must
12
13
  * equal `format_version`, as before. Quiet when the sheet carries both
13
14
  * fields (the both-fields rule reports that).
14
15
  */
@@ -1,15 +1,13 @@
1
- import { KNOWN_RELEASES, schemaUrlFor, sheetSpecVersionFor } from "../releases.js";
1
+ import { knownReleases, schemaUrlFor, sheetSpecVersionFor } from "../releases.js";
2
+ import { SHEET_LABEL } from "../sheet-type.js";
2
3
  /**
3
- * Shared checks for the envelope's version fields, used by the product
4
- * and organization catalogues (PDP-E-002 / ORG-E-003, PDP-E-016 /
5
- * ORG-E-002). Transcribed from commerce/spec/rules.md (normative).
4
+ * Shared checks for the envelope's version fields, used by the product,
5
+ * organization and collection catalogues (PDP-E-002 / ORG-E-003 /
6
+ * COL-E-003, PDP-E-016 / ORG-E-002, PDP-E-018 / ORG-E-004 / COL-E-004).
7
+ * Transcribed from commerce/spec/rules.md (normative).
6
8
  */
7
9
  /** The trailing `v<version>.json` of a `$schema` URL. */
8
10
  const SCHEMA_VERSION = /v(\d+(?:\.\d+)*)\.json$/;
9
- const SHEET_LABEL = {
10
- product: "Product Sheet",
11
- organization: "Organization Sheet",
12
- };
13
11
  /**
14
12
  * Release / schema mismatch. The rule reads `$schema` and is quiet
15
13
  * whenever it is absent, on both lines: whether `$schema` is required is
@@ -18,7 +16,8 @@ const SHEET_LABEL = {
18
16
  * present `$schema` must be the URL of the sheet-spec schema the release
19
17
  * manifest maps the declared `release` and the sheet type to; a release
20
18
  * the manifest does not list has no mapping (the unknown-release rule
21
- * reports it). On the legacy line the `$schema` version segment must
19
+ * reports it), and neither has a listed release that bundles no schema
20
+ * for the sheet type (Layer 1 reports that). On the legacy line the `$schema` version segment must
22
21
  * equal `format_version`, as before. Quiet when the sheet carries both
23
22
  * fields (the both-fields rule reports that).
24
23
  */
@@ -104,9 +103,10 @@ export const BOTH_VERSION_FIELDS = {
104
103
  */
105
104
  export const unknownRelease = (_match, ctx) => {
106
105
  const { release } = ctx.sheet;
107
- if (typeof release !== "string" || KNOWN_RELEASES.includes(release))
106
+ const known = knownReleases();
107
+ if (typeof release !== "string" || known.includes(release))
108
108
  return null;
109
- return { path: "$['release']", values: { release, known: KNOWN_RELEASES.join(", ") } };
109
+ return { path: "$['release']", values: { release, known: known.join(", ") } };
110
110
  };
111
111
  export const UNKNOWN_RELEASE = {
112
112
  title: "Unknown release",
@@ -18,6 +18,17 @@ export type SchemaResolution = {
18
18
  kind: "unknown-release";
19
19
  declared: string;
20
20
  }
21
+ /**
22
+ * The release bundles no schema for the sheet's type: the type joined
23
+ * the line after that release (`release` is the release tried, the
24
+ * declared one or the current one), or the sheet is on the legacy
25
+ * draft line and the type was never on it (`release` is null).
26
+ */
27
+ | {
28
+ kind: "unbundled-type";
29
+ sheetType: SheetType;
30
+ release: string | null;
31
+ }
21
32
  /** A legacy sheet's `$schema` names no schema this package bundles. */
22
33
  | {
23
34
  kind: "unknown-schema";
@@ -1,5 +1,6 @@
1
1
  import { SCHEMA_IDS } from "./generated/schema-ids.js";
2
- import { CURRENT_RELEASE, legacySchemaUrl, schemaUrlFor, sheetSpecVersionFor, } from "./releases.js";
2
+ import { overrides } from "./overrides.js";
3
+ import { currentRelease, knownReleases, legacySchemaUrl, schemaUrlFor, sheetSpecVersionFor, } from "./releases.js";
3
4
  import { detectSheetLine, detectSheetType } from "./sheet-type.js";
4
5
  /**
5
6
  * The identifiers the legacy schemas carried before 2026-09-08
@@ -29,24 +30,38 @@ export function resolveSchema(sheet, sheetType) {
29
30
  if (sheet.release !== undefined) {
30
31
  // A non-string release is validated against the current schema,
31
32
  // whose `release` pattern rejects it.
32
- if (typeof sheet.release !== "string") {
33
- return { kind: "schema", id: schemaUrlFor(type, sheetSpecVersionFor(CURRENT_RELEASE, type)) };
34
- }
33
+ if (typeof sheet.release !== "string")
34
+ return currentSchema(type);
35
+ if (!knownReleases().includes(sheet.release))
36
+ return { kind: "unknown-release", declared: sheet.release };
35
37
  const version = sheetSpecVersionFor(sheet.release, type);
36
38
  if (version === undefined)
37
- return { kind: "unknown-release", declared: sheet.release };
39
+ return { kind: "unbundled-type", sheetType: type, release: sheet.release };
38
40
  return { kind: "schema", id: schemaUrlFor(type, version) };
39
41
  }
40
42
  if (typeof declared === "string") {
41
- if (SCHEMA_IDS.includes(declared))
43
+ if (SCHEMA_IDS.includes(declared) || overrides().validators?.[declared] !== undefined) {
42
44
  return { kind: "schema", id: declared };
45
+ }
43
46
  const former = FORMER_LEGACY_ID.exec(declared);
44
47
  if (former !== null)
45
48
  return { kind: "schema", id: legacySchemaUrl(former[1]) };
46
49
  if (detectSheetLine(sheet) === "legacy")
47
50
  return { kind: "unknown-schema", declared };
48
51
  }
49
- if (detectSheetLine(sheet) === "legacy")
50
- return { kind: "schema", id: legacySchemaUrl(type) };
51
- return { kind: "schema", id: schemaUrlFor(type, sheetSpecVersionFor(CURRENT_RELEASE, type)) };
52
+ if (detectSheetLine(sheet) === "legacy") {
53
+ const legacy = legacySchemaUrl(type);
54
+ return legacy === undefined
55
+ ? { kind: "unbundled-type", sheetType: type, release: null }
56
+ : { kind: "schema", id: legacy };
57
+ }
58
+ return currentSchema(type);
59
+ }
60
+ /** The current release's schema for a type, or unbundled-type when the current release has none. */
61
+ function currentSchema(type) {
62
+ const release = currentRelease();
63
+ const version = sheetSpecVersionFor(release, type);
64
+ if (version === undefined)
65
+ return { kind: "unbundled-type", sheetType: type, release };
66
+ return { kind: "schema", id: schemaUrlFor(type, version) };
52
67
  }