@caelo-cms/shared 0.10.55 → 0.10.58
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 +19 -0
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +19 -9
- package/dist/ai-tools.js.map +1 -1
- package/dist/auto-update.d.ts +15 -4
- package/dist/auto-update.d.ts.map +1 -1
- package/dist/auto-update.js +19 -14
- package/dist/auto-update.js.map +1 -1
- package/dist/google-oauth.d.ts +22 -0
- package/dist/google-oauth.d.ts.map +1 -0
- package/dist/google-oauth.js +25 -0
- package/dist/google-oauth.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/link-canonicalise.d.ts +109 -0
- package/dist/link-canonicalise.d.ts.map +1 -0
- package/dist/link-canonicalise.js +471 -0
- package/dist/link-canonicalise.js.map +1 -0
- package/dist/link-refs.d.ts +63 -8
- package/dist/link-refs.d.ts.map +1 -1
- package/dist/link-refs.js +80 -14
- package/dist/link-refs.js.map +1 -1
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +15 -0
- package/dist/media.js.map +1 -1
- package/dist/nested-module-render.d.ts +17 -0
- package/dist/nested-module-render.d.ts.map +1 -1
- package/dist/nested-module-render.js +45 -6
- package/dist/nested-module-render.js.map +1 -1
- package/dist/platform-updates.d.ts +20 -3
- package/dist/platform-updates.d.ts.map +1 -1
- package/dist/platform-updates.js +24 -1
- package/dist/platform-updates.js.map +1 -1
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +12 -2
- package/dist/preview-compose.js.map +1 -1
- package/dist/proposal-status.d.ts +1 -1
- package/dist/redirect-emit-gcp.d.ts +11 -1
- package/dist/redirect-emit-gcp.d.ts.map +1 -1
- package/dist/redirect-emit-gcp.js +16 -2
- package/dist/redirect-emit-gcp.js.map +1 -1
- package/dist/release-manifest.d.ts +14 -4
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +19 -3
- package/dist/release-manifest.js.map +1 -1
- package/dist/theme-normalize.d.ts.map +1 -1
- package/dist/theme-normalize.js +17 -2
- package/dist/theme-normalize.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/ai-tools.ts +22 -11
- package/src/auto-update.test.ts +47 -8
- package/src/auto-update.ts +29 -14
- package/src/google-oauth.ts +27 -0
- package/src/index.ts +2 -0
- package/src/link-canonicalise.test.ts +387 -0
- package/src/link-canonicalise.ts +598 -0
- package/src/link-refs.test.ts +206 -0
- package/src/link-refs.ts +119 -21
- package/src/media.ts +16 -0
- package/src/nested-module-render.ts +59 -4
- package/src/platform-updates.ts +26 -1
- package/src/preview-compose.ts +11 -2
- package/src/redirect-emit-gcp.ts +16 -2
- package/src/redirect-emit.test.ts +6 -4
- package/src/release-manifest.ts +39 -7
- package/src/theme-normalize-typography-subfields.test.ts +54 -0
- package/src/theme-normalize.ts +19 -2
- package/src/version.ts +1 -1
|
@@ -153,20 +153,22 @@ describe("GCP URL-map rules", () => {
|
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
it("refuses a plan over the 1,000-predicate quota with the counts, before any change", () => {
|
|
156
|
+
// The base rule routes /api/* and the remote-MCP discovery paths (#806).
|
|
157
|
+
expect(GCP_PUBLIC_BASE_PATHS).toHaveLength(3);
|
|
156
158
|
const base = [{ paths: [...GCP_PUBLIC_BASE_PATHS] }];
|
|
157
159
|
const rows = (n: number) => Array.from({ length: n }, (_, i) => row(`/p${i}`, "/x"));
|
|
158
|
-
expect(() => assertGcpRedirectBudget(planOf(rows(
|
|
160
|
+
expect(() => assertGcpRedirectBudget(planOf(rows(498)), base)).not.toThrow();
|
|
159
161
|
try {
|
|
160
|
-
assertGcpRedirectBudget(planOf(rows(
|
|
162
|
+
assertGcpRedirectBudget(planOf(rows(499)), base);
|
|
161
163
|
throw new Error("expected GcpUrlMapBudgetError");
|
|
162
164
|
} catch (e) {
|
|
163
165
|
expect(e).toBeInstanceOf(GcpUrlMapBudgetError);
|
|
164
166
|
expect((e as GcpUrlMapBudgetError).predicatesNeeded).toBe(1001);
|
|
165
|
-
expect((e as Error).message).toContain("
|
|
167
|
+
expect((e as Error).message).toContain("499 redirect(s) need 1001 paths");
|
|
166
168
|
expect((e as Error).message).toContain("bulk_delete_redirects");
|
|
167
169
|
}
|
|
168
170
|
// File-like paths cost one predicate each.
|
|
169
|
-
const files = Array.from({ length:
|
|
171
|
+
const files = Array.from({ length: 997 }, (_, i) => row(`/p${i}.html`, "/x"));
|
|
170
172
|
expect(() => assertGcpRedirectBudget(planOf(files), base)).not.toThrow();
|
|
171
173
|
});
|
|
172
174
|
|
package/src/release-manifest.ts
CHANGED
|
@@ -71,12 +71,23 @@ export const SIGSTORE_BUNDLE_MEDIA_TYPE = "application/vnd.dev.sigstore.bundle.v
|
|
|
71
71
|
/** Why fetching the artifact failed. */
|
|
72
72
|
export type ReleaseArtifactErrorCode = "registry" | "artifact-shape" | "blob-digest";
|
|
73
73
|
|
|
74
|
+
/** Why {@link fetchReleaseArtifact} failed. */
|
|
75
|
+
export interface ReleaseArtifactError {
|
|
76
|
+
readonly code: ReleaseArtifactErrorCode;
|
|
77
|
+
readonly message: string;
|
|
78
|
+
/**
|
|
79
|
+
* Set on `registry` errors a retry may cure: the registry could not be
|
|
80
|
+
* reached, timed out, rate-limited (429) or failed (5xx). Absent when the
|
|
81
|
+
* registry answered that the manifest, tag or blob does not exist (404 and
|
|
82
|
+
* other 4xx) — retrying that changes nothing, so callers that would
|
|
83
|
+
* otherwise "try again later" must treat it as a verdict.
|
|
84
|
+
*/
|
|
85
|
+
readonly transient?: true;
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
export type ReleaseArtifactResult =
|
|
75
89
|
| { readonly ok: true; readonly manifestBytes: Uint8Array; readonly bundleBytes: Uint8Array }
|
|
76
|
-
| {
|
|
77
|
-
readonly ok: false;
|
|
78
|
-
readonly error: { readonly code: ReleaseArtifactErrorCode; readonly message: string };
|
|
79
|
-
};
|
|
90
|
+
| { readonly ok: false; readonly error: ReleaseArtifactError };
|
|
80
91
|
|
|
81
92
|
/** Where and how {@link fetchReleaseArtifact} reaches the registry. */
|
|
82
93
|
export interface ReleaseArtifactDeps {
|
|
@@ -101,6 +112,22 @@ const fail = (code: ReleaseArtifactErrorCode, message: string): ReleaseArtifactR
|
|
|
101
112
|
error: { code, message },
|
|
102
113
|
});
|
|
103
114
|
|
|
115
|
+
/** A `registry` failure a retry may cure (see {@link ReleaseArtifactError.transient}). */
|
|
116
|
+
const failTransient = (message: string): ReleaseArtifactResult => ({
|
|
117
|
+
ok: false,
|
|
118
|
+
error: { code: "registry", message, transient: true },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** Whether an HTTP status says "try again" rather than "it does not exist". */
|
|
122
|
+
function isTransientStatus(status: number): boolean {
|
|
123
|
+
return status >= 500 || status === 429 || status === 408;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A non-OK registry response as a failure, transient or not by its status. */
|
|
127
|
+
function failStatus(status: number, message: string): ReleaseArtifactResult {
|
|
128
|
+
return isTransientStatus(status) ? failTransient(message) : fail("registry", message);
|
|
129
|
+
}
|
|
130
|
+
|
|
104
131
|
/**
|
|
105
132
|
* Fetch the release manifest artifact of `version` and both its layers,
|
|
106
133
|
* each checked against its content digest. Verifies NO signature: callers
|
|
@@ -120,6 +147,11 @@ export async function fetchReleaseArtifact(
|
|
|
120
147
|
headers: { Accept: "application/vnd.oci.image.manifest.v1+json" },
|
|
121
148
|
});
|
|
122
149
|
if (!res.ok) {
|
|
150
|
+
if (isTransientStatus(res.status)) {
|
|
151
|
+
return failTransient(
|
|
152
|
+
`the registry at ${base} could not serve the release manifest of v${version} (HTTP ${res.status}); try again later.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
123
155
|
return fail(
|
|
124
156
|
"registry",
|
|
125
157
|
`no release manifest for v${version} at ${base} (HTTP ${res.status}). Releases before the signed manifest (epic #696) have none; otherwise check the version exists.`,
|
|
@@ -131,7 +163,7 @@ export async function fetchReleaseArtifact(
|
|
|
131
163
|
}
|
|
132
164
|
artifact = parsed.data;
|
|
133
165
|
} catch (e) {
|
|
134
|
-
return
|
|
166
|
+
return failTransient(`registry unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
135
167
|
}
|
|
136
168
|
|
|
137
169
|
if (artifact.artifactType !== RELEASE_MANIFEST_ARTIFACT_TYPE) {
|
|
@@ -153,7 +185,7 @@ export async function fetchReleaseArtifact(
|
|
|
153
185
|
|
|
154
186
|
const blob = async (layerDigest: string): Promise<Uint8Array | ReleaseArtifactResult> => {
|
|
155
187
|
const res = await deps.fetch(`${base}/blobs/${layerDigest}`);
|
|
156
|
-
if (!res.ok) return
|
|
188
|
+
if (!res.ok) return failStatus(res.status, `blob ${layerDigest}: HTTP ${res.status}`);
|
|
157
189
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
158
190
|
const actual = await sha256Digest(bytes);
|
|
159
191
|
if (actual !== layerDigest) {
|
|
@@ -171,7 +203,7 @@ export async function fetchReleaseArtifact(
|
|
|
171
203
|
if (!(bundleBytes instanceof Uint8Array)) return bundleBytes;
|
|
172
204
|
return { ok: true, manifestBytes, bundleBytes };
|
|
173
205
|
} catch (e) {
|
|
174
|
-
return
|
|
206
|
+
return failTransient(`registry unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
175
207
|
}
|
|
176
208
|
}
|
|
177
209
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Regression: saving the Typography tab posts every sub-field of a named
|
|
4
|
+
* style as its own `typography.<style>.<sub>` key, with lineHeight /
|
|
5
|
+
* fontWeight coerced to numbers. Numbers used to skip the composite wrap
|
|
6
|
+
* (so `$value` became a bare `1.6`) and sibling sub-fields overwrote each
|
|
7
|
+
* other on the shared composite path.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import { normalizeTokens } from "./theme-normalize.js";
|
|
11
|
+
import { applyDtcgWrites, themeDocument } from "./themes.js";
|
|
12
|
+
|
|
13
|
+
describe("normalizeTokens typography sub-fields", () => {
|
|
14
|
+
it("wraps numeric sub-field values into the composite", () => {
|
|
15
|
+
const r = normalizeTokens({ "typography.body.lineHeight": 1.6 });
|
|
16
|
+
expect(r.set["typography.body"]).toEqual({ lineHeight: 1.6 });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("merges several sub-fields of one style into one composite", () => {
|
|
20
|
+
const r = normalizeTokens({
|
|
21
|
+
fontHeading: "Inter, sans-serif",
|
|
22
|
+
"typography.heading.fontSize": "3rem",
|
|
23
|
+
"typography.heading.fontWeight": 650,
|
|
24
|
+
"typography.heading.lineHeight": 1.08,
|
|
25
|
+
});
|
|
26
|
+
expect(r.set["typography.heading"]).toEqual({
|
|
27
|
+
fontFamily: "Inter, sans-serif",
|
|
28
|
+
fontSize: "3rem",
|
|
29
|
+
fontWeight: 650,
|
|
30
|
+
lineHeight: 1.08,
|
|
31
|
+
});
|
|
32
|
+
expect(r.canonicalPaths).toEqual(["typography.heading"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("produces a valid document when applied over an existing theme", () => {
|
|
36
|
+
const current = {
|
|
37
|
+
typography: {
|
|
38
|
+
body: { $type: "typography", $value: { fontFamily: "Inter", fontSize: "1rem" } },
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const r = normalizeTokens({
|
|
42
|
+
"typography.body.fontWeight": 400,
|
|
43
|
+
"typography.body.lineHeight": 1.6,
|
|
44
|
+
});
|
|
45
|
+
const next = applyDtcgWrites(current as never, r.set, r.types);
|
|
46
|
+
expect(themeDocument.safeParse(next).success).toBe(true);
|
|
47
|
+
expect((next.typography as Record<string, { $value: unknown }>).body?.$value).toEqual({
|
|
48
|
+
fontFamily: "Inter",
|
|
49
|
+
fontSize: "1rem",
|
|
50
|
+
fontWeight: 400,
|
|
51
|
+
lineHeight: 1.6,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/theme-normalize.ts
CHANGED
|
@@ -284,8 +284,14 @@ export function normalizeTokens(input: Record<string, unknown>): NormalizeResult
|
|
|
284
284
|
// resulting document validates. applyDtcgWrites merges this into
|
|
285
285
|
// any existing typography composite at the path so partial updates
|
|
286
286
|
// don't erase sibling sub-fields.
|
|
287
|
+
// Numbers wrap too: the admin form coerces `.lineHeight` / `.fontWeight`
|
|
288
|
+
// to numbers, and an unwrapped `1.6` would replace the whole composite.
|
|
287
289
|
let storedValue: unknown = rawValue;
|
|
288
|
-
if (
|
|
290
|
+
if (
|
|
291
|
+
resolved.compositeWrap &&
|
|
292
|
+
(typeof rawValue === "number" ||
|
|
293
|
+
(typeof rawValue === "string" && !ALIAS_REGEX.test(rawValue)))
|
|
294
|
+
) {
|
|
289
295
|
storedValue = { [resolved.compositeWrap]: rawValue };
|
|
290
296
|
} else if (
|
|
291
297
|
resolved.inferredType === "gradient" &&
|
|
@@ -303,10 +309,17 @@ export function normalizeTokens(input: Record<string, unknown>): NormalizeResult
|
|
|
303
309
|
// rather than us silently inventing a gradient (CLAUDE.md §2).
|
|
304
310
|
storedValue = gradientObjectToCss(rawValue as Record<string, unknown>) ?? rawValue;
|
|
305
311
|
}
|
|
312
|
+
// Several sub-field inputs (`typography.body.fontSize` +
|
|
313
|
+
// `typography.body.lineHeight`) collapse onto the same composite
|
|
314
|
+
// path; merge them so the last one doesn't silently drop the others.
|
|
315
|
+
const prior = set[resolved.path];
|
|
316
|
+
if (resolved.compositeWrap && isPlainObject(prior) && isPlainObject(storedValue)) {
|
|
317
|
+
storedValue = { ...prior, ...storedValue };
|
|
318
|
+
}
|
|
306
319
|
set[resolved.path] = storedValue;
|
|
307
320
|
types[resolved.path] = resolved.inferredType;
|
|
308
321
|
if (envelopeDescription !== undefined) descriptions[resolved.path] = envelopeDescription;
|
|
309
|
-
paths.push(resolved.path);
|
|
322
|
+
if (!paths.includes(resolved.path)) paths.push(resolved.path);
|
|
310
323
|
}
|
|
311
324
|
|
|
312
325
|
return { set, types, descriptions, canonicalPaths: paths };
|
|
@@ -721,3 +734,7 @@ function similarity(a: string, b: string): number {
|
|
|
721
734
|
function tokenise(s: string): string[] {
|
|
722
735
|
return s.split(/[.\-_]/).filter((p) => p.length > 0);
|
|
723
736
|
}
|
|
737
|
+
|
|
738
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
739
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
740
|
+
}
|