@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
package/dist/index.d.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * else: `lintSheet` (Layer 2 only, for already-validated Sheets),
6
6
  * `validateAndLint` (Layer 1 → Layer 2, the CLI's own pipeline),
7
7
  * `validateOrganizationSheet` (Layer 1 for the Commerce Organization
8
- * Sheet; no organization catalogue exists yet), the commerce
9
- * catalogue object, and the types those signatures mention.
8
+ * Sheet alone), the three page-type catalogue objects, and the types
9
+ * those signatures mention.
10
10
  * The bundled schemas (`generated/`), the compiled validators and the
11
11
  * individual rule modules are internal. The release manifest
12
12
  * (`RELEASE_MANIFEST`, `sheetSpecVersionFor`, `schemaUrlFor`) and the
@@ -21,6 +21,7 @@ export { validateOrganizationSheet } from "./validate-organization.js";
21
21
  export type { ValidateOrganizationResult } from "./validate-organization.js";
22
22
  export { commerceCatalogue } from "./catalogue.js";
23
23
  export { organizationCatalogue } from "./organization-catalogue.js";
24
+ export { collectionCatalogue } from "./collection-catalogue.js";
24
25
  export { detectSheetType, detectSheetLine } from "./sheet-type.js";
25
26
  export type { SheetType, SheetLine } from "./sheet-type.js";
26
27
  export { RELEASE_MANIFEST, KNOWN_RELEASES, CURRENT_RELEASE, sheetSpecVersionFor, schemaUrlFor, } from "./releases.js";
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * else: `lintSheet` (Layer 2 only, for already-validated Sheets),
6
6
  * `validateAndLint` (Layer 1 → Layer 2, the CLI's own pipeline),
7
7
  * `validateOrganizationSheet` (Layer 1 for the Commerce Organization
8
- * Sheet; no organization catalogue exists yet), the commerce
9
- * catalogue object, and the types those signatures mention.
8
+ * Sheet alone), the three page-type catalogue objects, and the types
9
+ * those signatures mention.
10
10
  * The bundled schemas (`generated/`), the compiled validators and the
11
11
  * individual rule modules are internal. The release manifest
12
12
  * (`RELEASE_MANIFEST`, `sheetSpecVersionFor`, `schemaUrlFor`) and the
@@ -19,6 +19,7 @@ export { validateAndLint } from "./validate-and-lint.js";
19
19
  export { validateOrganizationSheet } from "./validate-organization.js";
20
20
  export { commerceCatalogue } from "./catalogue.js";
21
21
  export { organizationCatalogue } from "./organization-catalogue.js";
22
+ export { collectionCatalogue } from "./collection-catalogue.js";
22
23
  export { detectSheetType, detectSheetLine } from "./sheet-type.js";
23
24
  export { RELEASE_MANIFEST, KNOWN_RELEASES, CURRENT_RELEASE, sheetSpecVersionFor, schemaUrlFor, } from "./releases.js";
24
25
  export { SCHEMA_IDS as BUNDLED_SCHEMA_IDS } from "./generated/schema-ids.js";
