shipseal 0.1.1 → 0.4.2
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 +1 -1
- package/dist/cli.js +370 -305
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { cac } from "cac";
|
|
5
5
|
import { appendFile, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -241,6 +241,8 @@ const DEFAULT_CONFIG = {
|
|
|
241
241
|
},
|
|
242
242
|
milestones: {
|
|
243
243
|
stars: [
|
|
244
|
+
10,
|
|
245
|
+
50,
|
|
244
246
|
100,
|
|
245
247
|
250,
|
|
246
248
|
500,
|
|
@@ -323,6 +325,24 @@ const COPY_LIMITS = {
|
|
|
323
325
|
milestoneLine: 48
|
|
324
326
|
};
|
|
325
327
|
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/copy/headline.ts
|
|
330
|
+
/** Below this a line is a fragment or a label, not a description of a change. */
|
|
331
|
+
const MIN_WORDS = 3;
|
|
332
|
+
const MIN_CHARS = 12;
|
|
333
|
+
/** Housekeeping prefixes. A chore is not what a release is about. */
|
|
334
|
+
const CHORE = /^(chore|docs?|ci|build|test|refactor|style|revert|bump|deps?|dependencies)\b[:(]?/i;
|
|
335
|
+
/** A line that is only a person's name: "Adam Chmara", "Jane Q. Smith". */
|
|
336
|
+
const PERSON_NAME = /^[A-Z][\p{L}'’-]+(?:\s+[A-Z][.\p{L}'’-]*){1,2}$/u;
|
|
337
|
+
/** Package-ish fragments: "/api", "@scope/pkg", "solid-query.0.0-rc.3", "v1.2.3". */
|
|
338
|
+
const FRAGMENT = /^[@/]|^v?\d+\.\d+|^[\w@/.-]+$/;
|
|
339
|
+
function isHeadlineWorthy(line) {
|
|
340
|
+
const text = line.trim();
|
|
341
|
+
if (text.length < MIN_CHARS || text.split(/\s+/).length < MIN_WORDS) return false;
|
|
342
|
+
if (CHORE.test(text) || PERSON_NAME.test(text) || FRAGMENT.test(text)) return false;
|
|
343
|
+
return !/https?:\/\//.test(text);
|
|
344
|
+
}
|
|
345
|
+
|
|
326
346
|
//#endregion
|
|
327
347
|
//#region src/copy/deterministic.ts
|
|
328
348
|
function deterministicCopy(facts, maxHighlights = 4, displayName) {
|
|
@@ -331,7 +351,7 @@ function deterministicCopy(facts, maxHighlights = 4, displayName) {
|
|
|
331
351
|
const features = (facts.release?.features ?? []).map((item) => item.value);
|
|
332
352
|
const fixes = (facts.release?.fixes ?? []).map((item) => item.value);
|
|
333
353
|
const breaking = (facts.release?.breaking ?? []).map((item) => item.value);
|
|
334
|
-
const headlineSource = [...breaking, ...features].map((line) => cleanLine(firstSentence(line))).find((line) => line.length <= HEADLINE_MAX_CHARS);
|
|
354
|
+
const headlineSource = [...breaking, ...features].map((line) => cleanLine(firstSentence(line))).find((line) => line.length <= HEADLINE_MAX_CHARS && isHeadlineWorthy(line));
|
|
335
355
|
const title = displayName ?? name;
|
|
336
356
|
const headline = facts.release?.headline?.value ?? headlineSource ?? (version === void 0 ? title : `${title} ${version}`);
|
|
337
357
|
const tagline = facts.release?.subheadline?.value ?? facts.project.tagline?.value;
|
|
@@ -1832,7 +1852,7 @@ const storyPage = {
|
|
|
1832
1852
|
box: { width: 220 }
|
|
1833
1853
|
},
|
|
1834
1854
|
title: {
|
|
1835
|
-
maxLines: tall ?
|
|
1855
|
+
maxLines: tall ? 4 : 2,
|
|
1836
1856
|
maxFontSize: tall ? 64 : 48,
|
|
1837
1857
|
minFontSize: tall ? 36 : 30,
|
|
1838
1858
|
step: 2,
|
|
@@ -2522,6 +2542,14 @@ function stripBadges(text) {
|
|
|
2522
2542
|
return text.replace(/\[!\[[^\]]*]\([^)]+\)]\([^)]+\)/g, "").replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/<img[^>]*>/gi, "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
2523
2543
|
}
|
|
2524
2544
|
|
|
2545
|
+
//#endregion
|
|
2546
|
+
//#region src/sources/release-line.ts
|
|
2547
|
+
/** Markdown escapes a maintainer never meant to see rendered: \[ruff\] becomes [ruff]. */
|
|
2548
|
+
const MARKDOWN_ESCAPE = /\\([[\]()*_`~#+\-.!])/g;
|
|
2549
|
+
function cleanReleaseLine(item) {
|
|
2550
|
+
return item.replace(/^\s*[-*]\s*/, "").replace(/\[`[a-f0-9]{7,40}`]\([^)]+\)/gi, "").replace(/^[a-f0-9]{7,40}:\s*/i, "").replace(/\(#\d+\)/g, "").replace(/\[#\d+]\([^)]+\)/g, "").replace(/#\d+\b/g, "").replace(/Thanks\s+\[@[\w-]+]\([^)]+\)!?\s*-?\s*/gi, "").replace(/\[@[\w-]+]\([^)]+\)/g, "").replace(/\(@[\w-]+\)/g, "").replace(/\s+by\s+@[\w-]+/gi, "").replace(/@[\w-]+/g, "").replace(/^Thanks\s*!?\s*-?\s*/i, "").replaceAll(/!\[[^\]]*\]\([^)]*\)/g, "").replaceAll(/\[([^\]]+)\]\([^)]*\)/g, "$1").replaceAll(/<[^>]*>/g, "").replaceAll(MARKDOWN_ESCAPE, "$1").replaceAll(/[*`]/g, "").replaceAll(/\(\s*[a-f0-9]{6,40}\s*\)/gi, "").replace(/^\[[^\]]{1,24}\]\s*/, "").replace(/^\s*[-*]\s*/, "").replace(/\s*\(\s*\)/g, "").replace(/\s+([,.;:])/g, "$1").replace(/\s+/g, " ").trim().replace(/[\s,;:-]+$/, "").replace(/\.$/, "");
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2525
2553
|
//#endregion
|
|
2526
2554
|
//#region src/sources/changelog.ts
|
|
2527
2555
|
async function collectChangelog(cwd, version, changelogPath = "CHANGELOG.md") {
|
|
@@ -2623,7 +2651,7 @@ function findVersionSection(markdown, version) {
|
|
|
2623
2651
|
}
|
|
2624
2652
|
}
|
|
2625
2653
|
function cleanChangelogItem(item) {
|
|
2626
|
-
return item
|
|
2654
|
+
return cleanReleaseLine(item);
|
|
2627
2655
|
}
|
|
2628
2656
|
function headingIncludesVersion(heading, version) {
|
|
2629
2657
|
const unwrapped = heading.replace(/[[\]]/g, " ");
|
|
@@ -4552,6 +4580,254 @@ function isRecord(value) {
|
|
|
4552
4580
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4553
4581
|
}
|
|
4554
4582
|
|
|
4583
|
+
//#endregion
|
|
4584
|
+
//#region src/brand/tailwind.ts
|
|
4585
|
+
const COLOR_PROP = /--color-([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
4586
|
+
const CUSTOM_PROP = /--([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
4587
|
+
const SKIP_PREFIXES = /* @__PURE__ */ new Set([
|
|
4588
|
+
"chart",
|
|
4589
|
+
"sidebar",
|
|
4590
|
+
"ring",
|
|
4591
|
+
"border",
|
|
4592
|
+
"input",
|
|
4593
|
+
"destructive"
|
|
4594
|
+
]);
|
|
4595
|
+
function extractTailwindV4Colors(css) {
|
|
4596
|
+
const blocks = themeBlocks(css);
|
|
4597
|
+
const custom = collectCustomProperties(css);
|
|
4598
|
+
const fromTheme = {};
|
|
4599
|
+
for (const block of blocks) {
|
|
4600
|
+
COLOR_PROP.lastIndex = 0;
|
|
4601
|
+
let match;
|
|
4602
|
+
while ((match = COLOR_PROP.exec(block)) !== null) {
|
|
4603
|
+
const name = match[1];
|
|
4604
|
+
const raw = match[2];
|
|
4605
|
+
if (name === void 0 || raw === void 0 || shouldSkip(name)) continue;
|
|
4606
|
+
fromTheme[name] = raw.trim();
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
const resolved = {};
|
|
4610
|
+
for (const [name, raw] of Object.entries(fromTheme)) {
|
|
4611
|
+
const hex = resolveToHex(raw, custom, 0);
|
|
4612
|
+
if (hex !== void 0) resolved[name] = hex;
|
|
4613
|
+
}
|
|
4614
|
+
return mapBrandColors(resolved);
|
|
4615
|
+
}
|
|
4616
|
+
function extractTailwindV3Colors(source) {
|
|
4617
|
+
const found = {};
|
|
4618
|
+
const re = /\b(primary|brand|accent|background|foreground|muted|secondary)\b\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
4619
|
+
let match;
|
|
4620
|
+
while ((match = re.exec(source)) !== null) {
|
|
4621
|
+
const name = match[1];
|
|
4622
|
+
const raw = match[2];
|
|
4623
|
+
if (name === void 0 || raw === void 0) continue;
|
|
4624
|
+
const hex = parseCssColor(raw);
|
|
4625
|
+
if (hex !== void 0) found[name] = hex;
|
|
4626
|
+
}
|
|
4627
|
+
return mapBrandColors(found);
|
|
4628
|
+
}
|
|
4629
|
+
function themeBlocks(css) {
|
|
4630
|
+
const blocks = [];
|
|
4631
|
+
const re = /@theme(?:\s+inline)?\s*\{/g;
|
|
4632
|
+
let match;
|
|
4633
|
+
while ((match = re.exec(css)) !== null) {
|
|
4634
|
+
const open = css.indexOf("{", match.index);
|
|
4635
|
+
if (open === -1) break;
|
|
4636
|
+
const close = matchingBrace(css, open);
|
|
4637
|
+
if (close === -1) break;
|
|
4638
|
+
blocks.push(css.slice(open + 1, close));
|
|
4639
|
+
}
|
|
4640
|
+
return blocks;
|
|
4641
|
+
}
|
|
4642
|
+
function matchingBrace(source, openIndex) {
|
|
4643
|
+
let depth = 0;
|
|
4644
|
+
for (let i = openIndex; i < source.length; i += 1) {
|
|
4645
|
+
const ch = source[i];
|
|
4646
|
+
if (ch === "{") depth += 1;
|
|
4647
|
+
else if (ch === "}") {
|
|
4648
|
+
depth -= 1;
|
|
4649
|
+
if (depth === 0) return i;
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
return -1;
|
|
4653
|
+
}
|
|
4654
|
+
function collectCustomProperties(css) {
|
|
4655
|
+
const props = /* @__PURE__ */ new Map();
|
|
4656
|
+
CUSTOM_PROP.lastIndex = 0;
|
|
4657
|
+
let match;
|
|
4658
|
+
while ((match = CUSTOM_PROP.exec(css)) !== null) {
|
|
4659
|
+
const name = match[1];
|
|
4660
|
+
const value = match[2];
|
|
4661
|
+
if (name !== void 0 && value !== void 0) props.set(`--${name}`, value.trim());
|
|
4662
|
+
}
|
|
4663
|
+
return props;
|
|
4664
|
+
}
|
|
4665
|
+
function resolveToHex(raw, custom, depth) {
|
|
4666
|
+
if (depth > 3) return;
|
|
4667
|
+
const direct = parseCssColor(raw);
|
|
4668
|
+
if (direct !== void 0) return direct;
|
|
4669
|
+
const varMatch = /^var\(\s*(--[a-zA-Z0-9-]+)\s*(?:,[^)]+)?\)$/.exec(raw);
|
|
4670
|
+
if (varMatch === null) return;
|
|
4671
|
+
const ref = varMatch[1];
|
|
4672
|
+
if (ref === void 0) return;
|
|
4673
|
+
const next = custom.get(ref);
|
|
4674
|
+
if (next === void 0) return;
|
|
4675
|
+
return resolveToHex(next, custom, depth + 1);
|
|
4676
|
+
}
|
|
4677
|
+
function shouldSkip(name) {
|
|
4678
|
+
const root = name.split("-")[0];
|
|
4679
|
+
return root !== void 0 && SKIP_PREFIXES.has(root);
|
|
4680
|
+
}
|
|
4681
|
+
function mapBrandColors(found) {
|
|
4682
|
+
const out = {};
|
|
4683
|
+
const background = found.background ?? found.bg;
|
|
4684
|
+
if (background !== void 0) out.background = background;
|
|
4685
|
+
const foreground = found.foreground ?? found.fg;
|
|
4686
|
+
if (foreground !== void 0) out.foreground = foreground;
|
|
4687
|
+
const muted = found["muted-foreground"] ?? found.muted;
|
|
4688
|
+
if (muted !== void 0) out.muted = muted;
|
|
4689
|
+
const primary = found.primary ?? found.brand;
|
|
4690
|
+
if (primary !== void 0) out.primary = primary;
|
|
4691
|
+
const accent = found.accent ?? found.secondary;
|
|
4692
|
+
if (accent !== void 0) out.accent = accent;
|
|
4693
|
+
return out;
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
//#endregion
|
|
4697
|
+
//#region src/brand/detect-colors.ts
|
|
4698
|
+
/** Path helpers, kept local so this module never imports `node:path`. */
|
|
4699
|
+
function basename$1(path) {
|
|
4700
|
+
return path.split("/").pop() ?? path;
|
|
4701
|
+
}
|
|
4702
|
+
function relative(from, path) {
|
|
4703
|
+
const prefix = from.endsWith("/") ? from : `${from}/`;
|
|
4704
|
+
return path.startsWith(prefix) ? path.slice(prefix.length) : path;
|
|
4705
|
+
}
|
|
4706
|
+
async function detectColorsFrom(files, cwd, logoPath, sources, notes, logoColor = () => Promise.resolve(void 0)) {
|
|
4707
|
+
const merged = {};
|
|
4708
|
+
const paths = await files.list();
|
|
4709
|
+
const cssFiles = paths.filter((file) => file.endsWith(".css"));
|
|
4710
|
+
const cssContents = await Promise.all(cssFiles.map(async (file) => ({
|
|
4711
|
+
file,
|
|
4712
|
+
css: await files.read(file)
|
|
4713
|
+
})));
|
|
4714
|
+
for (const { file, css } of cssContents) {
|
|
4715
|
+
if (css === void 0) continue;
|
|
4716
|
+
const fromTheme = extractTailwindV4Colors(css);
|
|
4717
|
+
const fromRoot = extractCssRootColors(css);
|
|
4718
|
+
const rel = relative(cwd, file);
|
|
4719
|
+
applyColors(merged, fromTheme, sources, `Tailwind v4 @theme in ${rel}`);
|
|
4720
|
+
applyColors(merged, fromRoot, sources, `:root variables in ${rel}`);
|
|
4721
|
+
}
|
|
4722
|
+
const configFiles = paths.filter((file) => basename$1(file).startsWith("tailwind.config."));
|
|
4723
|
+
const configContents = await Promise.all(configFiles.map(async (file) => ({
|
|
4724
|
+
file,
|
|
4725
|
+
source: await files.read(file)
|
|
4726
|
+
})));
|
|
4727
|
+
for (const { file, source } of configContents) {
|
|
4728
|
+
if (source === void 0) continue;
|
|
4729
|
+
applyColors(merged, extractTailwindV3Colors(source), sources, `Tailwind config ${relative(cwd, file)}`);
|
|
4730
|
+
}
|
|
4731
|
+
const tokenFile = paths.find((file) => file.endsWith(".tokens.json"));
|
|
4732
|
+
if (tokenFile !== void 0) {
|
|
4733
|
+
const raw = await files.read(tokenFile);
|
|
4734
|
+
if (raw !== void 0) try {
|
|
4735
|
+
const parsed = JSON.parse(raw);
|
|
4736
|
+
applyColors(merged, extractDtcgColors(parsed), sources, relative(cwd, tokenFile));
|
|
4737
|
+
} catch (error) {
|
|
4738
|
+
if (error instanceof SyntaxError) notes.push(`Could not parse design tokens file ${relative(cwd, tokenFile)}.`);
|
|
4739
|
+
else throw error;
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
if (merged.primary === void 0 && logoPath !== void 0) {
|
|
4743
|
+
const fromPng = logoPath.endsWith(".png") ? await logoColor(files, logoPath) : void 0;
|
|
4744
|
+
if (fromPng !== void 0) {
|
|
4745
|
+
merged.primary = fromPng;
|
|
4746
|
+
sources.push({
|
|
4747
|
+
field: "colors.primary",
|
|
4748
|
+
source: `dominant color in ${logoPath}`
|
|
4749
|
+
});
|
|
4750
|
+
}
|
|
4751
|
+
const svg = logoPath.endsWith(".svg") ? await files.read(logoPath) : void 0;
|
|
4752
|
+
if (svg !== void 0) {
|
|
4753
|
+
const fromLogo = firstNonNeutralSvgColor(svg);
|
|
4754
|
+
if (fromLogo !== void 0) {
|
|
4755
|
+
merged.primary = fromLogo;
|
|
4756
|
+
sources.push({
|
|
4757
|
+
field: "colors.primary",
|
|
4758
|
+
source: `SVG fill in ${logoPath}`
|
|
4759
|
+
});
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
const fellBack = [
|
|
4764
|
+
"background",
|
|
4765
|
+
"foreground",
|
|
4766
|
+
"muted",
|
|
4767
|
+
"primary",
|
|
4768
|
+
"accent"
|
|
4769
|
+
].filter((field) => merged[field] === void 0);
|
|
4770
|
+
if (fellBack.length > 0) notes.push(`Using built-in defaults for ${fellBack.map((f) => `colors.${f}`).join(", ")}. Nothing in this project set them. Edit .shipseal/brand.json to use your own.`);
|
|
4771
|
+
return {
|
|
4772
|
+
background: merged.background ?? DEFAULT_BRAND_COLORS.background,
|
|
4773
|
+
foreground: merged.foreground ?? DEFAULT_BRAND_COLORS.foreground,
|
|
4774
|
+
muted: merged.muted ?? DEFAULT_BRAND_COLORS.muted,
|
|
4775
|
+
primary: merged.primary ?? DEFAULT_BRAND_COLORS.primary,
|
|
4776
|
+
accent: merged.accent ?? DEFAULT_BRAND_COLORS.accent
|
|
4777
|
+
};
|
|
4778
|
+
}
|
|
4779
|
+
function applyColors(target, incoming, sources, source) {
|
|
4780
|
+
for (const key of [
|
|
4781
|
+
"background",
|
|
4782
|
+
"foreground",
|
|
4783
|
+
"muted",
|
|
4784
|
+
"primary",
|
|
4785
|
+
"accent"
|
|
4786
|
+
]) {
|
|
4787
|
+
const value = incoming[key];
|
|
4788
|
+
if (value !== void 0 && target[key] === void 0) {
|
|
4789
|
+
target[key] = value;
|
|
4790
|
+
sources.push({
|
|
4791
|
+
field: `colors.${key}`,
|
|
4792
|
+
source
|
|
4793
|
+
});
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
function applyReadableMuted(colors, sources, notes) {
|
|
4798
|
+
if (colors.muted === void 0 || readableOnBackground(colors.background, colors.muted)) return;
|
|
4799
|
+
colors.muted = DEFAULT_BRAND_COLORS.muted;
|
|
4800
|
+
notes.push(`Muted text color failed WCAG AA large-text contrast (3:1) against background, so it was set to ${DEFAULT_BRAND_COLORS.muted}.`);
|
|
4801
|
+
upsertSource(sources, "colors.muted", "contrast adjustment");
|
|
4802
|
+
}
|
|
4803
|
+
function applyReadableAccent(colors, sources, notes) {
|
|
4804
|
+
if (colors.accent === void 0 || readableOnBackground(colors.background, colors.accent)) return;
|
|
4805
|
+
colors.accent = colors.primary ?? DEFAULT_BRAND_COLORS.primary;
|
|
4806
|
+
notes.push("Accent failed contrast against background, so it uses the primary color.");
|
|
4807
|
+
upsertSource(sources, "colors.accent", "contrast adjustment");
|
|
4808
|
+
}
|
|
4809
|
+
function upsertSource(sources, field, source) {
|
|
4810
|
+
const existing = sources.find((entry) => entry.field === field);
|
|
4811
|
+
if (existing !== void 0) {
|
|
4812
|
+
existing.source = source;
|
|
4813
|
+
return;
|
|
4814
|
+
}
|
|
4815
|
+
sources.push({
|
|
4816
|
+
field,
|
|
4817
|
+
source
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
function firstNonNeutralSvgColor(svg) {
|
|
4821
|
+
const re = /(?:fill|stroke)="([^"]+)"/g;
|
|
4822
|
+
let match;
|
|
4823
|
+
while ((match = re.exec(svg)) !== null) {
|
|
4824
|
+
const raw = match[1];
|
|
4825
|
+
if (raw === void 0 || raw === "none" || raw === "currentColor") continue;
|
|
4826
|
+
const hex = parseCssColor(raw);
|
|
4827
|
+
if (hex !== void 0 && !isNeutralHex(hex)) return hex;
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4555
4831
|
//#endregion
|
|
4556
4832
|
//#region src/brand/logo.ts
|
|
4557
4833
|
const DIRECTORIES = [
|
|
@@ -4797,131 +5073,12 @@ function dominantNonNeutralColor(png) {
|
|
|
4797
5073
|
return `#${channel(best.r)}${channel(best.g)}${channel(best.b)}`;
|
|
4798
5074
|
}
|
|
4799
5075
|
|
|
4800
|
-
//#endregion
|
|
4801
|
-
//#region src/brand/tailwind.ts
|
|
4802
|
-
const COLOR_PROP = /--color-([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
4803
|
-
const CUSTOM_PROP = /--([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
4804
|
-
const SKIP_PREFIXES = /* @__PURE__ */ new Set([
|
|
4805
|
-
"chart",
|
|
4806
|
-
"sidebar",
|
|
4807
|
-
"ring",
|
|
4808
|
-
"border",
|
|
4809
|
-
"input",
|
|
4810
|
-
"destructive"
|
|
4811
|
-
]);
|
|
4812
|
-
function extractTailwindV4Colors(css) {
|
|
4813
|
-
const blocks = themeBlocks(css);
|
|
4814
|
-
const custom = collectCustomProperties(css);
|
|
4815
|
-
const fromTheme = {};
|
|
4816
|
-
for (const block of blocks) {
|
|
4817
|
-
COLOR_PROP.lastIndex = 0;
|
|
4818
|
-
let match;
|
|
4819
|
-
while ((match = COLOR_PROP.exec(block)) !== null) {
|
|
4820
|
-
const name = match[1];
|
|
4821
|
-
const raw = match[2];
|
|
4822
|
-
if (name === void 0 || raw === void 0 || shouldSkip(name)) continue;
|
|
4823
|
-
fromTheme[name] = raw.trim();
|
|
4824
|
-
}
|
|
4825
|
-
}
|
|
4826
|
-
const resolved = {};
|
|
4827
|
-
for (const [name, raw] of Object.entries(fromTheme)) {
|
|
4828
|
-
const hex = resolveToHex(raw, custom, 0);
|
|
4829
|
-
if (hex !== void 0) resolved[name] = hex;
|
|
4830
|
-
}
|
|
4831
|
-
return mapBrandColors(resolved);
|
|
4832
|
-
}
|
|
4833
|
-
function extractTailwindV3Colors(source) {
|
|
4834
|
-
const found = {};
|
|
4835
|
-
const re = /\b(primary|brand|accent|background|foreground|muted|secondary)\b\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
4836
|
-
let match;
|
|
4837
|
-
while ((match = re.exec(source)) !== null) {
|
|
4838
|
-
const name = match[1];
|
|
4839
|
-
const raw = match[2];
|
|
4840
|
-
if (name === void 0 || raw === void 0) continue;
|
|
4841
|
-
const hex = parseCssColor(raw);
|
|
4842
|
-
if (hex !== void 0) found[name] = hex;
|
|
4843
|
-
}
|
|
4844
|
-
return mapBrandColors(found);
|
|
4845
|
-
}
|
|
4846
|
-
function themeBlocks(css) {
|
|
4847
|
-
const blocks = [];
|
|
4848
|
-
const re = /@theme(?:\s+inline)?\s*\{/g;
|
|
4849
|
-
let match;
|
|
4850
|
-
while ((match = re.exec(css)) !== null) {
|
|
4851
|
-
const open = css.indexOf("{", match.index);
|
|
4852
|
-
if (open === -1) break;
|
|
4853
|
-
const close = matchingBrace(css, open);
|
|
4854
|
-
if (close === -1) break;
|
|
4855
|
-
blocks.push(css.slice(open + 1, close));
|
|
4856
|
-
}
|
|
4857
|
-
return blocks;
|
|
4858
|
-
}
|
|
4859
|
-
function matchingBrace(source, openIndex) {
|
|
4860
|
-
let depth = 0;
|
|
4861
|
-
for (let i = openIndex; i < source.length; i += 1) {
|
|
4862
|
-
const ch = source[i];
|
|
4863
|
-
if (ch === "{") depth += 1;
|
|
4864
|
-
else if (ch === "}") {
|
|
4865
|
-
depth -= 1;
|
|
4866
|
-
if (depth === 0) return i;
|
|
4867
|
-
}
|
|
4868
|
-
}
|
|
4869
|
-
return -1;
|
|
4870
|
-
}
|
|
4871
|
-
function collectCustomProperties(css) {
|
|
4872
|
-
const props = /* @__PURE__ */ new Map();
|
|
4873
|
-
CUSTOM_PROP.lastIndex = 0;
|
|
4874
|
-
let match;
|
|
4875
|
-
while ((match = CUSTOM_PROP.exec(css)) !== null) {
|
|
4876
|
-
const name = match[1];
|
|
4877
|
-
const value = match[2];
|
|
4878
|
-
if (name !== void 0 && value !== void 0) props.set(`--${name}`, value.trim());
|
|
4879
|
-
}
|
|
4880
|
-
return props;
|
|
4881
|
-
}
|
|
4882
|
-
function resolveToHex(raw, custom, depth) {
|
|
4883
|
-
if (depth > 3) return;
|
|
4884
|
-
const direct = parseCssColor(raw);
|
|
4885
|
-
if (direct !== void 0) return direct;
|
|
4886
|
-
const varMatch = /^var\(\s*(--[a-zA-Z0-9-]+)\s*(?:,[^)]+)?\)$/.exec(raw);
|
|
4887
|
-
if (varMatch === null) return;
|
|
4888
|
-
const ref = varMatch[1];
|
|
4889
|
-
if (ref === void 0) return;
|
|
4890
|
-
const next = custom.get(ref);
|
|
4891
|
-
if (next === void 0) return;
|
|
4892
|
-
return resolveToHex(next, custom, depth + 1);
|
|
4893
|
-
}
|
|
4894
|
-
function shouldSkip(name) {
|
|
4895
|
-
const root = name.split("-")[0];
|
|
4896
|
-
return root !== void 0 && SKIP_PREFIXES.has(root);
|
|
4897
|
-
}
|
|
4898
|
-
function mapBrandColors(found) {
|
|
4899
|
-
const out = {};
|
|
4900
|
-
const background = found.background ?? found.bg;
|
|
4901
|
-
if (background !== void 0) out.background = background;
|
|
4902
|
-
const foreground = found.foreground ?? found.fg;
|
|
4903
|
-
if (foreground !== void 0) out.foreground = foreground;
|
|
4904
|
-
const muted = found["muted-foreground"] ?? found.muted;
|
|
4905
|
-
if (muted !== void 0) out.muted = muted;
|
|
4906
|
-
const primary = found.primary ?? found.brand;
|
|
4907
|
-
if (primary !== void 0) out.primary = primary;
|
|
4908
|
-
const accent = found.accent ?? found.secondary;
|
|
4909
|
-
if (accent !== void 0) out.accent = accent;
|
|
4910
|
-
return out;
|
|
4911
|
-
}
|
|
4912
|
-
|
|
4913
5076
|
//#endregion
|
|
4914
5077
|
//#region src/brand/detect.ts
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
"
|
|
4918
|
-
|
|
4919
|
-
"dist",
|
|
4920
|
-
"coverage",
|
|
4921
|
-
".shipseal",
|
|
4922
|
-
".next",
|
|
4923
|
-
"build"
|
|
4924
|
-
]);
|
|
5078
|
+
/** Path helpers, kept local so this module stays free of `node:path` and reaches the browser. */
|
|
5079
|
+
function basename(path) {
|
|
5080
|
+
return path.split("/").pop() ?? path;
|
|
5081
|
+
}
|
|
4925
5082
|
const packageSchema = z.object({
|
|
4926
5083
|
private: z.boolean().optional(),
|
|
4927
5084
|
name: z.string().optional(),
|
|
@@ -4929,20 +5086,24 @@ const packageSchema = z.object({
|
|
|
4929
5086
|
homepage: z.string().optional(),
|
|
4930
5087
|
repository: z.union([z.string(), z.object({ url: z.string().optional() })]).optional()
|
|
4931
5088
|
});
|
|
4932
|
-
|
|
5089
|
+
/**
|
|
5090
|
+
* Detection over any source. `root` is only used to make reported paths readable, so the
|
|
5091
|
+
* GitHub implementation can pass the repository slug.
|
|
5092
|
+
*/
|
|
5093
|
+
async function detectBrandFrom(files, cwd = ".") {
|
|
4933
5094
|
const sources = [];
|
|
4934
5095
|
const notes = [];
|
|
4935
|
-
const pkg = await readPackage(
|
|
4936
|
-
const readme = await
|
|
5096
|
+
const pkg = await readPackage(files);
|
|
5097
|
+
const readme = await files.read("README.md");
|
|
4937
5098
|
const name = detectName(pkg, readme, cwd, sources);
|
|
4938
5099
|
const tagline = detectTagline(pkg, readme, sources);
|
|
4939
|
-
const url = await detectUrl(pkg, cwd, sources);
|
|
5100
|
+
const url = await detectUrl(files, pkg, cwd, sources);
|
|
4940
5101
|
const logo = findLogoPair(cwd);
|
|
4941
5102
|
if (logo !== void 0) sources.push({
|
|
4942
5103
|
field: "logo",
|
|
4943
5104
|
source: logo.light
|
|
4944
5105
|
});
|
|
4945
|
-
const colors = await
|
|
5106
|
+
const colors = await detectColorsFrom(files, cwd, logo?.light, sources, notes, dominantLogoColor);
|
|
4946
5107
|
const contrast = ensureForegroundContrast(colors.background, colors.foreground);
|
|
4947
5108
|
if (contrast.adjusted) {
|
|
4948
5109
|
colors.foreground = contrast.foreground;
|
|
@@ -5032,7 +5193,7 @@ function detectTagline(pkg, readme, sources) {
|
|
|
5032
5193
|
}
|
|
5033
5194
|
}
|
|
5034
5195
|
}
|
|
5035
|
-
async function detectUrl(pkg, cwd, sources) {
|
|
5196
|
+
async function detectUrl(files, pkg, cwd, sources) {
|
|
5036
5197
|
if (pkg?.homepage !== void 0 && pkg.homepage.length > 0) {
|
|
5037
5198
|
sources.push({
|
|
5038
5199
|
field: "url",
|
|
@@ -5048,7 +5209,7 @@ async function detectUrl(pkg, cwd, sources) {
|
|
|
5048
5209
|
});
|
|
5049
5210
|
return repo;
|
|
5050
5211
|
}
|
|
5051
|
-
const remote = await gitRemote(
|
|
5212
|
+
const remote = await files.gitRemote?.();
|
|
5052
5213
|
if (remote !== void 0) {
|
|
5053
5214
|
sources.push({
|
|
5054
5215
|
field: "url",
|
|
@@ -5057,122 +5218,8 @@ async function detectUrl(pkg, cwd, sources) {
|
|
|
5057
5218
|
return remote;
|
|
5058
5219
|
}
|
|
5059
5220
|
}
|
|
5060
|
-
async function
|
|
5061
|
-
const
|
|
5062
|
-
const files = await listFiles(cwd, 4);
|
|
5063
|
-
const cssFiles = files.filter((file) => file.endsWith(".css"));
|
|
5064
|
-
const cssContents = await Promise.all(cssFiles.map(async (file) => ({
|
|
5065
|
-
file,
|
|
5066
|
-
css: await readMaybe(file)
|
|
5067
|
-
})));
|
|
5068
|
-
for (const { file, css } of cssContents) {
|
|
5069
|
-
if (css === void 0) continue;
|
|
5070
|
-
const fromTheme = extractTailwindV4Colors(css);
|
|
5071
|
-
const fromRoot = extractCssRootColors(css);
|
|
5072
|
-
const rel = relative(cwd, file);
|
|
5073
|
-
applyColors(merged, fromTheme, sources, `Tailwind v4 @theme in ${rel}`);
|
|
5074
|
-
applyColors(merged, fromRoot, sources, `:root variables in ${rel}`);
|
|
5075
|
-
}
|
|
5076
|
-
const configFiles = files.filter((file) => basename(file).startsWith("tailwind.config."));
|
|
5077
|
-
const configContents = await Promise.all(configFiles.map(async (file) => ({
|
|
5078
|
-
file,
|
|
5079
|
-
source: await readMaybe(file)
|
|
5080
|
-
})));
|
|
5081
|
-
for (const { file, source } of configContents) {
|
|
5082
|
-
if (source === void 0) continue;
|
|
5083
|
-
applyColors(merged, extractTailwindV3Colors(source), sources, `Tailwind config ${relative(cwd, file)}`);
|
|
5084
|
-
}
|
|
5085
|
-
const tokenFile = files.find((file) => file.endsWith(".tokens.json"));
|
|
5086
|
-
if (tokenFile !== void 0) {
|
|
5087
|
-
const raw = await readMaybe(tokenFile);
|
|
5088
|
-
if (raw !== void 0) try {
|
|
5089
|
-
const parsed = JSON.parse(raw);
|
|
5090
|
-
applyColors(merged, extractDtcgColors(parsed), sources, relative(cwd, tokenFile));
|
|
5091
|
-
} catch (error) {
|
|
5092
|
-
if (error instanceof SyntaxError) notes.push(`Could not parse design tokens file ${relative(cwd, tokenFile)}.`);
|
|
5093
|
-
else throw error;
|
|
5094
|
-
}
|
|
5095
|
-
}
|
|
5096
|
-
if (merged.primary === void 0 && logoPath !== void 0) {
|
|
5097
|
-
const fromPng = logoPath.endsWith(".png") ? await dominantLogoColor(join(cwd, logoPath)) : void 0;
|
|
5098
|
-
if (fromPng !== void 0) {
|
|
5099
|
-
merged.primary = fromPng;
|
|
5100
|
-
sources.push({
|
|
5101
|
-
field: "colors.primary",
|
|
5102
|
-
source: `dominant color in ${logoPath}`
|
|
5103
|
-
});
|
|
5104
|
-
}
|
|
5105
|
-
const svg = logoPath.endsWith(".svg") ? await readMaybe(join(cwd, logoPath)) : void 0;
|
|
5106
|
-
if (svg !== void 0) {
|
|
5107
|
-
const fromLogo = firstNonNeutralSvgColor(svg);
|
|
5108
|
-
if (fromLogo !== void 0) {
|
|
5109
|
-
merged.primary = fromLogo;
|
|
5110
|
-
sources.push({
|
|
5111
|
-
field: "colors.primary",
|
|
5112
|
-
source: `SVG fill in ${logoPath}`
|
|
5113
|
-
});
|
|
5114
|
-
}
|
|
5115
|
-
}
|
|
5116
|
-
}
|
|
5117
|
-
const fellBack = [
|
|
5118
|
-
"background",
|
|
5119
|
-
"foreground",
|
|
5120
|
-
"muted",
|
|
5121
|
-
"primary",
|
|
5122
|
-
"accent"
|
|
5123
|
-
].filter((field) => merged[field] === void 0);
|
|
5124
|
-
if (fellBack.length > 0) notes.push(`Using built-in defaults for ${fellBack.map((f) => `colors.${f}`).join(", ")}. Nothing in this project set them. Edit .shipseal/brand.json to use your own.`);
|
|
5125
|
-
return {
|
|
5126
|
-
background: merged.background ?? DEFAULT_BRAND_COLORS.background,
|
|
5127
|
-
foreground: merged.foreground ?? DEFAULT_BRAND_COLORS.foreground,
|
|
5128
|
-
muted: merged.muted ?? DEFAULT_BRAND_COLORS.muted,
|
|
5129
|
-
primary: merged.primary ?? DEFAULT_BRAND_COLORS.primary,
|
|
5130
|
-
accent: merged.accent ?? DEFAULT_BRAND_COLORS.accent
|
|
5131
|
-
};
|
|
5132
|
-
}
|
|
5133
|
-
function applyReadableMuted(colors, sources, notes) {
|
|
5134
|
-
if (colors.muted === void 0 || readableOnBackground(colors.background, colors.muted)) return;
|
|
5135
|
-
colors.muted = DEFAULT_BRAND_COLORS.muted;
|
|
5136
|
-
notes.push(`Muted text color failed WCAG AA large-text contrast (3:1) against background, so it was set to ${DEFAULT_BRAND_COLORS.muted}.`);
|
|
5137
|
-
upsertSource(sources, "colors.muted", "contrast adjustment");
|
|
5138
|
-
}
|
|
5139
|
-
function applyReadableAccent(colors, sources, notes) {
|
|
5140
|
-
if (colors.accent === void 0 || readableOnBackground(colors.background, colors.accent)) return;
|
|
5141
|
-
colors.accent = colors.primary ?? DEFAULT_BRAND_COLORS.primary;
|
|
5142
|
-
notes.push("Accent failed contrast against background, so it uses the primary color.");
|
|
5143
|
-
upsertSource(sources, "colors.accent", "contrast adjustment");
|
|
5144
|
-
}
|
|
5145
|
-
function applyColors(target, incoming, sources, source) {
|
|
5146
|
-
for (const key of [
|
|
5147
|
-
"background",
|
|
5148
|
-
"foreground",
|
|
5149
|
-
"muted",
|
|
5150
|
-
"primary",
|
|
5151
|
-
"accent"
|
|
5152
|
-
]) {
|
|
5153
|
-
const value = incoming[key];
|
|
5154
|
-
if (value !== void 0 && target[key] === void 0) {
|
|
5155
|
-
target[key] = value;
|
|
5156
|
-
sources.push({
|
|
5157
|
-
field: `colors.${key}`,
|
|
5158
|
-
source
|
|
5159
|
-
});
|
|
5160
|
-
}
|
|
5161
|
-
}
|
|
5162
|
-
}
|
|
5163
|
-
function upsertSource(sources, field, source) {
|
|
5164
|
-
const existing = sources.find((entry) => entry.field === field);
|
|
5165
|
-
if (existing !== void 0) {
|
|
5166
|
-
existing.source = source;
|
|
5167
|
-
return;
|
|
5168
|
-
}
|
|
5169
|
-
sources.push({
|
|
5170
|
-
field,
|
|
5171
|
-
source
|
|
5172
|
-
});
|
|
5173
|
-
}
|
|
5174
|
-
async function readPackage(cwd) {
|
|
5175
|
-
const raw = await readMaybe(join(cwd, "package.json"));
|
|
5221
|
+
async function readPackage(files) {
|
|
5222
|
+
const raw = await files.read("package.json");
|
|
5176
5223
|
if (raw === void 0) return;
|
|
5177
5224
|
try {
|
|
5178
5225
|
const parsed = JSON.parse(raw);
|
|
@@ -5183,18 +5230,69 @@ async function readPackage(cwd) {
|
|
|
5183
5230
|
throw error;
|
|
5184
5231
|
}
|
|
5185
5232
|
}
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5233
|
+
function stripScope(name) {
|
|
5234
|
+
const parts = name.split("/");
|
|
5235
|
+
return parts[parts.length - 1] ?? name;
|
|
5236
|
+
}
|
|
5237
|
+
function repositoryUrl(repository) {
|
|
5238
|
+
if (typeof repository === "string") return normalizeGitUrl(repository);
|
|
5239
|
+
if (repository?.url !== void 0) return normalizeGitUrl(repository.url);
|
|
5240
|
+
}
|
|
5241
|
+
function normalizeGitUrl(url) {
|
|
5242
|
+
const ssh = /^git@([^:]+):(.+)$/.exec(url);
|
|
5243
|
+
if (ssh !== null && ssh[1] !== void 0 && ssh[2] !== void 0) return `https://${ssh[1]}/${ssh[2].replace(/\.git$/, "")}`;
|
|
5244
|
+
return url.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
5245
|
+
}
|
|
5246
|
+
/**
|
|
5247
|
+
* Dominant non-neutral color of a PNG logo, or undefined when the file cannot be read or
|
|
5248
|
+
* carries no color. Detection reports the color as not found rather than falling back to a
|
|
5249
|
+
* built-in default that would be presented to the user as "your brand".
|
|
5250
|
+
*/
|
|
5251
|
+
async function dominantLogoColor(files, path) {
|
|
5252
|
+
const buffer = await files.readBinary(path);
|
|
5253
|
+
if (buffer === void 0) return;
|
|
5254
|
+
const png = decodePng(Buffer.from(buffer));
|
|
5255
|
+
if (png === void 0) return;
|
|
5256
|
+
return dominantNonNeutralColor(png);
|
|
5257
|
+
}
|
|
5258
|
+
|
|
5259
|
+
//#endregion
|
|
5260
|
+
//#region src/brand/detect-node.ts
|
|
5261
|
+
const execFileAsync = promisify(execFile);
|
|
5262
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5263
|
+
"node_modules",
|
|
5264
|
+
".git",
|
|
5265
|
+
"dist",
|
|
5266
|
+
"build",
|
|
5267
|
+
"coverage",
|
|
5268
|
+
".next",
|
|
5269
|
+
".astro",
|
|
5270
|
+
"out",
|
|
5271
|
+
"vendor"
|
|
5272
|
+
]);
|
|
5273
|
+
async function detectBrand(cwd) {
|
|
5274
|
+
return detectBrandFrom(diskFiles(cwd), cwd);
|
|
5275
|
+
}
|
|
5276
|
+
/** The disk implementation of the port, used by the CLI. Paths stay absolute here. */
|
|
5277
|
+
function diskFiles(cwd, maxDepth = 4) {
|
|
5278
|
+
return {
|
|
5279
|
+
list: () => listFiles(cwd, maxDepth),
|
|
5280
|
+
read: async (path) => {
|
|
5281
|
+
try {
|
|
5282
|
+
return await readFile(isAbsolute(path) ? path : join(cwd, path), "utf8");
|
|
5283
|
+
} catch {
|
|
5284
|
+
return;
|
|
5285
|
+
}
|
|
5286
|
+
},
|
|
5287
|
+
gitRemote: () => gitRemote(cwd),
|
|
5288
|
+
readBinary: async (path) => {
|
|
5289
|
+
try {
|
|
5290
|
+
return await readFile(isAbsolute(path) ? path : join(cwd, path));
|
|
5291
|
+
} catch {
|
|
5292
|
+
return;
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
};
|
|
5198
5296
|
}
|
|
5199
5297
|
async function listFiles(root, maxDepth) {
|
|
5200
5298
|
const out = [];
|
|
@@ -5220,50 +5318,17 @@ async function listFiles(root, maxDepth) {
|
|
|
5220
5318
|
await walk(root, maxDepth);
|
|
5221
5319
|
return out;
|
|
5222
5320
|
}
|
|
5223
|
-
async function
|
|
5224
|
-
try {
|
|
5225
|
-
return await readFile(path, "utf8");
|
|
5226
|
-
} catch {
|
|
5227
|
-
return;
|
|
5228
|
-
}
|
|
5229
|
-
}
|
|
5230
|
-
function stripScope(name) {
|
|
5231
|
-
const parts = name.split("/");
|
|
5232
|
-
return parts[parts.length - 1] ?? name;
|
|
5233
|
-
}
|
|
5234
|
-
function repositoryUrl(repository) {
|
|
5235
|
-
if (typeof repository === "string") return normalizeGitUrl(repository);
|
|
5236
|
-
if (repository?.url !== void 0) return normalizeGitUrl(repository.url);
|
|
5237
|
-
}
|
|
5238
|
-
function normalizeGitUrl(url) {
|
|
5239
|
-
const ssh = /^git@([^:]+):(.+)$/.exec(url);
|
|
5240
|
-
if (ssh !== null && ssh[1] !== void 0 && ssh[2] !== void 0) return `https://${ssh[1]}/${ssh[2].replace(/\.git$/, "")}`;
|
|
5241
|
-
return url.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
5242
|
-
}
|
|
5243
|
-
/**
|
|
5244
|
-
* Dominant non-neutral color of a PNG logo, or undefined when the file cannot be read or
|
|
5245
|
-
* carries no color. Detection reports the color as not found rather than falling back to a
|
|
5246
|
-
* built-in default that would be presented to the user as "your brand".
|
|
5247
|
-
*/
|
|
5248
|
-
async function dominantLogoColor(path) {
|
|
5249
|
-
let buffer;
|
|
5321
|
+
async function gitRemote(cwd) {
|
|
5250
5322
|
try {
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
const re = /(?:fill|stroke)="([^"]+)"/g;
|
|
5261
|
-
let match;
|
|
5262
|
-
while ((match = re.exec(svg)) !== null) {
|
|
5263
|
-
const raw = match[1];
|
|
5264
|
-
if (raw === void 0 || raw === "none" || raw === "currentColor") continue;
|
|
5265
|
-
const hex = parseCssColor(raw);
|
|
5266
|
-
if (hex !== void 0 && !isNeutralHex(hex)) return hex;
|
|
5323
|
+
const { stdout } = await execFileAsync("git", [
|
|
5324
|
+
"remote",
|
|
5325
|
+
"get-url",
|
|
5326
|
+
"origin"
|
|
5327
|
+
], { cwd });
|
|
5328
|
+
return normalizeGitUrl(stdout.trim());
|
|
5329
|
+
} catch (error) {
|
|
5330
|
+
if (error instanceof Error) return;
|
|
5331
|
+
throw error;
|
|
5267
5332
|
}
|
|
5268
5333
|
}
|
|
5269
5334
|
|