uhuu-components 0.4.6 → 0.4.7
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/cli.js +548 -0
- package/index.d.ts +49 -62
- package/package.json +6 -2
- package/uhuu-components.es.js +1950 -1896
- package/uhuu-components.es.js.map +1 -1
- package/uhuu-components.umd.cjs +10 -11
- package/uhuu-components.umd.cjs.map +1 -1
package/cli.js
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/uhuu-components.js
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
// src/cli/brand-command.js
|
|
7
|
+
import { Buffer } from "node:buffer";
|
|
8
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
// src/uhuu/utility/json.js
|
|
12
|
+
var isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
var textOf = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
14
|
+
|
|
15
|
+
// src/uhuu/brand-kit/brand-kit-assets.js
|
|
16
|
+
var ABSOLUTE = /^https?:\/\//i;
|
|
17
|
+
function brandKitBaseUrl(brandKit, sourceUrl) {
|
|
18
|
+
const own = isPlainObject(brandKit) ? textOf(brandKit.baseUrl) : void 0;
|
|
19
|
+
if (own) return own.replace(/\/+$/, "");
|
|
20
|
+
const source = textOf(sourceUrl);
|
|
21
|
+
if (!source || !ABSOLUTE.test(source)) return void 0;
|
|
22
|
+
try {
|
|
23
|
+
return new URL(".", source).toString().replace(/\/+$/, "");
|
|
24
|
+
} catch {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function resolveBrandKitUrl(value, base) {
|
|
29
|
+
const url = textOf(value);
|
|
30
|
+
if (!url || /\s/.test(url)) return void 0;
|
|
31
|
+
if (ABSOLUTE.test(url) || /^data:/i.test(url)) return url;
|
|
32
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith("//")) return void 0;
|
|
33
|
+
if (!base) return url;
|
|
34
|
+
try {
|
|
35
|
+
return new URL(url, `${base}/`).toString();
|
|
36
|
+
} catch {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
var LOGO_KIND = { primary: "primary", logo: "primary", lockup: "primary", mark: "mark", icon: "mark", symbol: "mark", wordmark: "wordmark" };
|
|
41
|
+
var fileOf = (files) => isPlainObject(files) ? textOf(files.svg) : void 0;
|
|
42
|
+
function brandKitLogo(brandKit, options = {}) {
|
|
43
|
+
if (!isPlainObject(brandKit)) return null;
|
|
44
|
+
const kind = LOGO_KIND[String(options.kind ?? "primary").toLowerCase()] ?? "primary";
|
|
45
|
+
const background = options.background === "dark" ? "dark" : "light";
|
|
46
|
+
const base = brandKitBaseUrl(brandKit, options.sourceUrl);
|
|
47
|
+
const alt = textOf(brandKit.name);
|
|
48
|
+
const found = logoFromSystem(brandKit, kind, background, base, alt, options);
|
|
49
|
+
if (found || kind !== "wordmark") return found;
|
|
50
|
+
return brandKitLogo(brandKit, { ...options, kind: "primary" });
|
|
51
|
+
}
|
|
52
|
+
function pickByLocale(variants, locale) {
|
|
53
|
+
const unlocalized = variants.filter((variant) => !textOf(variant.locale));
|
|
54
|
+
const wanted = textOf(locale)?.toLowerCase();
|
|
55
|
+
if (!wanted) return unlocalized[0] ?? variants[0];
|
|
56
|
+
const language = wanted.split("-")[0];
|
|
57
|
+
return variants.find((variant) => textOf(variant.locale)?.toLowerCase() === wanted) ?? variants.find((variant) => textOf(variant.locale)?.toLowerCase().split("-")[0] === language) ?? unlocalized[0] ?? variants[0];
|
|
58
|
+
}
|
|
59
|
+
function logoFromSystem(brandKit, kind, background, base, alt, options = {}) {
|
|
60
|
+
const variants = Array.isArray(brandKit.logoSystem?.variants) ? brandKit.logoSystem.variants.filter(isPlainObject) : [];
|
|
61
|
+
const ofKind = variants.filter((variant2) => variant2.kind === kind && fileOf(variant2.files));
|
|
62
|
+
const ordered = [
|
|
63
|
+
...ofKind.filter((item) => item.background === background),
|
|
64
|
+
...ofKind.filter((item) => item.background === "any"),
|
|
65
|
+
...ofKind.filter((item) => item.background !== background && item.background !== "any")
|
|
66
|
+
];
|
|
67
|
+
const wantedName = textOf(options.name)?.toLowerCase();
|
|
68
|
+
const named = wantedName ? ordered.filter((item) => textOf(item.id)?.toLowerCase() === wantedName || textOf(item.name)?.toLowerCase() === wantedName) : [];
|
|
69
|
+
const candidates = named.length ? named : ordered;
|
|
70
|
+
const fitting = candidates.filter((item) => item.background === background || item.background === "any");
|
|
71
|
+
const variant = pickByLocale(fitting.length ? fitting : candidates, options.locale);
|
|
72
|
+
if (!variant) return null;
|
|
73
|
+
const src = resolveBrandKitUrl(fileOf(variant.files), base);
|
|
74
|
+
if (!src) return null;
|
|
75
|
+
return {
|
|
76
|
+
src,
|
|
77
|
+
alt: textOf(variant.name) ?? alt,
|
|
78
|
+
kind,
|
|
79
|
+
background: variant.background,
|
|
80
|
+
...textOf(variant.locale) ? { locale: variant.locale } : {}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/uhuu/brand-kit/brand-kit-css-vars.js
|
|
85
|
+
var BRAND_KIT_RUNTIME_VERSION = 3;
|
|
86
|
+
var ROLE_NAMES = [
|
|
87
|
+
"background",
|
|
88
|
+
"foreground",
|
|
89
|
+
"card",
|
|
90
|
+
"card-foreground",
|
|
91
|
+
"popover",
|
|
92
|
+
"popover-foreground",
|
|
93
|
+
"primary",
|
|
94
|
+
"primary-foreground",
|
|
95
|
+
"secondary",
|
|
96
|
+
"secondary-foreground",
|
|
97
|
+
"muted",
|
|
98
|
+
"muted-foreground",
|
|
99
|
+
"accent",
|
|
100
|
+
"accent-foreground",
|
|
101
|
+
"destructive",
|
|
102
|
+
"destructive-foreground",
|
|
103
|
+
"border",
|
|
104
|
+
"input",
|
|
105
|
+
"ring",
|
|
106
|
+
"chart-1",
|
|
107
|
+
"chart-2",
|
|
108
|
+
"chart-3",
|
|
109
|
+
"chart-4",
|
|
110
|
+
"chart-5",
|
|
111
|
+
"sidebar",
|
|
112
|
+
"sidebar-foreground",
|
|
113
|
+
"sidebar-primary",
|
|
114
|
+
"sidebar-primary-foreground",
|
|
115
|
+
"sidebar-accent",
|
|
116
|
+
"sidebar-accent-foreground",
|
|
117
|
+
"sidebar-border",
|
|
118
|
+
"sidebar-ring"
|
|
119
|
+
];
|
|
120
|
+
var RUNTIME_ROLE_NAME = new RegExp(`^--(?:color-)?(?:${ROLE_NAMES.join("|")})$`);
|
|
121
|
+
function brandKitRuntime(brandKit) {
|
|
122
|
+
const runtime = isPlainObject(brandKit) ? brandKit.runtime : void 0;
|
|
123
|
+
return isPlainObject(runtime) && runtime.version === BRAND_KIT_RUNTIME_VERSION && isPlainObject(runtime.light) ? runtime : void 0;
|
|
124
|
+
}
|
|
125
|
+
var OK_COMPONENT = "(?:[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:%|deg)?|none)";
|
|
126
|
+
var OKLAB = new RegExp(`^ok(?:lch|lab)\\(\\s*${OK_COMPONENT}\\s+${OK_COMPONENT}\\s+${OK_COMPONENT}(?:\\s*\\/\\s*${OK_COMPONENT})?\\s*\\)$`, "i");
|
|
127
|
+
var NUMBER = "[+-]?(?:\\d+\\.?\\d*|\\.\\d+)";
|
|
128
|
+
var HSL_TRIPLET = new RegExp(
|
|
129
|
+
`^(${NUMBER})(deg|turn|rad|grad)?\\s+(${NUMBER})%\\s+(${NUMBER})%(?:\\s*\\/\\s*(${NUMBER})(%)?)?$`,
|
|
130
|
+
"i"
|
|
131
|
+
);
|
|
132
|
+
var HSL_FUNCTION = new RegExp(
|
|
133
|
+
`^hsla?\\(\\s*(${NUMBER})(deg|turn|rad|grad)?\\s*(?:,\\s*|\\s+)(${NUMBER})%\\s*(?:,\\s*|\\s+)(${NUMBER})%\\s*(?:(?:,|\\/)\\s*(${NUMBER})(%)?\\s*)?\\)$`,
|
|
134
|
+
"i"
|
|
135
|
+
);
|
|
136
|
+
var HUE_UNIT = { deg: 1, turn: 360, rad: 180 / Math.PI, grad: 0.9 };
|
|
137
|
+
|
|
138
|
+
// src/uhuu/brand-kit/brand-kit-preview.js
|
|
139
|
+
var VERSION = /^v[1-9]\d*$/;
|
|
140
|
+
var VERSION_IN_PATH = /\/versions\/(v[1-9]\d*)\//;
|
|
141
|
+
var VERSION_DOCUMENT = /\/versions\/v[1-9]\d*\/brandkit\.json$/;
|
|
142
|
+
var DOCUMENT_VERSION = /^3(\.\d+){0,2}$/;
|
|
143
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
144
|
+
var BRANDKIT_TRUSTED_ORIGINS = Object.freeze([
|
|
145
|
+
"https://uhuu-brandkit.s3.eu-west-1.amazonaws.com",
|
|
146
|
+
"https://brandkit.uhuu.io",
|
|
147
|
+
"https://uhuu-layout-kit.s3.eu-west-1.amazonaws.com"
|
|
148
|
+
]);
|
|
149
|
+
function isTrustedOrigin(url, extraOrigins = []) {
|
|
150
|
+
if (LOCAL_HOSTS.has(url.hostname)) return true;
|
|
151
|
+
return [...BRANDKIT_TRUSTED_ORIGINS, ...extraOrigins].some((origin) => parseUrl(origin)?.origin === url.origin);
|
|
152
|
+
}
|
|
153
|
+
function parseUrl(value, base) {
|
|
154
|
+
try {
|
|
155
|
+
return new URL(value, base);
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function isFetchable(url) {
|
|
161
|
+
return Boolean(url) && !url.username && !url.password && (url.protocol === "https:" || url.protocol === "http:" && LOCAL_HOSTS.has(url.hostname));
|
|
162
|
+
}
|
|
163
|
+
function isBrandKitVersionUrl(value) {
|
|
164
|
+
const url = typeof value === "string" ? parseUrl(value) : null;
|
|
165
|
+
return isFetchable(url) && VERSION_DOCUMENT.test(url.pathname);
|
|
166
|
+
}
|
|
167
|
+
function brandKitUrlVersion(value) {
|
|
168
|
+
const url = typeof value === "string" ? parseUrl(value, "http://localhost/") : null;
|
|
169
|
+
return url ? VERSION_IN_PATH.exec(url.pathname)?.[1] : void 0;
|
|
170
|
+
}
|
|
171
|
+
function brandKitRelease(brandKit) {
|
|
172
|
+
const release = isPlainObject(brandKit) && isPlainObject(brandKit.generator) ? brandKit.generator.release : void 0;
|
|
173
|
+
if (!isPlainObject(release)) return void 0;
|
|
174
|
+
const version = textOf(release.version);
|
|
175
|
+
const url = textOf(release.url);
|
|
176
|
+
if (!version || !VERSION.test(version) || !isBrandKitVersionUrl(url)) return void 0;
|
|
177
|
+
const teamId = typeof release.teamId === "number" ? String(release.teamId) : textOf(release.teamId);
|
|
178
|
+
const kitId = textOf(release.kitId);
|
|
179
|
+
const appUrl = textOf(release.appUrl);
|
|
180
|
+
return { version, url, ...teamId ? { teamId } : {}, ...kitId ? { kitId } : {}, ...appUrl ? { appUrl } : {} };
|
|
181
|
+
}
|
|
182
|
+
function brandKitVersionUrl(release, version) {
|
|
183
|
+
const url = new URL(release.url);
|
|
184
|
+
url.pathname = url.pathname.replace(VERSION_IN_PATH, `/versions/${version}/`);
|
|
185
|
+
return url.toString();
|
|
186
|
+
}
|
|
187
|
+
function brandKitLatestManifestUrl(release) {
|
|
188
|
+
const app = release?.appUrl ? parseUrl(release.appUrl) : null;
|
|
189
|
+
if (!isFetchable(app) || !release.teamId || !release.kitId) return void 0;
|
|
190
|
+
const base = release.appUrl.replace(/\/+$/, "");
|
|
191
|
+
return `${base}/api/brandkit/teams/${encodeURIComponent(release.teamId)}/kits/${encodeURIComponent(release.kitId)}/versions/latest/export/manifest.json`;
|
|
192
|
+
}
|
|
193
|
+
function resolveBrandCandidate(value, options = {}) {
|
|
194
|
+
const input = typeof value === "string" ? value.trim() : "";
|
|
195
|
+
const kinds = `a version (v9), a version link (\u2026/versions/vN/brandkit.json)${options.origin ? " or a path on this page's origin (/candidate.json)" : ""}`;
|
|
196
|
+
if (!input) return { input, error: `it is empty: pass ${kinds}` };
|
|
197
|
+
if (VERSION.test(input)) {
|
|
198
|
+
const release = brandKitRelease(options.brandKit);
|
|
199
|
+
if (!release) {
|
|
200
|
+
return { input, error: `${input} needs the template's brandkit.json to name its published version (generator.release); pass the version link instead` };
|
|
201
|
+
}
|
|
202
|
+
return { input, url: brandKitVersionUrl(release, input), version: input };
|
|
203
|
+
}
|
|
204
|
+
if (options.origin && input.startsWith("/") && !input.startsWith("//")) {
|
|
205
|
+
const page = parseUrl(options.origin);
|
|
206
|
+
const url2 = page ? parseUrl(input, page) : null;
|
|
207
|
+
if (!url2 || url2.origin !== page.origin) return { input, error: `${input} is not a path on this page's origin` };
|
|
208
|
+
const version = VERSION_IN_PATH.exec(url2.pathname)?.[1];
|
|
209
|
+
return { input, url: url2.toString(), ...version ? { version } : {} };
|
|
210
|
+
}
|
|
211
|
+
const url = /^https?:\/\//i.test(input) ? parseUrl(input) : null;
|
|
212
|
+
if (!url) return { input, error: `${input} is not ${kinds}` };
|
|
213
|
+
if (!isFetchable(url)) return { input, error: `${input} is not an https link without credentials` };
|
|
214
|
+
if (!isTrustedOrigin(url, options.extraOrigins)) {
|
|
215
|
+
return { input, error: `${url.origin} is not a Brand Kit origin (${BRANDKIT_TRUSTED_ORIGINS.join(", ")})` };
|
|
216
|
+
}
|
|
217
|
+
if (!VERSION_DOCUMENT.test(url.pathname)) {
|
|
218
|
+
return {
|
|
219
|
+
input,
|
|
220
|
+
error: `${input} is not a published version (\u2026/versions/vN/brandkit.json): a kit's root brandkit.json and versions/latest change with every publish`
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return { input, url: url.toString(), version: VERSION_IN_PATH.exec(url.pathname)[1] };
|
|
224
|
+
}
|
|
225
|
+
function brandKitDocumentProblem(document) {
|
|
226
|
+
if (!isPlainObject(document)) return "it is not a JSON object";
|
|
227
|
+
if (typeof document.version !== "string" || !DOCUMENT_VERSION.test(document.version)) {
|
|
228
|
+
return `its version is ${JSON.stringify(document.version ?? null)}, not 3.x`;
|
|
229
|
+
}
|
|
230
|
+
if (!isPlainObject(document.runtime) || document.runtime.version !== 3) return "it has no runtime version 3";
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
function brandKitLabel(brandKit, version = brandKitRelease(brandKit)?.version) {
|
|
234
|
+
const name = isPlainObject(brandKit) ? textOf(brandKit.name) : void 0;
|
|
235
|
+
return [name, version].filter(Boolean).join(" ") || void 0;
|
|
236
|
+
}
|
|
237
|
+
function runtimeVariables(brandKit) {
|
|
238
|
+
const light = brandKitRuntime(brandKit)?.light ?? {};
|
|
239
|
+
return Object.fromEntries(Object.entries(light).filter(([, value]) => typeof value === "string" || typeof value === "number").map(([name, value]) => [name, String(value)]));
|
|
240
|
+
}
|
|
241
|
+
function fontFamilies(brandKit) {
|
|
242
|
+
if (!isPlainObject(brandKit)) return {};
|
|
243
|
+
const fonts = isPlainObject(brandKit.fonts) ? brandKit.fonts : {};
|
|
244
|
+
const assignments = isPlainObject(brandKit.assignments) ? brandKit.assignments : {};
|
|
245
|
+
const families = {};
|
|
246
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
247
|
+
for (const [slot, id] of Object.entries(assignments)) {
|
|
248
|
+
if (!slot.startsWith("font.") || typeof id !== "string") continue;
|
|
249
|
+
assigned.add(id);
|
|
250
|
+
const family = isPlainObject(fonts[id]) ? textOf(fonts[id].family) : void 0;
|
|
251
|
+
if (family) families[slot.slice("font.".length)] = family;
|
|
252
|
+
}
|
|
253
|
+
for (const [key, font] of Object.entries(fonts)) {
|
|
254
|
+
const family = isPlainObject(font) ? textOf(font.family) : void 0;
|
|
255
|
+
if (family && !assigned.has(key) && !(key in families)) families[key] = family;
|
|
256
|
+
}
|
|
257
|
+
return families;
|
|
258
|
+
}
|
|
259
|
+
function logoFiles(brandKit) {
|
|
260
|
+
const files = {};
|
|
261
|
+
for (const kind of ["primary", "mark"]) {
|
|
262
|
+
const light = brandKitLogo(brandKit, { kind, background: "light" })?.src;
|
|
263
|
+
const dark = brandKitLogo(brandKit, { kind, background: "dark" })?.src;
|
|
264
|
+
if (light) files[kind] = light;
|
|
265
|
+
if (dark && dark !== light) files[`${kind} on dark`] = dark;
|
|
266
|
+
}
|
|
267
|
+
return files;
|
|
268
|
+
}
|
|
269
|
+
function compare(kind, before, after, changes) {
|
|
270
|
+
for (const name of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
271
|
+
if (before[name] === after[name]) continue;
|
|
272
|
+
changes.push({ kind, name, ...name in before ? { before: before[name] } : {}, ...name in after ? { after: after[name] } : {} });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function brandKitChanges(before, after) {
|
|
276
|
+
const changes = [];
|
|
277
|
+
compare("variable", runtimeVariables(before), runtimeVariables(after), changes);
|
|
278
|
+
compare("font", fontFamilies(before), fontFamilies(after), changes);
|
|
279
|
+
compare("logo", logoFiles(before), logoFiles(after), changes);
|
|
280
|
+
return changes;
|
|
281
|
+
}
|
|
282
|
+
function describeBrandKitChange(change) {
|
|
283
|
+
const label = change.kind === "font" ? `Font ${change.name}` : change.kind === "logo" ? `Logo ${change.name}` : change.name;
|
|
284
|
+
if (change.before === void 0) return `${label}: added, ${change.after}`;
|
|
285
|
+
if (change.after === void 0) return `${label}: removed (was ${change.before})`;
|
|
286
|
+
return `${label}: ${change.before} \u2192 ${change.after}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/cli/brand-command.js
|
|
290
|
+
var DEFAULT_BRAND_FILE = "src/brand/brandkit.json";
|
|
291
|
+
var STATUS_TIMEOUT_MS = 5e3;
|
|
292
|
+
var DOWNLOAD_TIMEOUT_MS = 2e4;
|
|
293
|
+
var USAGE = `Usage: uhuu-components brand <command> [--file <path>]
|
|
294
|
+
|
|
295
|
+
Commands:
|
|
296
|
+
brand status The brand version the template holds, and whether a newer one is published
|
|
297
|
+
brand activate <vN | link> Replace the brand file with a published version: v9, or a
|
|
298
|
+
https://\u2026/versions/vN/brandkit.json link
|
|
299
|
+
|
|
300
|
+
Options:
|
|
301
|
+
--file <path> The template's brand file (default: ${DEFAULT_BRAND_FILE})
|
|
302
|
+
-h, --help This help
|
|
303
|
+
|
|
304
|
+
Preview a version before activating it: open the template's development page with ?brand=v9.`;
|
|
305
|
+
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
306
|
+
var decode = (bytes) => new TextDecoder().decode(bytes);
|
|
307
|
+
var versionNumber = (version) => Number(version.slice(1));
|
|
308
|
+
function parseArgs(args) {
|
|
309
|
+
const positional = [];
|
|
310
|
+
let file = DEFAULT_BRAND_FILE;
|
|
311
|
+
let help = false;
|
|
312
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
313
|
+
const arg = args[i];
|
|
314
|
+
if (arg === "-h" || arg === "--help") help = true;
|
|
315
|
+
else if (arg === "--file") {
|
|
316
|
+
file = args[i + 1];
|
|
317
|
+
i += 1;
|
|
318
|
+
if (!file) return { error: "--file needs a path" };
|
|
319
|
+
} else if (arg.startsWith("--file=")) {
|
|
320
|
+
file = arg.slice("--file=".length);
|
|
321
|
+
if (!file) return { error: "--file needs a path" };
|
|
322
|
+
} else if (arg.startsWith("-")) return { error: `unknown option ${arg}` };
|
|
323
|
+
else positional.push(arg);
|
|
324
|
+
}
|
|
325
|
+
return { positional, file, help };
|
|
326
|
+
}
|
|
327
|
+
async function readBrandFile(file) {
|
|
328
|
+
let bytes;
|
|
329
|
+
try {
|
|
330
|
+
bytes = await readFile(file);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (error?.code === "ENOENT") return null;
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
let document;
|
|
336
|
+
try {
|
|
337
|
+
document = JSON.parse(decode(bytes));
|
|
338
|
+
} catch {
|
|
339
|
+
document = void 0;
|
|
340
|
+
}
|
|
341
|
+
return { bytes, document };
|
|
342
|
+
}
|
|
343
|
+
async function newestVersion(manifestUrl, doFetch) {
|
|
344
|
+
const response = await doFetch(manifestUrl, { redirect: "manual", signal: AbortSignal.timeout(STATUS_TIMEOUT_MS) });
|
|
345
|
+
await response.body?.cancel?.().catch(() => {
|
|
346
|
+
});
|
|
347
|
+
const location = response.headers?.get?.("location");
|
|
348
|
+
if (response.status >= 300 && response.status < 400 && location) {
|
|
349
|
+
const version = brandKitUrlVersion(new URL(location, manifestUrl).toString());
|
|
350
|
+
if (version) return version;
|
|
351
|
+
throw new Error(`Brand Kit redirected to ${location}, which names no version`);
|
|
352
|
+
}
|
|
353
|
+
if (response.status === 404) throw new Error("Brand Kit has no published version of this brand (404)");
|
|
354
|
+
throw new Error(`Brand Kit answered ${response.status} instead of a redirect`);
|
|
355
|
+
}
|
|
356
|
+
async function brandStatus({ file, shown, io }) {
|
|
357
|
+
let held;
|
|
358
|
+
try {
|
|
359
|
+
held = await readBrandFile(file);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
io.log(`Could not read ${shown}: ${messageOf(error)}`);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (!held) {
|
|
365
|
+
io.log(`No brand file at ${shown}. Take a published version with: uhuu-components brand activate <\u2026/versions/vN/brandkit.json>`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (!isPlainObject(held.document)) {
|
|
369
|
+
io.log(`${shown} is not a brandkit.json (not a JSON object).`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const release = brandKitRelease(held.document);
|
|
373
|
+
const name = brandKitLabel(held.document, null) ?? "The brand";
|
|
374
|
+
if (!release) {
|
|
375
|
+
io.log(
|
|
376
|
+
`${name} (${shown}) does not name its published version (no generator.release), so newer versions cannot be looked up.
|
|
377
|
+
Take a version by its link to start: uhuu-components brand activate <\u2026/versions/vN/brandkit.json>`
|
|
378
|
+
);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const label = brandKitLabel(held.document);
|
|
382
|
+
const manifest = brandKitLatestManifestUrl(release);
|
|
383
|
+
if (!manifest) {
|
|
384
|
+
io.log(`${label} (${shown}) \xB7 its generator.release names no Brand Kit app (appUrl), so newer versions cannot be looked up.`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
let newest;
|
|
388
|
+
try {
|
|
389
|
+
if (typeof io.fetch !== "function") throw new Error("this Node has no fetch (Node 18 or newer needed)");
|
|
390
|
+
newest = await newestVersion(manifest, io.fetch);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
io.log(`${label} (${shown}) \xB7 could not check for newer versions: ${messageOf(error)}`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const ahead = versionNumber(newest) - versionNumber(release.version);
|
|
396
|
+
if (ahead > 0) {
|
|
397
|
+
io.log(
|
|
398
|
+
`${label} \xB7 ${newest} available (${ahead} newer)
|
|
399
|
+
Preview: open the template with ?brand=${newest}
|
|
400
|
+
Take it: uhuu-components brand activate ${newest}`
|
|
401
|
+
);
|
|
402
|
+
} else if (ahead === 0) {
|
|
403
|
+
io.log(`${label} \xB7 up to date`);
|
|
404
|
+
} else {
|
|
405
|
+
io.log(`${label} \xB7 the newest published version is ${newest}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async function download(url, doFetch) {
|
|
409
|
+
const response = await doFetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
410
|
+
if (!response.ok) throw new Error(`answered ${response.status}`);
|
|
411
|
+
if (response.redirected && response.url && !isBrandKitVersionUrl(response.url)) {
|
|
412
|
+
throw new Error(`redirected to ${response.url}, which is not a published version`);
|
|
413
|
+
}
|
|
414
|
+
return Buffer.from(await response.arrayBuffer());
|
|
415
|
+
}
|
|
416
|
+
function commitMessage(before, after, version) {
|
|
417
|
+
const from = brandKitRelease(before)?.version;
|
|
418
|
+
const name = brandKitLabel(after, null) ?? brandKitLabel(before, null) ?? "brand";
|
|
419
|
+
const previousName = brandKitLabel(before, null);
|
|
420
|
+
if (previousName && previousName !== name) return `Brand: ${brandKitLabel(before) ?? previousName} \u2192 ${brandKitLabel(after, version)}`;
|
|
421
|
+
return from ? `Brand: ${name} ${from} \u2192 ${version}` : `Brand: ${name} ${version}`;
|
|
422
|
+
}
|
|
423
|
+
async function brandActivate({ target, file, shown, io }) {
|
|
424
|
+
let held;
|
|
425
|
+
try {
|
|
426
|
+
held = await readBrandFile(file);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
io.error(`Not activated: could not read ${shown} (${messageOf(error)}).`);
|
|
429
|
+
return 1;
|
|
430
|
+
}
|
|
431
|
+
const candidate = resolveBrandCandidate(target, { brandKit: held?.document, extraOrigins: io.extraOrigins });
|
|
432
|
+
if ("error" in candidate) {
|
|
433
|
+
io.error(`Not activated: ${candidate.error}.`);
|
|
434
|
+
return 1;
|
|
435
|
+
}
|
|
436
|
+
if (typeof io.fetch !== "function") {
|
|
437
|
+
io.error("Not activated: this Node has no fetch (Node 18 or newer needed).");
|
|
438
|
+
return 1;
|
|
439
|
+
}
|
|
440
|
+
let bytes;
|
|
441
|
+
try {
|
|
442
|
+
bytes = await download(candidate.url, io.fetch);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
io.error(`Not activated: ${candidate.url} ${messageOf(error)}.`);
|
|
445
|
+
return 1;
|
|
446
|
+
}
|
|
447
|
+
let document;
|
|
448
|
+
try {
|
|
449
|
+
document = JSON.parse(decode(bytes));
|
|
450
|
+
} catch {
|
|
451
|
+
io.error(`Not activated: ${candidate.url} is not JSON.`);
|
|
452
|
+
return 1;
|
|
453
|
+
}
|
|
454
|
+
const problem = brandKitDocumentProblem(document);
|
|
455
|
+
if (problem) {
|
|
456
|
+
io.error(`Not activated: ${candidate.url} is not a current Brand Kit document (${problem}).`);
|
|
457
|
+
return 1;
|
|
458
|
+
}
|
|
459
|
+
const before = held?.document;
|
|
460
|
+
const label = brandKitLabel(document, candidate.version);
|
|
461
|
+
if (held && Buffer.compare(held.bytes, bytes) === 0) {
|
|
462
|
+
io.log(`${shown} already holds ${label}; nothing written.`);
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
466
|
+
await writeFile(file, bytes);
|
|
467
|
+
const from = before === void 0 ? void 0 : brandKitLabel(before) ?? "a brand without a version";
|
|
468
|
+
const changes = before === void 0 ? [] : brandKitChanges(before, document);
|
|
469
|
+
const message = commitMessage(before, document, candidate.version);
|
|
470
|
+
const lines = [
|
|
471
|
+
`Activated ${label} in ${shown}${from ? ` (was ${from})` : ""}.`,
|
|
472
|
+
` Written as published: ${bytes.length} bytes from ${candidate.url}`,
|
|
473
|
+
""
|
|
474
|
+
];
|
|
475
|
+
if (before === void 0) {
|
|
476
|
+
lines.push(held ? `${shown} was not JSON before, so there is nothing to compare.` : `${shown} is new.`);
|
|
477
|
+
} else if (changes.length) {
|
|
478
|
+
lines.push(`What changed (${changes.length}):`, ...changes.map((change) => ` ${describeBrandKitChange(change)}`));
|
|
479
|
+
} else {
|
|
480
|
+
lines.push("No runtime variable, font or logo changed.");
|
|
481
|
+
}
|
|
482
|
+
lines.push(
|
|
483
|
+
"",
|
|
484
|
+
"Check the template (pages, pagination, print preview), then commit:",
|
|
485
|
+
` git add ${shown} && git commit -m ${JSON.stringify(message)}`
|
|
486
|
+
);
|
|
487
|
+
io.log(lines.join("\n"));
|
|
488
|
+
return 0;
|
|
489
|
+
}
|
|
490
|
+
async function runCli(args, io) {
|
|
491
|
+
const parsed = parseArgs(args);
|
|
492
|
+
if (parsed.error) {
|
|
493
|
+
io.error(`uhuu-components: ${parsed.error}
|
|
494
|
+
|
|
495
|
+
${USAGE}`);
|
|
496
|
+
return 1;
|
|
497
|
+
}
|
|
498
|
+
const [group, command, ...rest] = parsed.positional;
|
|
499
|
+
if (parsed.help) {
|
|
500
|
+
io.log(USAGE);
|
|
501
|
+
return 0;
|
|
502
|
+
}
|
|
503
|
+
if (group !== "brand" || command !== "status" && command !== "activate") {
|
|
504
|
+
const named = [group, command].filter(Boolean).join(" ");
|
|
505
|
+
io.error(`${named ? `uhuu-components: unknown command "${named}"
|
|
506
|
+
|
|
507
|
+
` : ""}${USAGE}`);
|
|
508
|
+
return 1;
|
|
509
|
+
}
|
|
510
|
+
const file = path.resolve(io.cwd, parsed.file);
|
|
511
|
+
const shown = parsed.file;
|
|
512
|
+
if (command === "status") {
|
|
513
|
+
try {
|
|
514
|
+
await brandStatus({ file, shown, io });
|
|
515
|
+
} catch (error) {
|
|
516
|
+
io.log(`Could not check ${shown}: ${messageOf(error)}`);
|
|
517
|
+
}
|
|
518
|
+
return 0;
|
|
519
|
+
}
|
|
520
|
+
if (rest.length !== 1) {
|
|
521
|
+
io.error(`brand activate takes one version: v9, or a \u2026/versions/vN/brandkit.json link.
|
|
522
|
+
|
|
523
|
+
${USAGE}`);
|
|
524
|
+
return 1;
|
|
525
|
+
}
|
|
526
|
+
return brandActivate({ target: rest[0], file, shown, io });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/cli/uhuu-components.js
|
|
530
|
+
runCli(process.argv.slice(2), {
|
|
531
|
+
cwd: process.cwd(),
|
|
532
|
+
fetch: globalThis.fetch,
|
|
533
|
+
// Extra Brand Kit origins (a CDN, a staging deployment), comma-separated.
|
|
534
|
+
extraOrigins: (process.env.UHUU_BRANDKIT_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
535
|
+
log: (text) => process.stdout.write(`${text}
|
|
536
|
+
`),
|
|
537
|
+
error: (text) => process.stderr.write(`${text}
|
|
538
|
+
`)
|
|
539
|
+
}).then(
|
|
540
|
+
(code) => {
|
|
541
|
+
process.exitCode = code;
|
|
542
|
+
},
|
|
543
|
+
(error) => {
|
|
544
|
+
process.stderr.write(`uhuu-components: ${error?.stack ?? error}
|
|
545
|
+
`);
|
|
546
|
+
process.exitCode = 1;
|
|
547
|
+
}
|
|
548
|
+
);
|