@@ -2,9 +2,10 @@ import type { LintOptions, Report, Sheet } from "@pagefront/lint-core";
2
2
  /**
3
3
  * Lint a Sheet against the commerce rule catalogue for its page type:
4
4
  * the product catalogue for product sheets, the organization catalogue
5
- * for Commerce Organization Sheets, dispatched by sheet-type detection
6
- * (see sheet-type.ts). Product rules never run on organization sheets
7
- * and vice versa.
5
+ * for Commerce Organization Sheets, the collection catalogue for
6
+ * Commerce Collection Sheets, dispatched by sheet-type detection (see
7
+ * sheet-type.ts). No page type's rules run on another page type's
8
+ * sheets.
8
9
  *
9
10
  * `Report.linter_version` identifies the invoking implementation, so the
10
11
  * default here is this package's version — lint-core's own fallback (its
@@ -1,14 +1,16 @@
1
1
  import { lint } from "@pagefront/lint-core";
2
2
  import { commerceCatalogue } from "./catalogue.js";
3
+ import { collectionCatalogue } from "./collection-catalogue.js";
3
4
  import { organizationCatalogue } from "./organization-catalogue.js";
4
5
  import { detectSheetType } from "./sheet-type.js";
5
6
  import { PACKAGE_VERSION } from "./version.js";
6
7
  /**
7
8
  * Lint a Sheet against the commerce rule catalogue for its page type:
8
9
  * the product catalogue for product sheets, the organization catalogue
9
- * for Commerce Organization Sheets, dispatched by sheet-type detection
10
- * (see sheet-type.ts). Product rules never run on organization sheets
11
- * and vice versa.
10
+ * for Commerce Organization Sheets, the collection catalogue for
11
+ * Commerce Collection Sheets, dispatched by sheet-type detection (see
12
+ * sheet-type.ts). No page type's rules run on another page type's
13
+ * sheets.
12
14
  *
13
15
  * `Report.linter_version` identifies the invoking implementation, so the
14
16
  * default here is this package's version — lint-core's own fallback (its
@@ -18,8 +20,9 @@ export function lintSheet(sheet, options) {
18
20
  const opts = { ...options };
19
21
  if (opts.linterVersion === undefined)
20
22
  opts.linterVersion = PACKAGE_VERSION;
21
- const catalogue = detectSheetType(sheet) === "organization"
22
- ? organizationCatalogue
23
- : commerceCatalogue;
23
+ const type = detectSheetType(sheet);
24
+ const catalogue = type === "organization" ? organizationCatalogue
25
+ : type === "collection" ? collectionCatalogue
26
+ : commerceCatalogue;
24
27
  return lint(sheet, catalogue, opts);
25
28
  }
@@ -3,9 +3,9 @@ import type { Catalogue } from "@pagefront/lint-core";
3
3
  * The Commerce Organization Sheet rule catalogue — the executable form
4
4
  * of the "Commerce Organization Sheet rules" section of
5
5
  * commerce/spec/rules.md (normative). It shares rules.md's catalogue
6
- * version with the product catalogue: one normative document, one
7
- * version, two page-type rule sets dispatched by sheet type (see
8
- * sheet-type.ts). Product rules never run on organization sheets and
9
- * vice versa.
6
+ * version with the product and collection catalogues: one normative
7
+ * document, one version, three page-type rule sets dispatched by sheet
8
+ * type (see sheet-type.ts). No page type's rules run on another page
9
+ * type's sheets.
10
10
  */
11
11
  export declare const organizationCatalogue: Catalogue;
@@ -9,18 +9,19 @@ import { orgW003 } from "./rules/org-w-003.js";
9
9
  import { orgW004 } from "./rules/org-w-004.js";
10
10
  import { orgW005 } from "./rules/org-w-005.js";
11
11
  import { orgW006 } from "./rules/org-w-006.js";
12
+ import { orgW007 } from "./rules/org-w-007.js";
12
13
  /**
13
14
  * The Commerce Organization Sheet rule catalogue — the executable form
14
15
  * of the "Commerce Organization Sheet rules" section of
15
16
  * commerce/spec/rules.md (normative). It shares rules.md's catalogue
16
- * version with the product catalogue: one normative document, one
17
- * version, two page-type rule sets dispatched by sheet type (see
18
- * sheet-type.ts). Product rules never run on organization sheets and
19
- * vice versa.
17
+ * version with the product and collection catalogues: one normative
18
+ * document, one version, three page-type rule sets dispatched by sheet
19
+ * type (see sheet-type.ts). No page type's rules run on another page
20
+ * type's sheets.
20
21
  */
21
22
  export const organizationCatalogue = {
22
23
  vertical: "commerce",
23
24
  releases: KNOWN_RELEASES,
24
- catalogueVersion: "0.16.0",
25
- rules: [orgE001, orgE002, orgE003, orgE004, orgW001, orgW002, orgW003, orgW004, orgW005, orgW006],
25
+ catalogueVersion: "0.17.0",
26
+ rules: [orgE001, orgE002, orgE003, orgE004, orgW001, orgW002, orgW003, orgW004, orgW005, orgW006, orgW007],
26
27
  };
