@caelo-cms/shared 0.10.24 → 0.10.26
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/dist/ai-tools.d.ts +15 -10
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +13 -15
- package/dist/ai-tools.js.map +1 -1
- package/dist/build-page.d.ts +0 -2
- package/dist/build-page.d.ts.map +1 -1
- package/dist/build-page.js +2 -3
- package/dist/build-page.js.map +1 -1
- package/dist/content.d.ts +0 -3
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +91 -7
- package/dist/content.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/font-assets.d.ts +89 -0
- package/dist/font-assets.d.ts.map +1 -0
- package/dist/font-assets.js +61 -0
- package/dist/font-assets.js.map +1 -0
- package/dist/google-models.d.ts +35 -0
- package/dist/google-models.d.ts.map +1 -0
- package/dist/google-models.js +20 -0
- package/dist/google-models.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/media.d.ts +0 -3
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +0 -5
- package/dist/media.js.map +1 -1
- package/dist/plugin-preview.d.ts +44 -0
- package/dist/plugin-preview.d.ts.map +1 -0
- package/dist/plugin-preview.js +69 -0
- package/dist/plugin-preview.js.map +1 -0
- package/dist/preview-compose.d.ts +26 -0
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +92 -46
- package/dist/preview-compose.js.map +1 -1
- package/dist/responsive-images.d.ts +6 -5
- package/dist/responsive-images.d.ts.map +1 -1
- package/dist/responsive-images.js +6 -5
- package/dist/responsive-images.js.map +1 -1
- package/dist/seo.d.ts +10 -14
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +11 -12
- package/dist/seo.js.map +1 -1
- package/dist/template-engine.d.ts +18 -0
- package/dist/template-engine.d.ts.map +1 -1
- package/dist/template-engine.js +39 -2
- package/dist/template-engine.js.map +1 -1
- package/dist/theme-importers/loose.js +1 -1
- package/dist/theme-importers/loose.js.map +1 -1
- package/dist/theme-normalize.d.ts +9 -0
- package/dist/theme-normalize.d.ts.map +1 -1
- package/dist/theme-normalize.js +65 -6
- package/dist/theme-normalize.js.map +1 -1
- package/dist/theme-render.d.ts +15 -0
- package/dist/theme-render.d.ts.map +1 -1
- package/dist/theme-render.js +39 -0
- package/dist/theme-render.js.map +1 -1
- package/dist/themes.d.ts +15 -1
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +48 -1
- package/dist/themes.js.map +1 -1
- package/dist/url.d.ts +45 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +64 -0
- package/dist/url.js.map +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/redos-hardening.test.ts +1 -1
- package/src/ai-tools.ts +13 -15
- package/src/build-page.ts +3 -5
- package/src/content.test.ts +99 -17
- package/src/content.ts +95 -8
- package/src/context.ts +6 -0
- package/src/font-assets.ts +63 -0
- package/src/google-models.ts +20 -0
- package/src/index.ts +4 -2
- package/src/media.ts +0 -6
- package/src/plugin-preview.test.ts +79 -0
- package/src/plugin-preview.ts +81 -0
- package/src/preview-compose-deferrals.test.ts +144 -0
- package/src/preview-compose.test.ts +16 -0
- package/src/preview-compose.ts +125 -37
- package/src/responsive-images.ts +6 -5
- package/src/seo.test.ts +8 -8
- package/src/seo.ts +15 -23
- package/src/template-engine-data-lists.test.ts +90 -0
- package/src/template-engine.ts +72 -1
- package/src/theme-importers/loose.ts +1 -1
- package/src/theme-normalize.ts +70 -8
- package/src/theme-render.ts +36 -0
- package/src/theme-token-roles.test.ts +182 -0
- package/src/themes.ts +53 -0
- package/src/url.test.ts +40 -0
- package/src/url.ts +69 -0
- package/src/version.ts +1 -1
- package/dist/design-manifest.d.ts +0 -36
- package/dist/design-manifest.d.ts.map +0 -1
- package/dist/design-manifest.js +0 -90
- package/dist/design-manifest.js.map +0 -1
- package/dist/i18n.d.ts +0 -42
- package/dist/i18n.d.ts.map +0 -1
- package/dist/i18n.js +0 -85
- package/dist/i18n.js.map +0 -1
- package/src/design-manifest.ts +0 -93
- package/src/i18n.test.ts +0 -58
- package/src/i18n.ts +0 -91
package/src/themes.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
import { isUnsafeKey } from "./safe-keys.js";
|
|
27
|
+
import { UnknownTokenName } from "./themes-errors.js";
|
|
27
28
|
|
|
28
29
|
// ────────────────────────────────────────────────────────────────────
|
|
29
30
|
// Primitives
|
|
@@ -117,6 +118,7 @@ export const themeTypographyComposite = z
|
|
|
117
118
|
z
|
|
118
119
|
.object({
|
|
119
120
|
fontFamily: z.string().min(1).max(200).optional(),
|
|
121
|
+
fontStyle: z.enum(["normal", "italic"]).optional(),
|
|
120
122
|
fontSize: dimensionValueString.optional(),
|
|
121
123
|
fontWeight: fontWeightValue.optional(),
|
|
122
124
|
lineHeight: z.union([z.number().positive(), dimensionValueString]).optional(),
|
|
@@ -728,11 +730,18 @@ function pluralise(category: string, n: number): string {
|
|
|
728
730
|
* use this same logic — extracted so the dotted-path merge lives in
|
|
729
731
|
* one place and v0.11.1's OKLCH auto-ramp can extend it without
|
|
730
732
|
* forking.
|
|
733
|
+
*
|
|
734
|
+
* issue #430 — `$description` (the token's ROLE: what it is for and
|
|
735
|
+
* where it must not be used) is preserved across value-only edits and
|
|
736
|
+
* overwritten only when `descriptions` supplies a new one. Before this,
|
|
737
|
+
* every routine value edit silently erased the role, so recording a
|
|
738
|
+
* role at all was pointless: the next `set_theme_tokens` wiped it.
|
|
731
739
|
*/
|
|
732
740
|
export function applyDtcgWrites(
|
|
733
741
|
current: ThemeDocument,
|
|
734
742
|
writes: Record<string, unknown>,
|
|
735
743
|
types: Record<string, string>,
|
|
744
|
+
descriptions: Record<string, string> = {},
|
|
736
745
|
): ThemeDocument {
|
|
737
746
|
const out: ThemeDocument = JSON.parse(JSON.stringify(current));
|
|
738
747
|
for (const [path, value] of Object.entries(writes)) {
|
|
@@ -759,9 +768,53 @@ export function applyDtcgWrites(
|
|
|
759
768
|
...(value as Record<string, unknown>),
|
|
760
769
|
};
|
|
761
770
|
}
|
|
771
|
+
// Role precedence: an explicitly supplied description wins; otherwise
|
|
772
|
+
// the leaf keeps the one it already carries. Only a token that never
|
|
773
|
+
// had a role ends up without one.
|
|
774
|
+
const existingDescription =
|
|
775
|
+
existing && typeof existing === "object"
|
|
776
|
+
? (existing as { $description?: unknown }).$description
|
|
777
|
+
: undefined;
|
|
778
|
+
const nextDescription =
|
|
779
|
+
descriptions[path] ??
|
|
780
|
+
(typeof existingDescription === "string" ? existingDescription : undefined);
|
|
781
|
+
const extensions =
|
|
782
|
+
existing && typeof existing === "object"
|
|
783
|
+
? { ...((existing as { $extensions?: Record<string, unknown> }).$extensions ?? {}) }
|
|
784
|
+
: {};
|
|
785
|
+
const oldValue = (existing as { $value?: { fontFamily?: string } } | undefined)?.$value;
|
|
786
|
+
if (
|
|
787
|
+
path.startsWith("typography.") &&
|
|
788
|
+
nextValue &&
|
|
789
|
+
typeof nextValue === "object" &&
|
|
790
|
+
(nextValue as { fontFamily?: string }).fontFamily !== oldValue?.fontFamily
|
|
791
|
+
)
|
|
792
|
+
delete extensions["caelo.font"];
|
|
762
793
|
setLeafAtPath(out, path, {
|
|
794
|
+
...(Object.keys(extensions).length ? { $extensions: extensions } : {}),
|
|
763
795
|
$value: nextValue,
|
|
764
796
|
...(inferredType ? { $type: inferredType } : {}),
|
|
797
|
+
...(nextDescription !== undefined ? { $description: nextDescription } : {}),
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// issue #430 — ROLE-ONLY paths: a description with no value in `writes`.
|
|
802
|
+
// The anchor-page design review annotates tokens whose values are already
|
|
803
|
+
// settled, so it must not have to restate them (a restated value is a
|
|
804
|
+
// chance to drift one). Patch the metadata, leave `$value` and `$type`
|
|
805
|
+
// exactly as they are.
|
|
806
|
+
for (const [path, description] of Object.entries(descriptions)) {
|
|
807
|
+
if (path in writes) continue;
|
|
808
|
+
const existing = readLeafAtPath(out, path);
|
|
809
|
+
if (!existing) {
|
|
810
|
+
// Annotating a token that does not exist is a mistake worth naming —
|
|
811
|
+
// silently minting a value-less leaf would produce a document the
|
|
812
|
+
// renderer can't emit (CLAUDE.md §2).
|
|
813
|
+
throw new UnknownTokenName(path, []);
|
|
814
|
+
}
|
|
815
|
+
setLeafAtPath(out, path, {
|
|
816
|
+
...(existing as Record<string, unknown>),
|
|
817
|
+
$description: description,
|
|
765
818
|
});
|
|
766
819
|
}
|
|
767
820
|
return out;
|
package/src/url.test.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { isDesignatedHomePage, isHomeSlug } from "./url.js";
|
|
5
|
+
|
|
6
|
+
describe("isHomeSlug", () => {
|
|
7
|
+
it("recognises the magic-slug sentinels regardless of surrounding slashes", () => {
|
|
8
|
+
for (const s of ["", "home", "index", "/", "/home/", "/index"]) {
|
|
9
|
+
expect(isHomeSlug(s)).toBe(true);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("rejects ordinary slugs", () => {
|
|
14
|
+
for (const s of ["about", "homepage", "index2", "blog/home"]) {
|
|
15
|
+
expect(isHomeSlug(s)).toBe(false);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("isDesignatedHomePage (0184 shared predicate)", () => {
|
|
21
|
+
const PAGE = "11111111-1111-1111-1111-111111111111";
|
|
22
|
+
const OTHER = "22222222-2222-2222-2222-222222222222";
|
|
23
|
+
|
|
24
|
+
it("explicit designation wins regardless of slug", () => {
|
|
25
|
+
expect(isDesignatedHomePage(PAGE, "welcome", PAGE)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("magic slug is the fallback when no designation exists", () => {
|
|
29
|
+
expect(isDesignatedHomePage(PAGE, "home", null)).toBe(true);
|
|
30
|
+
expect(isDesignatedHomePage(PAGE, "home", undefined)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("a designation pointing at ANOTHER page does not claim this one", () => {
|
|
34
|
+
expect(isDesignatedHomePage(PAGE, "welcome", OTHER)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("plain page, no designation: not home", () => {
|
|
38
|
+
expect(isDesignatedHomePage(PAGE, "about", null)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/src/url.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slug/URL primitives shared between the static generator, the preview
|
|
5
|
+
* op, and the admin UI (epic #380 #384 — successor of the deleted
|
|
6
|
+
* i18n.ts). URL shape beyond base + slug becomes a plugin contribution
|
|
7
|
+
* on the URL composition point (#390).
|
|
8
|
+
*
|
|
9
|
+
* isHomeSlug / isDesignatedHomePage — the home-decision predicate.
|
|
10
|
+
* trimSlashes / trimTrailingSlashes — linear-scan slug hygiene.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Strip leading and trailing `/` characters with a single linear scan.
|
|
15
|
+
*
|
|
16
|
+
* Replaces the regex `/^\/+|\/+$/g`, whose trailing-slash branch
|
|
17
|
+
* backtracks O(n²) on a long run of slashes that is not at end-of-string
|
|
18
|
+
* (CodeQL js/polynomial-redos). This char-index walk is unconditionally
|
|
19
|
+
* linear and produces byte-identical output for every input.
|
|
20
|
+
*/
|
|
21
|
+
export function trimSlashes(s: string): string {
|
|
22
|
+
let start = 0;
|
|
23
|
+
let end = s.length;
|
|
24
|
+
while (start < end && s.charCodeAt(start) === 47 /* '/' */) start += 1;
|
|
25
|
+
while (end > start && s.charCodeAt(end - 1) === 47 /* '/' */) end -= 1;
|
|
26
|
+
return s.slice(start, end);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Strip only trailing `/` characters with a single linear scan — the
|
|
31
|
+
* non-backtracking replacement for `/\/+$/` (same ReDoS class as above).
|
|
32
|
+
* Used for base URLs, which carry a scheme and must keep their leading
|
|
33
|
+
* characters.
|
|
34
|
+
*/
|
|
35
|
+
export function trimTrailingSlashes(s: string): string {
|
|
36
|
+
let end = s.length;
|
|
37
|
+
while (end > 0 && s.charCodeAt(end - 1) === 47 /* '/' */) end -= 1;
|
|
38
|
+
return s.slice(0, end);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The "magic slug" sentinel test: a page whose slug is empty, `home`,
|
|
43
|
+
* or `index` is the site root by convention even without an explicit
|
|
44
|
+
* `site_defaults.home_page_id` designation. Leading/trailing slashes
|
|
45
|
+
* are ignored so `/home/` reads the same as `home`.
|
|
46
|
+
*/
|
|
47
|
+
export function isHomeSlug(slug: string): boolean {
|
|
48
|
+
const s = trimSlashes(slug);
|
|
49
|
+
return s === "" || s === "home" || s === "index";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The single home-decision predicate every URL/output-path site must
|
|
54
|
+
* use so canonical, redirects, and the emitted file agree (0184
|
|
55
|
+
* explicit-homepage feature). A page is the site root when it IS the
|
|
56
|
+
* designated `site_defaults.home_page_id` OR carries a magic slug.
|
|
57
|
+
*
|
|
58
|
+
* @param pageId The page being resolved.
|
|
59
|
+
* @param slug That page's slug.
|
|
60
|
+
* @param designatedHomePageId `site_defaults.home_page_id`
|
|
61
|
+
* (null/undefined when none designated).
|
|
62
|
+
*/
|
|
63
|
+
export function isDesignatedHomePage(
|
|
64
|
+
pageId: string,
|
|
65
|
+
slug: string,
|
|
66
|
+
designatedHomePageId: string | null | undefined,
|
|
67
|
+
): boolean {
|
|
68
|
+
return (designatedHomePageId != null && pageId === designatedHomePageId) || isHomeSlug(slug);
|
|
69
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* issue #165 — the Design Manifest: a generated, versioned description
|
|
3
|
-
* of THIS site's design system (epic #149, growth-time convergence).
|
|
4
|
-
*
|
|
5
|
-
* The operator requirement is asymmetric: pages of the SAME site share
|
|
6
|
-
* one visual line, while different sites are free to look nothing
|
|
7
|
-
* alike. A global stylesheet solves the first at the cost of the
|
|
8
|
-
* second; the per-site manifest solves both — it captures what this
|
|
9
|
-
* site's tokens/patterns MEAN so the AI converges toward its own
|
|
10
|
-
* design, not a Caelo default.
|
|
11
|
-
*
|
|
12
|
-
* Written at Genesis materialisation (or curated later via chat);
|
|
13
|
-
* rendered as the `## Design system` system-prompt block.
|
|
14
|
-
*/
|
|
15
|
-
import { z } from "zod";
|
|
16
|
-
export declare const designManifestPattern: z.ZodObject<{
|
|
17
|
-
name: z.ZodString;
|
|
18
|
-
moduleType: z.ZodOptional<z.ZodString>;
|
|
19
|
-
spec: z.ZodString;
|
|
20
|
-
}, z.core.$strict>;
|
|
21
|
-
export declare const designManifestSchema: z.ZodObject<{
|
|
22
|
-
tokenRoles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
23
|
-
typography: z.ZodOptional<z.ZodString>;
|
|
24
|
-
rhythm: z.ZodOptional<z.ZodString>;
|
|
25
|
-
patterns: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
26
|
-
name: z.ZodString;
|
|
27
|
-
moduleType: z.ZodOptional<z.ZodString>;
|
|
28
|
-
spec: z.ZodString;
|
|
29
|
-
}, z.core.$strict>>>;
|
|
30
|
-
imagery: z.ZodOptional<z.ZodString>;
|
|
31
|
-
avoid: z.ZodOptional<z.ZodString>;
|
|
32
|
-
}, z.core.$strict>;
|
|
33
|
-
export type DesignManifest = z.infer<typeof designManifestSchema>;
|
|
34
|
-
/** Render the `## Design system` prompt block. Null when empty. */
|
|
35
|
-
export declare function formatDesignSystemBlock(manifest: DesignManifest | null): string | null;
|
|
36
|
-
//# sourceMappingURL=design-manifest.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"design-manifest.d.ts","sourceRoot":"","sources":["../src/design-manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,qBAAqB;;;;kBASvB,CAAC;AAEZ,eAAO,MAAM,oBAAoB;;;;;;;;;;;kBAYtB,CAAC;AACZ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,mEAAmE;AACnE,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CA8CtF"}
|
package/dist/design-manifest.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
-
/**
|
|
3
|
-
* issue #165 — the Design Manifest: a generated, versioned description
|
|
4
|
-
* of THIS site's design system (epic #149, growth-time convergence).
|
|
5
|
-
*
|
|
6
|
-
* The operator requirement is asymmetric: pages of the SAME site share
|
|
7
|
-
* one visual line, while different sites are free to look nothing
|
|
8
|
-
* alike. A global stylesheet solves the first at the cost of the
|
|
9
|
-
* second; the per-site manifest solves both — it captures what this
|
|
10
|
-
* site's tokens/patterns MEAN so the AI converges toward its own
|
|
11
|
-
* design, not a Caelo default.
|
|
12
|
-
*
|
|
13
|
-
* Written at Genesis materialisation (or curated later via chat);
|
|
14
|
-
* rendered as the `## Design system` system-prompt block.
|
|
15
|
-
*/
|
|
16
|
-
import { z } from "zod";
|
|
17
|
-
export const designManifestPattern = z
|
|
18
|
-
.object({
|
|
19
|
-
/** Pattern name the AI reasons in ("hero", "card grid", "cta band"). */
|
|
20
|
-
name: z.string().min(1).max(80),
|
|
21
|
-
/** Stable module `type` implementing it — reuse target (#159 place mode). */
|
|
22
|
-
moduleType: z.string().max(64).optional(),
|
|
23
|
-
/** One-line spec: what makes this pattern THIS site's version of it. */
|
|
24
|
-
spec: z.string().min(1).max(500),
|
|
25
|
-
})
|
|
26
|
-
.strict();
|
|
27
|
-
export const designManifestSchema = z
|
|
28
|
-
.object({
|
|
29
|
-
/** `--color-primary` → "CTAs and links only" — usage semantics per var. */
|
|
30
|
-
tokenRoles: z.record(z.string().min(3).max(64), z.string().min(1).max(300)).optional(),
|
|
31
|
-
/** Scale ratio, casing, measure rules. */
|
|
32
|
-
typography: z.string().min(1).max(1000).optional(),
|
|
33
|
-
/** Section padding, container width, grid gaps. */
|
|
34
|
-
rhythm: z.string().min(1).max(1000).optional(),
|
|
35
|
-
patterns: z.array(designManifestPattern).max(24).optional(),
|
|
36
|
-
imagery: z.string().min(1).max(500).optional(),
|
|
37
|
-
avoid: z.string().min(1).max(500).optional(),
|
|
38
|
-
})
|
|
39
|
-
.strict();
|
|
40
|
-
/** Render the `## Design system` prompt block. Null when empty. */
|
|
41
|
-
export function formatDesignSystemBlock(manifest) {
|
|
42
|
-
if (manifest === null)
|
|
43
|
-
return null;
|
|
44
|
-
const lines = [
|
|
45
|
-
"## Design system",
|
|
46
|
-
"",
|
|
47
|
-
"THIS site's own design language — derived from the chosen design. Every module you author or restyle conforms to it unless the operator explicitly asks to break the line. Reuse the pattern's module type (place mode) before minting lookalikes.",
|
|
48
|
-
"",
|
|
49
|
-
];
|
|
50
|
-
let hasContent = false;
|
|
51
|
-
const roles = manifest.tokenRoles ?? {};
|
|
52
|
-
const roleEntries = Object.entries(roles);
|
|
53
|
-
if (roleEntries.length > 0) {
|
|
54
|
-
hasContent = true;
|
|
55
|
-
lines.push("Token roles:");
|
|
56
|
-
for (const [token, role] of roleEntries.slice(0, 24))
|
|
57
|
-
lines.push(`- \`${token}\` — ${role}`);
|
|
58
|
-
lines.push("");
|
|
59
|
-
}
|
|
60
|
-
if (manifest.typography) {
|
|
61
|
-
hasContent = true;
|
|
62
|
-
lines.push(`Typography: ${manifest.typography}`, "");
|
|
63
|
-
}
|
|
64
|
-
if (manifest.rhythm) {
|
|
65
|
-
hasContent = true;
|
|
66
|
-
lines.push(`Rhythm: ${manifest.rhythm}`, "");
|
|
67
|
-
}
|
|
68
|
-
if (manifest.patterns && manifest.patterns.length > 0) {
|
|
69
|
-
hasContent = true;
|
|
70
|
-
lines.push("Patterns:");
|
|
71
|
-
for (const p of manifest.patterns) {
|
|
72
|
-
lines.push(`- **${p.name}**${p.moduleType ? ` (module type \`${p.moduleType}\`)` : ""} — ${p.spec}`);
|
|
73
|
-
}
|
|
74
|
-
lines.push("");
|
|
75
|
-
}
|
|
76
|
-
if (manifest.imagery) {
|
|
77
|
-
hasContent = true;
|
|
78
|
-
lines.push(`Imagery: ${manifest.imagery}`, "");
|
|
79
|
-
}
|
|
80
|
-
if (manifest.avoid) {
|
|
81
|
-
hasContent = true;
|
|
82
|
-
lines.push(`Never: ${manifest.avoid}`, "");
|
|
83
|
-
}
|
|
84
|
-
if (!hasContent)
|
|
85
|
-
return null;
|
|
86
|
-
while (lines[lines.length - 1] === "")
|
|
87
|
-
lines.pop();
|
|
88
|
-
return lines.join("\n");
|
|
89
|
-
}
|
|
90
|
-
//# sourceMappingURL=design-manifest.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"design-manifest.js","sourceRoot":"","sources":["../src/design-manifest.ts"],"names":[],"mappings":"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"}
|
package/dist/i18n.d.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Strip leading and trailing `/` characters with a single linear scan.
|
|
3
|
-
*
|
|
4
|
-
* Replaces the regex `/^\/+|\/+$/g`, whose trailing-slash branch
|
|
5
|
-
* backtracks O(n²) on a long run of slashes that is not at end-of-string
|
|
6
|
-
* (CodeQL js/polynomial-redos). This char-index walk is unconditionally
|
|
7
|
-
* linear and produces byte-identical output for every input.
|
|
8
|
-
*/
|
|
9
|
-
export declare function trimSlashes(s: string): string;
|
|
10
|
-
/**
|
|
11
|
-
* Strip only trailing `/` characters with a single linear scan — the
|
|
12
|
-
* non-backtracking replacement for `/\/+$/` (same ReDoS class as above).
|
|
13
|
-
* Used for base URLs, which carry a scheme and must keep their leading
|
|
14
|
-
* characters.
|
|
15
|
-
*/
|
|
16
|
-
export declare function trimTrailingSlashes(s: string): string;
|
|
17
|
-
/**
|
|
18
|
-
* The "magic slug" sentinel test: a page whose slug is empty, `home`,
|
|
19
|
-
* or `index` is the locale root by convention even without an explicit
|
|
20
|
-
* `locales.home_page_id` designation. Leading/trailing slashes are
|
|
21
|
-
* ignored so `/home/` reads the same as `home`.
|
|
22
|
-
*/
|
|
23
|
-
export declare function isHomeSlug(slug: string): boolean;
|
|
24
|
-
/**
|
|
25
|
-
* The single home-decision predicate every URL/output-path site must
|
|
26
|
-
* use so canonical, hreflang, redirects, and the emitted file agree
|
|
27
|
-
* (0184 explicit-homepage feature). A page is the locale root when it
|
|
28
|
-
* IS the locale's designated `home_page_id` OR carries a magic slug.
|
|
29
|
-
*
|
|
30
|
-
* @param pageId The page being resolved.
|
|
31
|
-
* @param slug That page's slug.
|
|
32
|
-
* @param localeHomePageId `locales.home_page_id` for the page's locale
|
|
33
|
-
* (null/undefined when no page is designated).
|
|
34
|
-
*/
|
|
35
|
-
export declare function pageIsLocaleHome(pageId: string, slug: string, localeHomePageId: string | null | undefined): boolean;
|
|
36
|
-
/**
|
|
37
|
-
* Canonical-JSON serializer + sha256 → hex. Stable across runs because
|
|
38
|
-
* keys are sorted. The output is used for `pages.content_hash` so that
|
|
39
|
-
* a Mode-2 translation can detect whether its source has changed.
|
|
40
|
-
*/
|
|
41
|
-
export declare function computeContentHash(value: unknown): Promise<string>;
|
|
42
|
-
//# sourceMappingURL=i18n.d.ts.map
|
package/dist/i18n.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAgBA;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAM7C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAIrD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGhD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAWxE"}
|
package/dist/i18n.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Slug/URL primitives shared between the static generator, the preview
|
|
4
|
-
* op, and the admin UI. Epic #380 Phase A (#383): locale URL shaping
|
|
5
|
-
* (resolveLocaleUrl, buildHreflangLinks) is deleted — URL shape beyond
|
|
6
|
-
* base + slug becomes a plugin contribution on the URL composition
|
|
7
|
-
* point (#390). What survives until the page-identity cut (#384):
|
|
8
|
-
*
|
|
9
|
-
* isHomeSlug / pageIsLocaleHome — the home-decision predicate.
|
|
10
|
-
* computeContentHash — drives `pages.content_hash` (removed in #384).
|
|
11
|
-
* trimSlashes / trimTrailingSlashes — linear-scan slug hygiene.
|
|
12
|
-
*/
|
|
13
|
-
const TEXT_ENCODER = new TextEncoder();
|
|
14
|
-
/**
|
|
15
|
-
* Strip leading and trailing `/` characters with a single linear scan.
|
|
16
|
-
*
|
|
17
|
-
* Replaces the regex `/^\/+|\/+$/g`, whose trailing-slash branch
|
|
18
|
-
* backtracks O(n²) on a long run of slashes that is not at end-of-string
|
|
19
|
-
* (CodeQL js/polynomial-redos). This char-index walk is unconditionally
|
|
20
|
-
* linear and produces byte-identical output for every input.
|
|
21
|
-
*/
|
|
22
|
-
export function trimSlashes(s) {
|
|
23
|
-
let start = 0;
|
|
24
|
-
let end = s.length;
|
|
25
|
-
while (start < end && s.charCodeAt(start) === 47 /* '/' */)
|
|
26
|
-
start += 1;
|
|
27
|
-
while (end > start && s.charCodeAt(end - 1) === 47 /* '/' */)
|
|
28
|
-
end -= 1;
|
|
29
|
-
return s.slice(start, end);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Strip only trailing `/` characters with a single linear scan — the
|
|
33
|
-
* non-backtracking replacement for `/\/+$/` (same ReDoS class as above).
|
|
34
|
-
* Used for base URLs, which carry a scheme and must keep their leading
|
|
35
|
-
* characters.
|
|
36
|
-
*/
|
|
37
|
-
export function trimTrailingSlashes(s) {
|
|
38
|
-
let end = s.length;
|
|
39
|
-
while (end > 0 && s.charCodeAt(end - 1) === 47 /* '/' */)
|
|
40
|
-
end -= 1;
|
|
41
|
-
return s.slice(0, end);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* The "magic slug" sentinel test: a page whose slug is empty, `home`,
|
|
45
|
-
* or `index` is the locale root by convention even without an explicit
|
|
46
|
-
* `locales.home_page_id` designation. Leading/trailing slashes are
|
|
47
|
-
* ignored so `/home/` reads the same as `home`.
|
|
48
|
-
*/
|
|
49
|
-
export function isHomeSlug(slug) {
|
|
50
|
-
const s = trimSlashes(slug);
|
|
51
|
-
return s === "" || s === "home" || s === "index";
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* The single home-decision predicate every URL/output-path site must
|
|
55
|
-
* use so canonical, hreflang, redirects, and the emitted file agree
|
|
56
|
-
* (0184 explicit-homepage feature). A page is the locale root when it
|
|
57
|
-
* IS the locale's designated `home_page_id` OR carries a magic slug.
|
|
58
|
-
*
|
|
59
|
-
* @param pageId The page being resolved.
|
|
60
|
-
* @param slug That page's slug.
|
|
61
|
-
* @param localeHomePageId `locales.home_page_id` for the page's locale
|
|
62
|
-
* (null/undefined when no page is designated).
|
|
63
|
-
*/
|
|
64
|
-
export function pageIsLocaleHome(pageId, slug, localeHomePageId) {
|
|
65
|
-
return (localeHomePageId != null && pageId === localeHomePageId) || isHomeSlug(slug);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Canonical-JSON serializer + sha256 → hex. Stable across runs because
|
|
69
|
-
* keys are sorted. The output is used for `pages.content_hash` so that
|
|
70
|
-
* a Mode-2 translation can detect whether its source has changed.
|
|
71
|
-
*/
|
|
72
|
-
export async function computeContentHash(value) {
|
|
73
|
-
const canonical = JSON.stringify(value, (_key, val) => {
|
|
74
|
-
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
75
|
-
const sorted = {};
|
|
76
|
-
for (const k of Object.keys(val).sort())
|
|
77
|
-
sorted[k] = val[k];
|
|
78
|
-
return sorted;
|
|
79
|
-
}
|
|
80
|
-
return val;
|
|
81
|
-
});
|
|
82
|
-
const digest = await crypto.subtle.digest("SHA-256", TEXT_ENCODER.encode(canonical));
|
|
83
|
-
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
84
|
-
}
|
|
85
|
-
//# sourceMappingURL=i18n.js.map
|
package/dist/i18n.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC;;;;;;;;;;GAUG;AAEH,MAAM,YAAY,GAAG,IAAI,WAAW,EAAE,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,CAAS;IACnC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC;IACnB,OAAO,KAAK,GAAG,GAAG,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,SAAS;QAAE,KAAK,IAAI,CAAC,CAAC;IACvE,OAAO,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS;QAAE,GAAG,IAAI,CAAC,CAAC;IACvE,OAAO,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,CAAS;IAC3C,IAAI,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC;IACnB,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS;QAAE,GAAG,IAAI,CAAC,CAAC;IACnE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACzB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO,CAAC;AACnD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAc,EACd,IAAY,EACZ,gBAA2C;IAE3C,OAAO,CAAC,gBAAgB,IAAI,IAAI,IAAI,MAAM,KAAK,gBAAgB,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;AACvF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,KAAc;IACrD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACpD,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1D,MAAM,MAAM,GAA4B,EAAE,CAAC;YAC3C,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE;gBAAE,MAAM,CAAC,CAAC,CAAC,GAAI,GAA+B,CAAC,CAAC,CAAC,CAAC;YACzF,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IACrF,OAAO,CAAC,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC1F,CAAC"}
|
package/src/design-manifest.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* issue #165 — the Design Manifest: a generated, versioned description
|
|
5
|
-
* of THIS site's design system (epic #149, growth-time convergence).
|
|
6
|
-
*
|
|
7
|
-
* The operator requirement is asymmetric: pages of the SAME site share
|
|
8
|
-
* one visual line, while different sites are free to look nothing
|
|
9
|
-
* alike. A global stylesheet solves the first at the cost of the
|
|
10
|
-
* second; the per-site manifest solves both — it captures what this
|
|
11
|
-
* site's tokens/patterns MEAN so the AI converges toward its own
|
|
12
|
-
* design, not a Caelo default.
|
|
13
|
-
*
|
|
14
|
-
* Written at Genesis materialisation (or curated later via chat);
|
|
15
|
-
* rendered as the `## Design system` system-prompt block.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { z } from "zod";
|
|
19
|
-
|
|
20
|
-
export const designManifestPattern = z
|
|
21
|
-
.object({
|
|
22
|
-
/** Pattern name the AI reasons in ("hero", "card grid", "cta band"). */
|
|
23
|
-
name: z.string().min(1).max(80),
|
|
24
|
-
/** Stable module `type` implementing it — reuse target (#159 place mode). */
|
|
25
|
-
moduleType: z.string().max(64).optional(),
|
|
26
|
-
/** One-line spec: what makes this pattern THIS site's version of it. */
|
|
27
|
-
spec: z.string().min(1).max(500),
|
|
28
|
-
})
|
|
29
|
-
.strict();
|
|
30
|
-
|
|
31
|
-
export const designManifestSchema = z
|
|
32
|
-
.object({
|
|
33
|
-
/** `--color-primary` → "CTAs and links only" — usage semantics per var. */
|
|
34
|
-
tokenRoles: z.record(z.string().min(3).max(64), z.string().min(1).max(300)).optional(),
|
|
35
|
-
/** Scale ratio, casing, measure rules. */
|
|
36
|
-
typography: z.string().min(1).max(1000).optional(),
|
|
37
|
-
/** Section padding, container width, grid gaps. */
|
|
38
|
-
rhythm: z.string().min(1).max(1000).optional(),
|
|
39
|
-
patterns: z.array(designManifestPattern).max(24).optional(),
|
|
40
|
-
imagery: z.string().min(1).max(500).optional(),
|
|
41
|
-
avoid: z.string().min(1).max(500).optional(),
|
|
42
|
-
})
|
|
43
|
-
.strict();
|
|
44
|
-
export type DesignManifest = z.infer<typeof designManifestSchema>;
|
|
45
|
-
|
|
46
|
-
/** Render the `## Design system` prompt block. Null when empty. */
|
|
47
|
-
export function formatDesignSystemBlock(manifest: DesignManifest | null): string | null {
|
|
48
|
-
if (manifest === null) return null;
|
|
49
|
-
const lines: string[] = [
|
|
50
|
-
"## Design system",
|
|
51
|
-
"",
|
|
52
|
-
"THIS site's own design language — derived from the chosen design. Every module you author or restyle conforms to it unless the operator explicitly asks to break the line. Reuse the pattern's module type (place mode) before minting lookalikes.",
|
|
53
|
-
"",
|
|
54
|
-
];
|
|
55
|
-
let hasContent = false;
|
|
56
|
-
const roles = manifest.tokenRoles ?? {};
|
|
57
|
-
const roleEntries = Object.entries(roles);
|
|
58
|
-
if (roleEntries.length > 0) {
|
|
59
|
-
hasContent = true;
|
|
60
|
-
lines.push("Token roles:");
|
|
61
|
-
for (const [token, role] of roleEntries.slice(0, 24)) lines.push(`- \`${token}\` — ${role}`);
|
|
62
|
-
lines.push("");
|
|
63
|
-
}
|
|
64
|
-
if (manifest.typography) {
|
|
65
|
-
hasContent = true;
|
|
66
|
-
lines.push(`Typography: ${manifest.typography}`, "");
|
|
67
|
-
}
|
|
68
|
-
if (manifest.rhythm) {
|
|
69
|
-
hasContent = true;
|
|
70
|
-
lines.push(`Rhythm: ${manifest.rhythm}`, "");
|
|
71
|
-
}
|
|
72
|
-
if (manifest.patterns && manifest.patterns.length > 0) {
|
|
73
|
-
hasContent = true;
|
|
74
|
-
lines.push("Patterns:");
|
|
75
|
-
for (const p of manifest.patterns) {
|
|
76
|
-
lines.push(
|
|
77
|
-
`- **${p.name}**${p.moduleType ? ` (module type \`${p.moduleType}\`)` : ""} — ${p.spec}`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
lines.push("");
|
|
81
|
-
}
|
|
82
|
-
if (manifest.imagery) {
|
|
83
|
-
hasContent = true;
|
|
84
|
-
lines.push(`Imagery: ${manifest.imagery}`, "");
|
|
85
|
-
}
|
|
86
|
-
if (manifest.avoid) {
|
|
87
|
-
hasContent = true;
|
|
88
|
-
lines.push(`Never: ${manifest.avoid}`, "");
|
|
89
|
-
}
|
|
90
|
-
if (!hasContent) return null;
|
|
91
|
-
while (lines[lines.length - 1] === "") lines.pop();
|
|
92
|
-
return lines.join("\n");
|
|
93
|
-
}
|
package/src/i18n.test.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from "bun:test";
|
|
4
|
-
import { computeContentHash, isHomeSlug, pageIsLocaleHome } from "./i18n.js";
|
|
5
|
-
|
|
6
|
-
describe("isHomeSlug", () => {
|
|
7
|
-
it("recognises the magic-slug sentinels regardless of surrounding slashes", () => {
|
|
8
|
-
for (const s of ["", "home", "index", "/", "/home/", "/index"]) {
|
|
9
|
-
expect(isHomeSlug(s)).toBe(true);
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("treats any other slug as non-home", () => {
|
|
14
|
-
for (const s of ["about", "en", "blog/post", "homepage"]) {
|
|
15
|
-
expect(isHomeSlug(s)).toBe(false);
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe("pageIsLocaleHome (0184 shared predicate)", () => {
|
|
21
|
-
const PAGE = "11111111-1111-4111-8111-111111111111";
|
|
22
|
-
const OTHER = "22222222-2222-4222-8222-222222222222";
|
|
23
|
-
|
|
24
|
-
it("is true when the page IS the locale's designated home_page_id", () => {
|
|
25
|
-
expect(pageIsLocaleHome(PAGE, "en", PAGE)).toBe(true);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("is true for a magic slug even with no designation", () => {
|
|
29
|
-
expect(pageIsLocaleHome(PAGE, "home", null)).toBe(true);
|
|
30
|
-
expect(pageIsLocaleHome(PAGE, "index", undefined)).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("is false for a non-magic slug that isn't the designated page", () => {
|
|
34
|
-
expect(pageIsLocaleHome(PAGE, "en", OTHER)).toBe(false);
|
|
35
|
-
expect(pageIsLocaleHome(PAGE, "about", null)).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("computeContentHash", () => {
|
|
40
|
-
it("is stable across runs for the same input", async () => {
|
|
41
|
-
const a = await computeContentHash({ x: 1, y: [1, 2] });
|
|
42
|
-
const b = await computeContentHash({ x: 1, y: [1, 2] });
|
|
43
|
-
expect(a).toBe(b);
|
|
44
|
-
expect(a).toMatch(/^[0-9a-f]{64}$/);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("is order-independent for object keys", async () => {
|
|
48
|
-
const a = await computeContentHash({ x: 1, y: 2 });
|
|
49
|
-
const b = await computeContentHash({ y: 2, x: 1 });
|
|
50
|
-
expect(a).toBe(b);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("is order-dependent for arrays", async () => {
|
|
54
|
-
const a = await computeContentHash({ x: [1, 2] });
|
|
55
|
-
const b = await computeContentHash({ x: [2, 1] });
|
|
56
|
-
expect(a).not.toBe(b);
|
|
57
|
-
});
|
|
58
|
-
});
|