@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.
- package/README.md +57 -13
- package/dist/catalogue.js +6 -2
- package/dist/collection-catalogue.d.ts +15 -0
- package/dist/collection-catalogue.js +30 -0
- package/dist/generated/release-manifest.d.ts +9 -0
- package/dist/generated/release-manifest.js +10 -0
- package/dist/generated/schema-ids.js +3 -0
- package/dist/generated/schemas.js +2744 -23
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/lint-sheet.d.ts +4 -3
- package/dist/lint-sheet.js +9 -6
- package/dist/organization-catalogue.d.ts +4 -4
- package/dist/organization-catalogue.js +7 -6
- package/dist/overrides.d.ts +36 -0
- package/dist/overrides.js +5 -0
- package/dist/releases.d.ts +21 -6
- package/dist/releases.js +30 -8
- package/dist/rules/checkout-template.d.ts +69 -0
- package/dist/rules/checkout-template.js +165 -0
- package/dist/rules/col-e-001.d.ts +11 -0
- package/dist/rules/col-e-001.js +31 -0
- package/dist/rules/col-e-002.d.ts +2 -0
- package/dist/rules/col-e-002.js +37 -0
- package/dist/rules/col-e-003.d.ts +11 -0
- package/dist/rules/col-e-003.js +18 -0
- package/dist/rules/col-e-004.d.ts +12 -0
- package/dist/rules/col-e-004.js +19 -0
- package/dist/rules/col-e-005.d.ts +16 -0
- package/dist/rules/col-e-005.js +64 -0
- package/dist/rules/col-shared.d.ts +25 -0
- package/dist/rules/col-shared.js +43 -0
- package/dist/rules/col-w-001.d.ts +11 -0
- package/dist/rules/col-w-001.js +21 -0
- package/dist/rules/col-w-002.d.ts +12 -0
- package/dist/rules/col-w-002.js +31 -0
- package/dist/rules/col-w-004.d.ts +12 -0
- package/dist/rules/col-w-004.js +37 -0
- package/dist/rules/col-w-005.d.ts +12 -0
- package/dist/rules/col-w-005.js +41 -0
- package/dist/rules/col-w-006.d.ts +11 -0
- package/dist/rules/col-w-006.js +37 -0
- package/dist/rules/org-w-007.d.ts +15 -0
- package/dist/rules/org-w-007.js +47 -0
- package/dist/rules/pdp-e-005.d.ts +3 -1
- package/dist/rules/pdp-e-005.js +7 -5
- package/dist/rules/pdp-e-017.js +34 -6
- package/dist/rules/pdp-e-019.d.ts +2 -0
- package/dist/rules/pdp-e-019.js +50 -0
- package/dist/rules/pdp-e-020.d.ts +13 -0
- package/dist/rules/pdp-e-020.js +31 -0
- package/dist/rules/pdp-e-021.d.ts +11 -0
- package/dist/rules/pdp-e-021.js +32 -0
- package/dist/rules/pdp-w-021.js +1 -1
- package/dist/rules/pdp-w-031.d.ts +4 -4
- package/dist/rules/pdp-w-031.js +9 -7
- package/dist/rules/pdp-w-040.d.ts +12 -0
- package/dist/rules/pdp-w-040.js +37 -0
- package/dist/rules/release-schema.d.ts +3 -2
- package/dist/rules/release-schema.js +11 -11
- package/dist/schema-registry.d.ts +11 -0
- package/dist/schema-registry.js +24 -9
- package/dist/schema-validators.js +14 -5
- package/dist/sheet-type.d.ts +17 -12
- package/dist/sheet-type.js +11 -0
- package/dist/test-hooks.d.ts +15 -0
- package/dist/test-hooks.js +28 -0
- package/dist/validate-and-lint.d.ts +9 -5
- package/dist/validate-and-lint.js +33 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
|
9
|
-
*
|
|
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
|
|
9
|
-
*
|
|
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";
|
package/dist/lint-sheet.d.ts
CHANGED
|
@@ -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,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
package/dist/lint-sheet.js
CHANGED
|
@@ -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,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
22
|
-
|
|
23
|
-
:
|
|
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
|
|
7
|
-
* version,
|
|
8
|
-
* sheet-type.ts).
|
|
9
|
-
*
|
|
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
|
|
17
|
-
* version,
|
|
18
|
-
* sheet-type.ts).
|
|
19
|
-
*
|
|
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.
|
|
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;
|
package/dist/releases.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
22
|
-
|
|
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
|
|
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
|
|
41
|
+
return manifest().schema_url_template.replace("{sheet}", sheetType).replace("{version}", version);
|
|
23
42
|
}
|
|
24
|
-
/**
|
|
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
|
|
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,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;
|