@@ -0,0 +1,36 @@
1
+ import type { ValidateFunction } from "ajv";
2
+ import type { SheetType } from "./sheet-type.js";
3
+ /**
4
+ * The shape of the release manifest (commerce/spec/releases.json), as
5
+ * the registry reads it: each release names the member versions it
6
+ * bundles, and a sheet type a release does not bundle is absent.
7
+ */
8
+ export interface ManifestRelease {
9
+ release: string;
10
+ date: string;
11
+ vocabulary: string;
12
+ sheets: Readonly<Partial<Record<SheetType, string>>>;
13
+ }
14
+ export interface ReleaseManifestShape {
15
+ schema_url_template: string;
16
+ releases: readonly ManifestRelease[];
17
+ legacy: {
18
+ envelope_field: string;
19
+ format_versions: readonly string[];
20
+ schemas: Readonly<Partial<Record<SheetType, string>>>;
21
+ };
22
+ }
23
+ /**
24
+ * Test-only overrides of what the package bundles: a release manifest
25
+ * other than commerce/spec/releases.json, and Layer 1 validators for
26
+ * schema ids the frozen store does not carry (an editable head). The
27
+ * production code path never sets them, so it reads the bundled
28
+ * manifest and the compiled validators; tests install them through
29
+ * test-hooks.ts to exercise a release before it lands.
30
+ */
31
+ export interface TestOverrides {
32
+ manifest?: ReleaseManifestShape;
33
+ validators?: Readonly<Record<string, ValidateFunction>>;
34
+ }
35
+ export declare const overrides: () => TestOverrides;
36
+ export declare const setOverrides: (next: TestOverrides) => void;
@@ -0,0 +1,5 @@
1
+ let active = {};
2
+ export const overrides = () => active;
3
+ export const setOverrides = (next) => {
4
+ active = next;
5
+ };
@@ -1,22 +1,37 @@
1
1
  import { RELEASE_MANIFEST } from "./generated/release-manifest.js";
2
+ import { type ReleaseManifestShape } from "./overrides.js";
2
3
  import type { SheetType } from "./sheet-type.js";
3
4
  /**
4
5
  * The commerce release manifest (commerce/spec/releases.json): each
5
6
  * release and the member versions it bundles. Bundled at build time by
6
7
  * scripts/bundle-schemas.mjs, so the package always carries the
7
- * manifest it was built from.
8
+ * manifest it was built from. The exported constants describe the
9
+ * bundled manifest; the functions below read the manifest in force,
10
+ * which is the bundled one unless a test installed another through
11
+ * test-hooks.ts.
8
12
  */
9
13
  export { RELEASE_MANIFEST };
10
- /** Every release the manifest lists, in manifest order. */
14
+ /** Every release the bundled manifest lists, in manifest order. */
11
15
  export declare const KNOWN_RELEASES: readonly string[];
12
- /** The latest release the manifest lists. */
16
+ /** The latest release the bundled manifest lists. */
13
17
  export declare const CURRENT_RELEASE: string;
18
+ /** The manifest in force: the bundled one, or the one a test installed. */
19
+ export declare function manifest(): ReleaseManifestShape;
20
+ /** Every release the manifest in force lists, in manifest order. */
21
+ export declare function knownReleases(): readonly string[];
22
+ /** The latest release the manifest in force lists. */
23
+ export declare function currentRelease(): string;
14
24
  /**
15
25
  * The sheet-spec version a release bundles for a sheet type, or
16
- * undefined when the manifest does not list the release.
26
+ * undefined when the manifest does not list the release, or lists it
27
+ * without that sheet type (a type that joined the line in a later
28
+ * release).
17
29
  */
18
30
  export declare function sheetSpecVersionFor(release: string, sheetType: SheetType): string | undefined;
19
31
  /** The published URL of a sheet spec's JSON Schema, which is also its `$id`. */
20
32
  export declare function schemaUrlFor(sheetType: SheetType, version: string): string;
21
- /** The legacy draft line's schema URL for a sheet type. */
22
- export declare function legacySchemaUrl(sheetType: SheetType): string;
33
+ /**
34
+ * The legacy draft line's schema URL for a sheet type, or undefined for
35
+ * a type that was never on that line (the collection sheet).
36
+ */
37
+ export declare function legacySchemaUrl(sheetType: SheetType): string | undefined;
package/dist/releases.js CHANGED
@@ -1,27 +1,49 @@
1
1
  import { RELEASE_MANIFEST } from "./generated/release-manifest.js";
2
+ import { overrides } from "./overrides.js";
2
3
  /**
3
4
  * The commerce release manifest (commerce/spec/releases.json): each
4
5
  * release and the member versions it bundles. Bundled at build time by
5
6
  * scripts/bundle-schemas.mjs, so the package always carries the
6
- * manifest it was built from.
7
+ * manifest it was built from. The exported constants describe the
8
+ * bundled manifest; the functions below read the manifest in force,
9
+ * which is the bundled one unless a test installed another through
10
+ * test-hooks.ts.
7
11
  */
8
12
  export { RELEASE_MANIFEST };
9
- /** Every release the manifest lists, in manifest order. */
13
+ /** Every release the bundled manifest lists, in manifest order. */
10
14
  export const KNOWN_RELEASES = RELEASE_MANIFEST.releases.map((r) => r.release);
11
- /** The latest release the manifest lists. */
15
+ /** The latest release the bundled manifest lists. */
12
16
  export const CURRENT_RELEASE = KNOWN_RELEASES[KNOWN_RELEASES.length - 1];
17
+ /** The manifest in force: the bundled one, or the one a test installed. */
18
+ export function manifest() {
19
+ return overrides().manifest ?? RELEASE_MANIFEST;
20
+ }
21
+ /** Every release the manifest in force lists, in manifest order. */
22
+ export function knownReleases() {
23
+ return manifest().releases.map((r) => r.release);
24
+ }
25
+ /** The latest release the manifest in force lists. */
26
+ export function currentRelease() {
27
+ const releases = knownReleases();
28
+ return releases[releases.length - 1];
29
+ }
13
30
  /**
14
31
  * The sheet-spec version a release bundles for a sheet type, or
15
- * undefined when the manifest does not list the release.
32
+ * undefined when the manifest does not list the release, or lists it
33
+ * without that sheet type (a type that joined the line in a later
34
+ * release).
16
35
  */
17
36
  export function sheetSpecVersionFor(release, sheetType) {
18
- return RELEASE_MANIFEST.releases.find((r) => r.release === release)?.sheets[sheetType];
37
+ return manifest().releases.find((r) => r.release === release)?.sheets[sheetType];
19
38
  }
20
39
  /** The published URL of a sheet spec's JSON Schema, which is also its `$id`. */
21
40
  export function schemaUrlFor(sheetType, version) {
22
- return RELEASE_MANIFEST.schema_url_template.replace("{sheet}", sheetType).replace("{version}", version);
41
+ return manifest().schema_url_template.replace("{sheet}", sheetType).replace("{version}", version);
23
42
  }
24
- /** The legacy draft line's schema URL for a sheet type. */
43
+ /**
44
+ * The legacy draft line's schema URL for a sheet type, or undefined for
45
+ * a type that was never on that line (the collection sheet).
46
+ */
25
47
  export function legacySchemaUrl(sheetType) {
26
- return RELEASE_MANIFEST.legacy.schemas[sheetType];
48
+ return manifest().legacy.schemas[sheetType];
27
49
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared by PDP-E-019 and PDP-E-020: the checkout template an Offer may
3
+ * carry as Schema.org's `checkoutPageURLTemplate` (product.md, "Where to
4
+ * buy"), and the RFC 6570 syntax check PDP-E-019 applies to it.
5
+ *
6
+ * The syntax check follows the RFC's grammar (sections 2.1 to 2.4):
7
+ *
8
+ * expression = "{" [ operator ] variable-list "}"
9
+ * operator = "+" / "#" / "." / "/" / ";" / "?" / "&" (levels 2 and 3)
10
+ * / "=" / "," / "!" / "@" / "|" (reserved)
11
+ * variable-list = varspec *( "," varspec )
12
+ * varspec = varname [ ":" max-length / "*" ]
13
+ * varname = varchar *( ["."] varchar )
14
+ * varchar = ALPHA / DIGIT / "_" / pct-encoded
15
+ *
16
+ * Literals outside an expression may be any character except a control
17
+ * character, a space, `"`, `'`, `%` (unless it starts a pct-encoded
18
+ * triplet), `<`, `>`, `\`, `^`, `` ` ``, `{`, `|` and `}`. The reserved
19
+ * operators are a template error here: the RFC reserves them for future
20
+ * extensions and defines no expansion for them.
21
+ *
22
+ * Expansion (section 3.2) is implemented for the one defined variable,
23
+ * a scalar: the operator sets the expression's first character and the
24
+ * separator between varspecs, and `;`, `?` and `&` prefix the value with
25
+ * `name=`; a prefix modifier truncates the value; explode has no effect
26
+ * on a scalar; an undefined variable expands to nothing.
27
+ */
28
+ /** The one template variable the Product Sheet spec defines. */
29
+ export declare const DEFINED_VARIABLES: readonly string[];
30
+ export type TemplateProblem = {
31
+ kind: "not-string";
32
+ } | {
33
+ kind: "invalid";
34
+ detail: string;
35
+ } | {
36
+ kind: "undefined-variables";
37
+ variables: string[];
38
+ } | {
39
+ kind: "not-https";
40
+ expansion: string;
41
+ };
42
+ /**
43
+ * Parse an RFC 6570 template. Returns the variable names it references
44
+ * (each once, in order of appearance), or the first syntax problem.
45
+ */
46
+ export declare function parseTemplate(template: string): {
47
+ variables: string[];
48
+ } | {
49
+ detail: string;
50
+ };
51
+ /**
52
+ * Expand a template that `parseTemplate` accepted, with scalar string
53
+ * values. Values are used as given (the one defined value, "1", needs no
54
+ * encoding). A variable missing from `values` is undefined and expands
55
+ * to nothing, as the RFC prescribes.
56
+ */
57
+ export declare function expandTemplate(template: string, values: Record<string, string>): string;
58
+ /** True when `value` is a full absolute URL with the scheme `https`. */
59
+ export declare function isAbsoluteHttpsUrl(value: unknown): boolean;
60
+ /** True when `value` is a full absolute URL with the scheme `http` or `https`. */
61
+ export declare function isAbsoluteHttpUrl(value: unknown): boolean;
62
+ /**
63
+ * The problem with an Offer's `checkoutPageURLTemplate`, if any: the
64
+ * first of syntax, undefined variables, and the expansion with
65
+ * `quantity` set to 1 not being an absolute https URL.
66
+ */
67
+ export declare function checkoutTemplateProblem(value: unknown): TemplateProblem | null;
68
+ /** Whether a region property is present and not empty or whitespace-only. */
69
+ export declare function declaresRegion(offer: Record<string, unknown>): boolean;
@@ -0,0 +1,165 @@
1
+ import { isFullAbsoluteUrl } from "@pagefront/lint-core";
2
+ import { asObject, own } from "./configurator-pricing.js";
3
+ /**
4
+ * Shared by PDP-E-019 and PDP-E-020: the checkout template an Offer may
5
+ * carry as Schema.org's `checkoutPageURLTemplate` (product.md, "Where to
6
+ * buy"), and the RFC 6570 syntax check PDP-E-019 applies to it.
7
+ *
8
+ * The syntax check follows the RFC's grammar (sections 2.1 to 2.4):
9
+ *
10
+ * expression = "{" [ operator ] variable-list "}"
11
+ * operator = "+" / "#" / "." / "/" / ";" / "?" / "&" (levels 2 and 3)
12
+ * / "=" / "," / "!" / "@" / "|" (reserved)
13
+ * variable-list = varspec *( "," varspec )
14
+ * varspec = varname [ ":" max-length / "*" ]
15
+ * varname = varchar *( ["."] varchar )
16
+ * varchar = ALPHA / DIGIT / "_" / pct-encoded
17
+ *
18
+ * Literals outside an expression may be any character except a control
19
+ * character, a space, `"`, `'`, `%` (unless it starts a pct-encoded
20
+ * triplet), `<`, `>`, `\`, `^`, `` ` ``, `{`, `|` and `}`. The reserved
21
+ * operators are a template error here: the RFC reserves them for future
22
+ * extensions and defines no expansion for them.
23
+ *
24
+ * Expansion (section 3.2) is implemented for the one defined variable,
25
+ * a scalar: the operator sets the expression's first character and the
26
+ * separator between varspecs, and `;`, `?` and `&` prefix the value with
27
+ * `name=`; a prefix modifier truncates the value; explode has no effect
28
+ * on a scalar; an undefined variable expands to nothing.
29
+ */
30
+ /** The one template variable the Product Sheet spec defines. */
31
+ export const DEFINED_VARIABLES = ["quantity"];
32
+ const VARSPEC = /^(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})(?:\.?(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2}))*(?:\*|:[1-9][0-9]{0,3})?$/;
33
+ const LEVEL_OPERATORS = new Set(["+", "#", ".", "/", ";", "?", "&"]);
34
+ const RESERVED_OPERATORS = new Set(["=", ",", "!", "@", "|"]);
35
+ const EXCLUDED_LITERALS = new Set(['"', "'", "<", ">", "\\", "^", "`", "|"]);
36
+ /** The variable name of a varspec, without its prefix or explode modifier. */
37
+ function varnameOf(varspec) {
38
+ return varspec.replace(/(?:\*|:[1-9][0-9]{0,3})$/, "");
39
+ }
40
+ /**
41
+ * Parse an RFC 6570 template. Returns the variable names it references
42
+ * (each once, in order of appearance), or the first syntax problem.
43
+ */
44
+ export function parseTemplate(template) {
45
+ const variables = [];
46
+ let i = 0;
47
+ while (i < template.length) {
48
+ const ch = template[i];
49
+ if (ch === "{") {
50
+ const close = template.indexOf("}", i + 1);
51
+ if (close === -1)
52
+ return { detail: `the expression opened at position ${i} is never closed` };
53
+ let body = template.slice(i + 1, close);
54
+ if (body.length === 0)
55
+ return { detail: `the expression at position ${i} is empty` };
56
+ if (RESERVED_OPERATORS.has(body[0])) {
57
+ return { detail: `the operator \`${body[0]}\` at position ${i + 1} is reserved by RFC 6570 and has no expansion` };
58
+ }
59
+ if (LEVEL_OPERATORS.has(body[0]))
60
+ body = body.slice(1);
61
+ for (const varspec of body.split(",")) {
62
+ if (!VARSPEC.test(varspec))
63
+ return { detail: `\`${varspec}\` in the expression at position ${i} is not a variable specifier` };
64
+ const name = varnameOf(varspec);
65
+ if (!variables.includes(name))
66
+ variables.push(name);
67
+ }
68
+ i = close + 1;
69
+ continue;
70
+ }
71
+ if (ch === "}")
72
+ return { detail: `\`}\` at position ${i} is outside an expression` };
73
+ if (ch === "%") {
74
+ if (!/^%[0-9A-Fa-f]{2}/.test(template.slice(i, i + 3))) {
75
+ return { detail: `\`%\` at position ${i} does not start a percent-encoded triplet` };
76
+ }
77
+ i += 3;
78
+ continue;
79
+ }
80
+ const code = ch.charCodeAt(0);
81
+ if (code < 0x20 || code === 0x7f || ch === " " || EXCLUDED_LITERALS.has(ch)) {
82
+ const shown = code < 0x20 || code === 0x7f ? `U+${code.toString(16).toUpperCase().padStart(4, "0")}` : ch === " " ? "a space" : `\`${ch}\``;
83
+ return { detail: `${shown} at position ${i} is not allowed in a literal` };
84
+ }
85
+ i += 1;
86
+ }
87
+ return { variables };
88
+ }
89
+ /** Expansion rules per operator (RFC 6570, section 3.2, table at 3.2.1). */
90
+ const EXPANSION = {
91
+ "": { first: "", sep: ",", named: false },
92
+ "+": { first: "", sep: ",", named: false },
93
+ "#": { first: "#", sep: ",", named: false },
94
+ ".": { first: ".", sep: ".", named: false },
95
+ "/": { first: "/", sep: "/", named: false },
96
+ ";": { first: ";", sep: ";", named: true },
97
+ "?": { first: "?", sep: "&", named: true },
98
+ "&": { first: "&", sep: "&", named: true },
99
+ };
100
+ /**
101
+ * Expand a template that `parseTemplate` accepted, with scalar string
102
+ * values. Values are used as given (the one defined value, "1", needs no
103
+ * encoding). A variable missing from `values` is undefined and expands
104
+ * to nothing, as the RFC prescribes.
105
+ */
106
+ export function expandTemplate(template, values) {
107
+ return template.replace(/\{([^}]*)\}/g, (_, body) => {
108
+ const operator = LEVEL_OPERATORS.has(body[0]) ? body[0] : "";
109
+ const rule = EXPANSION[operator];
110
+ const parts = [];
111
+ for (const varspec of (operator === "" ? body : body.slice(1)).split(",")) {
112
+ const name = varnameOf(varspec);
113
+ if (!(name in values))
114
+ continue;
115
+ let value = values[name];
116
+ const prefix = /:([1-9][0-9]{0,3})$/.exec(varspec);
117
+ if (prefix !== null)
118
+ value = value.slice(0, Number(prefix[1]));
119
+ if (rule.named) {
120
+ parts.push(operator === ";" && value === "" ? name : `${name}=${value}`);
121
+ }
122
+ else {
123
+ parts.push(value);
124
+ }
125
+ }
126
+ return parts.length === 0 ? "" : rule.first + parts.join(rule.sep);
127
+ });
128
+ }
129
+ /** True when `value` is a full absolute URL with the scheme `https`. */
130
+ export function isAbsoluteHttpsUrl(value) {
131
+ return isFullAbsoluteUrl(value) && /^https:/i.test(value);
132
+ }
133
+ /** True when `value` is a full absolute URL with the scheme `http` or `https`. */
134
+ export function isAbsoluteHttpUrl(value) {
135
+ return isFullAbsoluteUrl(value) && /^https?:/i.test(value);
136
+ }
137
+ /**
138
+ * The problem with an Offer's `checkoutPageURLTemplate`, if any: the
139
+ * first of syntax, undefined variables, and the expansion with
140
+ * `quantity` set to 1 not being an absolute https URL.
141
+ */
142
+ export function checkoutTemplateProblem(value) {
143
+ if (typeof value !== "string")
144
+ return { kind: "not-string" };
145
+ const parsed = parseTemplate(value);
146
+ if ("detail" in parsed)
147
+ return { kind: "invalid", detail: parsed.detail };
148
+ const undefinedVariables = parsed.variables.filter((v) => !DEFINED_VARIABLES.includes(v));
149
+ if (undefinedVariables.length > 0)
150
+ return { kind: "undefined-variables", variables: undefinedVariables };
151
+ const expansion = expandTemplate(value, { quantity: "1" });
152
+ return isAbsoluteHttpsUrl(expansion) ? null : { kind: "not-https", expansion };
153
+ }
154
+ /** Whether a region property is present and not empty or whitespace-only. */
155
+ export function declaresRegion(offer) {
156
+ return ["areaServed", "eligibleRegion"].some((key) => {
157
+ const value = own(offer, key);
158
+ if (typeof value === "string")
159
+ return value.trim().length > 0;
160
+ if (Array.isArray(value))
161
+ return value.length > 0;
162
+ const obj = asObject(value);
163
+ return obj !== null && Object.keys(obj).length > 0;
164
+ });
165
+ }
@@ -0,0 +1,11 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * COL-E-001 — Member entry without `@id`.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * `@id` is the identity every reference to a set or a product carries
7
+ * and is matched by exact string, so a `hasPart` or `isPartOf` entry
8
+ * without one names nothing a consumer can recognise. One finding per
9
+ * entry, at the entry's path.
10
+ */
11
+ export declare const colE001: Rule;
@@ -0,0 +1,31 @@
1
+ import { entryName, matchedField, memberEntries, nonBlank } from "./col-shared.js";
2
+ /**
3
+ * COL-E-001 — Member entry without `@id`.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * `@id` is the identity every reference to a set or a product carries
7
+ * and is matched by exact string, so a `hasPart` or `isPartOf` entry
8
+ * without one names nothing a consumer can recognise. One finding per
9
+ * entry, at the entry's path.
10
+ */
11
+ export const colE001 = {
12
+ id: "COL-E-001",
13
+ title: "Member entry without @id",
14
+ severity: "error",
15
+ target: "$.data['hasPart','isPartOf']",
16
+ check: (match) => {
17
+ const field = matchedField(match.path);
18
+ const hits = [];
19
+ for (const { entry, path } of memberEntries(match.value, match.path)) {
20
+ if (nonBlank(entry["@id"]))
21
+ continue;
22
+ hits.push({ path, values: { name: entryName(entry), field } });
23
+ }
24
+ return hits;
25
+ },
26
+ messageTemplate: "Entry `{name}` at `{path}` in `{field}` carries no `@id`.",
27
+ remediation: "Add `@id`, the exact `@id` string the member or parent declares in its own sheet. An entry that cannot name its target's identity does not belong in the array.",
28
+ introduced: "release 0.5.0",
29
+ appliesFrom: "0.5.0",
30
+ specReference: "collection.md#members",
31
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ export declare const colE002: Rule;
@@ -0,0 +1,37 @@
1
+ import { asRecord, COLLECTION_KINDS } from "./col-shared.js";
2
+ /**
3
+ * COL-E-002 — Collection kind absent or outside the enum.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * `pagefront:collectionKind` is the required, single-valued kind
7
+ * discriminator; without it a consumer cannot tell whether it is
8
+ * walking a tree or reading a list. The finding sits at the field when
9
+ * it is present and at the data block when it is absent.
10
+ */
11
+ const ENUM_TEXT = COLLECTION_KINDS.map((k) => `\`${k}\``).join(", ");
12
+ export const colE002 = {
13
+ id: "COL-E-002",
14
+ title: "Collection kind absent or outside the enum",
15
+ severity: "error",
16
+ target: "$.data",
17
+ check: (match) => {
18
+ const data = asRecord(match.value);
19
+ if (data === null)
20
+ return null;
21
+ const kind = data["pagefront:collectionKind"];
22
+ if (kind === undefined)
23
+ return { values: { problem: "absent" } };
24
+ if (typeof kind === "string" && COLLECTION_KINDS.includes(kind))
25
+ return null;
26
+ const rendered = typeof kind === "string" ? kind : JSON.stringify(kind);
27
+ return {
28
+ path: `${match.path}['pagefront:collectionKind']`,
29
+ values: { problem: `\`${rendered}\`, which is not one of ${ENUM_TEXT}` },
30
+ };
31
+ },
32
+ messageTemplate: "`pagefront:collectionKind` at `{path}` is {problem}.",
33
+ remediation: "Declare `pagefront:collectionKind` as `category` (a node of the storefront's navigation tree), `collection` (a set outside the tree) or `brand` (the products sold under one label). Additional values require a spec amendment.",
34
+ introduced: "release 0.5.0",
35
+ appliesFrom: "0.5.0",
36
+ specReference: "collection.md#kind-discriminator-pagefrontcollectionkind",
37
+ };
@@ -0,0 +1,11 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * COL-E-003 — Release / schema mismatch.
4
+ * The collection-sheet counterpart of PDP-E-002 and ORG-E-003.
5
+ * Transcribed from commerce/spec/rules.md (normative); the check is
6
+ * shared (release-schema.ts).
7
+ *
8
+ * The Collection Sheet spec has no legacy draft line, so the rule does
9
+ * not run on legacy sheets.
10
+ */
11
+ export declare const colE003: Rule;
@@ -0,0 +1,18 @@
1
+ import { RELEASE_SCHEMA_MISMATCH, releaseSchemaMismatch } from "./release-schema.js";
2
+ /**
3
+ * COL-E-003 — Release / schema mismatch.
4
+ * The collection-sheet counterpart of PDP-E-002 and ORG-E-003.
5
+ * Transcribed from commerce/spec/rules.md (normative); the check is
6
+ * shared (release-schema.ts).
7
+ *
8
+ * The Collection Sheet spec has no legacy draft line, so the rule does
9
+ * not run on legacy sheets.
10
+ */
11
+ export const colE003 = {
12
+ id: "COL-E-003",
13
+ ...RELEASE_SCHEMA_MISMATCH,
14
+ check: releaseSchemaMismatch("collection"),
15
+ introduced: "release 0.5.0",
16
+ appliesFrom: "0.5.0",
17
+ specReference: "collection.md#the-envelope",
18
+ };
@@ -0,0 +1,12 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * COL-E-004 — Unknown release.
4
+ * The collection-sheet counterpart of PDP-E-018 and ORG-E-004.
5
+ * Transcribed from commerce/spec/rules.md (normative); the check is
6
+ * shared (release-schema.ts).
7
+ *
8
+ * `appliesFrom` is "0.0.0": the rule judges the declared release
9
+ * itself, so it cannot be gated by it. It never fires on a sheet of a
10
+ * published release.
11
+ */
12
+ export declare const colE004: Rule;