@zenginui/registry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/dist/brand.d.ts +61 -0
- package/dist/brand.js +292 -0
- package/dist/build.d.ts +14 -0
- package/dist/build.js +344 -0
- package/dist/color.d.ts +24 -0
- package/dist/color.js +88 -0
- package/dist/create.d.ts +55 -0
- package/dist/create.js +437 -0
- package/dist/fonts.d.ts +42 -0
- package/dist/fonts.js +132 -0
- package/dist/html.d.ts +16 -0
- package/dist/html.js +63 -0
- package/dist/icons.d.ts +43 -0
- package/dist/icons.js +197 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/install.d.ts +34 -0
- package/dist/install.js +106 -0
- package/dist/load.d.ts +11 -0
- package/dist/load.js +70 -0
- package/dist/resolve.d.ts +7 -0
- package/dist/resolve.js +31 -0
- package/dist/schema.d.ts +71 -0
- package/dist/schema.js +13 -0
- package/dist/theme.d.ts +26 -0
- package/dist/theme.js +35 -0
- package/dist/tokens.d.ts +14 -0
- package/dist/tokens.js +44 -0
- package/dist/upgrade.d.ts +60 -0
- package/dist/upgrade.js +171 -0
- package/package.json +53 -0
package/dist/html.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const MARK = "data-zengin";
|
|
4
|
+
/**
|
|
5
|
+
* Edits index.html in place: the title, the theme-color meta, the icon, and one fonts link the CLI owns
|
|
6
|
+
* (marked with data-zengin so a later theme replaces it, never duplicates it). Returns false when there
|
|
7
|
+
* is no index.html to patch.
|
|
8
|
+
*/
|
|
9
|
+
export function patchIndexHtml(projectDir, patch) {
|
|
10
|
+
// A Vite project has index.html; a Next project has the root layout, whose <head> holds the same tags.
|
|
11
|
+
const p = existsSync(join(projectDir, "index.html")) ? join(projectDir, "index.html") : join(projectDir, "src", "app", "layout.tsx");
|
|
12
|
+
if (!existsSync(p))
|
|
13
|
+
return false;
|
|
14
|
+
const layout = p.endsWith("layout.tsx");
|
|
15
|
+
let html = readFileSync(p, "utf8");
|
|
16
|
+
if (patch.title !== undefined) {
|
|
17
|
+
if (layout) {
|
|
18
|
+
html = html.replace(/(export const metadata[^=]*=\s*\{[^}]*title:\s*)"[^"]*"/, `$1${JSON.stringify(patch.title)}`);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const title = `<title>${escapeHtml(patch.title)}</title>`;
|
|
22
|
+
html = /<title>[\s\S]*?<\/title>/.test(html) ? html.replace(/<title>[\s\S]*?<\/title>/, title) : insertInHead(html, title);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (patch.themeColor !== undefined) {
|
|
26
|
+
const meta = `<meta name="theme-color" content="${patch.themeColor}" />`;
|
|
27
|
+
html = /<meta\s+name="theme-color"[^>]*>/.test(html) ? html.replace(/<meta\s+name="theme-color"[^>]*>/, meta) : insertInHead(html, meta);
|
|
28
|
+
}
|
|
29
|
+
if (patch.icon !== undefined) {
|
|
30
|
+
const link = `<link rel="icon" href="${patch.icon}" />`;
|
|
31
|
+
html = /<link\s+rel="icon"[^>]*>/.test(html) ? html.replace(/<link\s+rel="icon"[^>]*>/, link) : insertInHead(html, link);
|
|
32
|
+
}
|
|
33
|
+
if (patch.fonts !== undefined) {
|
|
34
|
+
const owned = new RegExp(`\\s*<link[^>]*${MARK}="fonts"[^>]*>`, "g");
|
|
35
|
+
html = html.replace(owned, "");
|
|
36
|
+
if (patch.fonts) {
|
|
37
|
+
// In a React 19 layout a stylesheet link needs `precedence`, or Next's prerender fails on the hoisting.
|
|
38
|
+
const link = `<link rel="stylesheet" href="${patch.fonts}" ${MARK}="fonts"${layout ? ' precedence="default"' : ""} />`;
|
|
39
|
+
html = insertInHead(html, link);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
writeFileSync(p, html);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/** The Google Fonts css2 href for the families, at the four weights components use unless `Family:400;700` pins them. */
|
|
46
|
+
export function fontsHref(families) {
|
|
47
|
+
const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
|
|
48
|
+
const params = unique
|
|
49
|
+
.map((f) => {
|
|
50
|
+
const [family, weights] = f.split(":");
|
|
51
|
+
return `family=${encodeURIComponent(family.trim()).replace(/%20/g, "+")}:wght@${(weights ?? "400;500;600;700").trim()}`;
|
|
52
|
+
})
|
|
53
|
+
.join("&");
|
|
54
|
+
return `https://fonts.googleapis.com/css2?${params}&display=swap`;
|
|
55
|
+
}
|
|
56
|
+
function insertInHead(html, tag) {
|
|
57
|
+
if (/<\/head>/.test(html))
|
|
58
|
+
return html.replace(/(\s*)<\/head>/, `\n ${tag}$1</head>`);
|
|
59
|
+
return `${tag}\n${html}`;
|
|
60
|
+
}
|
|
61
|
+
function escapeHtml(s) {
|
|
62
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
63
|
+
}
|
package/dist/icons.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RegistrySource } from "./load.js";
|
|
2
|
+
/**
|
|
3
|
+
* Icon sets. The vocabulary is Zengin UI's: sixty-odd names every component and template draws by.
|
|
4
|
+
* A set maps each name to an export of one react-icons module, and `zengin icons <set>` rewrites
|
|
5
|
+
* src/lib/icons.tsx so the names come from that set. App code never imports react-icons directly; the
|
|
6
|
+
* Icon manifest shadows it, and the engine says so.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ICON_NAMES: readonly ["Search", "Close", "Check", "ChevronDown", "ChevronUp", "ChevronLeft", "ChevronRight", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Plus", "Minus", "Trash", "Edit", "Settings", "Menu", "More", "Home", "User", "Users", "Bell", "Info", "Warning", "Error", "Success", "Help", "Copy", "ExternalLink", "Download", "Upload", "Filter", "Sun", "Moon", "Loader", "Grid", "CreditCard", "Mail", "Calendar", "Clock", "Eye", "EyeOff", "Lock", "LogOut", "Star", "Send", "Paperclip", "Sparkles", "Link", "Refresh", "Globe", "File", "Folder", "Tag", "Inbox", "Message", "Code", "Terminal", "Chart", "Layers", "Database", "Shield", "Drag"];
|
|
9
|
+
export type IconName = (typeof ICON_NAMES)[number];
|
|
10
|
+
export interface IconSetSpec {
|
|
11
|
+
title: string;
|
|
12
|
+
description: string;
|
|
13
|
+
/** The react-icons module, e.g. `react-icons/lu`. */
|
|
14
|
+
module: string;
|
|
15
|
+
/** Vocabulary name -> export name in the module. */
|
|
16
|
+
names: Record<IconName, string>;
|
|
17
|
+
}
|
|
18
|
+
export declare const ICON_SETS: Record<string, IconSetSpec>;
|
|
19
|
+
export declare const REACT_ICONS_VERSION = "^5.5.0";
|
|
20
|
+
/** src/lib/icons.tsx for a set: the same `Icon` object and types the default file exports, drawn by react-icons. */
|
|
21
|
+
export declare function renderIconsModule(name: string, set: IconSetSpec): string;
|
|
22
|
+
export interface IconsSummary {
|
|
23
|
+
name: string;
|
|
24
|
+
title: string;
|
|
25
|
+
description: string;
|
|
26
|
+
module: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function listIconSets(source: RegistrySource): Promise<IconsSummary[]>;
|
|
29
|
+
export interface ApplyIconsResult {
|
|
30
|
+
name: string;
|
|
31
|
+
module: string;
|
|
32
|
+
files: string[];
|
|
33
|
+
/** npm packages the project now needs; the caller says how to install them. */
|
|
34
|
+
dependencies: Record<string, string>;
|
|
35
|
+
/** The file that was replaced, when the project already had one. */
|
|
36
|
+
replaced: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Points the project's icon vocabulary at a set: rewrites src/lib/icons.tsx and records the react-icons dependency. */
|
|
39
|
+
export declare function applyIcons(opts: {
|
|
40
|
+
projectDir: string;
|
|
41
|
+
name: string;
|
|
42
|
+
source: RegistrySource;
|
|
43
|
+
}): Promise<ApplyIconsResult>;
|
package/dist/icons.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { installItems } from "./install.js";
|
|
4
|
+
import { resolveItems } from "./resolve.js";
|
|
5
|
+
import { LAYOUT } from "./schema.js";
|
|
6
|
+
/**
|
|
7
|
+
* Icon sets. The vocabulary is Zengin UI's: sixty-odd names every component and template draws by.
|
|
8
|
+
* A set maps each name to an export of one react-icons module, and `zengin icons <set>` rewrites
|
|
9
|
+
* src/lib/icons.tsx so the names come from that set. App code never imports react-icons directly; the
|
|
10
|
+
* Icon manifest shadows it, and the engine says so.
|
|
11
|
+
*/
|
|
12
|
+
export const ICON_NAMES = [
|
|
13
|
+
"Search", "Close", "Check", "ChevronDown", "ChevronUp", "ChevronLeft", "ChevronRight", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight",
|
|
14
|
+
"Plus", "Minus", "Trash", "Edit", "Settings", "Menu", "More", "Home", "User", "Users", "Bell", "Info", "Warning", "Error", "Success", "Help",
|
|
15
|
+
"Copy", "ExternalLink", "Download", "Upload", "Filter", "Sun", "Moon", "Loader", "Grid", "CreditCard", "Mail", "Calendar", "Clock", "Eye", "EyeOff",
|
|
16
|
+
"Lock", "LogOut", "Star", "Send", "Paperclip", "Sparkles", "Link", "Refresh", "Globe", "File", "Folder", "Tag", "Inbox", "Message", "Code", "Terminal",
|
|
17
|
+
"Chart", "Layers", "Database", "Shield", "Drag",
|
|
18
|
+
];
|
|
19
|
+
const names = (prefix, map) => Object.fromEntries(Object.entries(map).map(([k, v]) => [k, prefix + v]));
|
|
20
|
+
export const ICON_SETS = {
|
|
21
|
+
lucide: {
|
|
22
|
+
title: "Lucide",
|
|
23
|
+
description: "The set most React apps already use: even 2px strokes, round joins, one voice across a thousand glyphs.",
|
|
24
|
+
module: "react-icons/lu",
|
|
25
|
+
names: names("Lu", {
|
|
26
|
+
Search: "Search", Close: "X", Check: "Check", ChevronDown: "ChevronDown", ChevronUp: "ChevronUp", ChevronLeft: "ChevronLeft", ChevronRight: "ChevronRight",
|
|
27
|
+
ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", Plus: "Plus", Minus: "Minus", Trash: "Trash2", Edit: "Pencil",
|
|
28
|
+
Settings: "Settings", Menu: "Menu", More: "Ellipsis", Home: "House", User: "User", Users: "Users", Bell: "Bell", Info: "Info", Warning: "TriangleAlert",
|
|
29
|
+
Error: "CircleX", Success: "CircleCheck", Help: "CircleHelp", Copy: "Copy", ExternalLink: "ExternalLink", Download: "Download", Upload: "Upload", Filter: "Filter",
|
|
30
|
+
Sun: "Sun", Moon: "Moon", Loader: "Loader", Grid: "LayoutGrid", CreditCard: "CreditCard", Mail: "Mail", Calendar: "Calendar", Clock: "Clock", Eye: "Eye",
|
|
31
|
+
EyeOff: "EyeOff", Lock: "Lock", LogOut: "LogOut", Star: "Star", Send: "Send", Paperclip: "Paperclip", Sparkles: "Sparkles", Link: "Link", Refresh: "RefreshCw",
|
|
32
|
+
Globe: "Globe", File: "File", Folder: "Folder", Tag: "Tag", Inbox: "Inbox", Message: "MessageSquare", Code: "Code", Terminal: "Terminal", Chart: "ChartBar",
|
|
33
|
+
Layers: "Layers", Database: "Database", Shield: "Shield", Drag: "GripVertical",
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
tabler: {
|
|
37
|
+
title: "Tabler",
|
|
38
|
+
description: "Thin, wide, a little technical. Four thousand glyphs on a 24 grid; suits dashboards and dense tables.",
|
|
39
|
+
module: "react-icons/tb",
|
|
40
|
+
names: names("Tb", {
|
|
41
|
+
Search: "Search", Close: "X", Check: "Check", ChevronDown: "ChevronDown", ChevronUp: "ChevronUp", ChevronLeft: "ChevronLeft", ChevronRight: "ChevronRight",
|
|
42
|
+
ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", Plus: "Plus", Minus: "Minus", Trash: "Trash", Edit: "Pencil",
|
|
43
|
+
Settings: "Settings", Menu: "Menu2", More: "Dots", Home: "Home", User: "User", Users: "Users", Bell: "Bell", Info: "InfoCircle", Warning: "AlertTriangle",
|
|
44
|
+
Error: "AlertCircle", Success: "CircleCheck", Help: "HelpCircle", Copy: "Copy", ExternalLink: "ExternalLink", Download: "Download", Upload: "Upload", Filter: "Filter",
|
|
45
|
+
Sun: "Sun", Moon: "Moon", Loader: "Loader2", Grid: "LayoutGrid", CreditCard: "CreditCard", Mail: "Mail", Calendar: "Calendar", Clock: "Clock", Eye: "Eye",
|
|
46
|
+
EyeOff: "EyeOff", Lock: "Lock", LogOut: "Logout", Star: "Star", Send: "Send", Paperclip: "Paperclip", Sparkles: "Sparkles", Link: "Link", Refresh: "Refresh",
|
|
47
|
+
Globe: "World", File: "File", Folder: "Folder", Tag: "Tag", Inbox: "Inbox", Message: "Message", Code: "Code", Terminal: "Terminal2", Chart: "ChartBar",
|
|
48
|
+
Layers: "Stack2", Database: "Database", Shield: "Shield", Drag: "GripVertical",
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
phosphor: {
|
|
52
|
+
title: "Phosphor",
|
|
53
|
+
description: "Friendly and geometric, with a bit more curve than Lucide. Regular weight here; the family has five more.",
|
|
54
|
+
module: "react-icons/pi",
|
|
55
|
+
names: names("Pi", {
|
|
56
|
+
Search: "MagnifyingGlass", Close: "X", Check: "Check", ChevronDown: "CaretDown", ChevronUp: "CaretUp", ChevronLeft: "CaretLeft", ChevronRight: "CaretRight",
|
|
57
|
+
ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", Plus: "Plus", Minus: "Minus", Trash: "Trash", Edit: "PencilSimple",
|
|
58
|
+
Settings: "Gear", Menu: "List", More: "DotsThree", Home: "House", User: "User", Users: "Users", Bell: "Bell", Info: "Info", Warning: "Warning",
|
|
59
|
+
Error: "WarningCircle", Success: "CheckCircle", Help: "Question", Copy: "Copy", ExternalLink: "ArrowSquareOut", Download: "DownloadSimple", Upload: "UploadSimple",
|
|
60
|
+
Filter: "Funnel", Sun: "Sun", Moon: "Moon", Loader: "Spinner", Grid: "SquaresFour", CreditCard: "CreditCard", Mail: "Envelope", Calendar: "Calendar", Clock: "Clock",
|
|
61
|
+
Eye: "Eye", EyeOff: "EyeSlash", Lock: "Lock", LogOut: "SignOut", Star: "Star", Send: "PaperPlaneRight", Paperclip: "Paperclip", Sparkles: "Sparkle", Link: "Link",
|
|
62
|
+
Refresh: "ArrowsClockwise", Globe: "Globe", File: "File", Folder: "Folder", Tag: "Tag", Inbox: "Tray", Message: "ChatCircle", Code: "Code", Terminal: "Terminal",
|
|
63
|
+
Chart: "ChartBar", Layers: "Stack", Database: "Database", Shield: "Shield", Drag: "DotsSixVertical",
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
heroicons: {
|
|
67
|
+
title: "Heroicons",
|
|
68
|
+
description: "Tailwind's set, outline weight. Slightly heavier strokes and rounder forms; reads well at 20 and 24.",
|
|
69
|
+
module: "react-icons/hi2",
|
|
70
|
+
names: names("HiOutline", {
|
|
71
|
+
Search: "MagnifyingGlass", Close: "XMark", Check: "Check", ChevronDown: "ChevronDown", ChevronUp: "ChevronUp", ChevronLeft: "ChevronLeft", ChevronRight: "ChevronRight",
|
|
72
|
+
ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", Plus: "Plus", Minus: "Minus", Trash: "Trash", Edit: "Pencil",
|
|
73
|
+
Settings: "Cog6Tooth", Menu: "Bars3", More: "EllipsisHorizontal", Home: "Home", User: "User", Users: "Users", Bell: "Bell", Info: "InformationCircle",
|
|
74
|
+
Warning: "ExclamationTriangle", Error: "ExclamationCircle", Success: "CheckCircle", Help: "QuestionMarkCircle", Copy: "DocumentDuplicate", ExternalLink: "ArrowTopRightOnSquare",
|
|
75
|
+
Download: "ArrowDownTray", Upload: "ArrowUpTray", Filter: "Funnel", Sun: "Sun", Moon: "Moon", Loader: "ArrowPath", Grid: "Squares2X2", CreditCard: "CreditCard",
|
|
76
|
+
Mail: "Envelope", Calendar: "Calendar", Clock: "Clock", Eye: "Eye", EyeOff: "EyeSlash", Lock: "LockClosed", LogOut: "ArrowRightOnRectangle", Star: "Star",
|
|
77
|
+
Send: "PaperAirplane", Paperclip: "PaperClip", Sparkles: "Sparkles", Link: "Link", Refresh: "ArrowPath", Globe: "GlobeAlt", File: "Document", Folder: "Folder",
|
|
78
|
+
Tag: "Tag", Inbox: "Inbox", Message: "ChatBubbleLeft", Code: "CodeBracket", Terminal: "CommandLine", Chart: "ChartBar", Layers: "Square3Stack3D",
|
|
79
|
+
Database: "CircleStack", Shield: "ShieldCheck", Drag: "EllipsisVertical",
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
feather: {
|
|
83
|
+
title: "Feather",
|
|
84
|
+
description: "The original minimal line set: 2px, 24 grid, nothing extra. Fewer glyphs, every one of them calm.",
|
|
85
|
+
module: "react-icons/fi",
|
|
86
|
+
names: names("Fi", {
|
|
87
|
+
Search: "Search", Close: "X", Check: "Check", ChevronDown: "ChevronDown", ChevronUp: "ChevronUp", ChevronLeft: "ChevronLeft", ChevronRight: "ChevronRight",
|
|
88
|
+
ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", Plus: "Plus", Minus: "Minus", Trash: "Trash2", Edit: "Edit2",
|
|
89
|
+
Settings: "Settings", Menu: "Menu", More: "MoreHorizontal", Home: "Home", User: "User", Users: "Users", Bell: "Bell", Info: "Info", Warning: "AlertTriangle",
|
|
90
|
+
Error: "AlertCircle", Success: "CheckCircle", Help: "HelpCircle", Copy: "Copy", ExternalLink: "ExternalLink", Download: "Download", Upload: "Upload", Filter: "Filter",
|
|
91
|
+
Sun: "Sun", Moon: "Moon", Loader: "Loader", Grid: "Grid", CreditCard: "CreditCard", Mail: "Mail", Calendar: "Calendar", Clock: "Clock", Eye: "Eye", EyeOff: "EyeOff",
|
|
92
|
+
Lock: "Lock", LogOut: "LogOut", Star: "Star", Send: "Send", Paperclip: "Paperclip", Sparkles: "Zap", Link: "Link", Refresh: "RefreshCw", Globe: "Globe", File: "File",
|
|
93
|
+
Folder: "Folder", Tag: "Tag", Inbox: "Inbox", Message: "MessageSquare", Code: "Code", Terminal: "Terminal", Chart: "BarChart2", Layers: "Layers", Database: "Database",
|
|
94
|
+
Shield: "Shield", Drag: "MoreVertical",
|
|
95
|
+
}),
|
|
96
|
+
},
|
|
97
|
+
radix: {
|
|
98
|
+
title: "Radix",
|
|
99
|
+
description: "15-pixel glyphs drawn for dense interfaces: crisp at small sizes, the set Radix Themes ships.",
|
|
100
|
+
module: "react-icons/rx",
|
|
101
|
+
names: names("Rx", {
|
|
102
|
+
Search: "MagnifyingGlass", Close: "Cross2", Check: "Check", ChevronDown: "ChevronDown", ChevronUp: "ChevronUp", ChevronLeft: "ChevronLeft", ChevronRight: "ChevronRight",
|
|
103
|
+
ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", Plus: "Plus", Minus: "Minus", Trash: "Trash", Edit: "Pencil1",
|
|
104
|
+
Settings: "Gear", Menu: "HamburgerMenu", More: "DotsHorizontal", Home: "Home", User: "Person", Users: "Person", Bell: "Bell", Info: "InfoCircled",
|
|
105
|
+
Warning: "ExclamationTriangle", Error: "CrossCircled", Success: "CheckCircled", Help: "QuestionMarkCircled", Copy: "Copy", ExternalLink: "ExternalLink",
|
|
106
|
+
Download: "Download", Upload: "Upload", Filter: "MixerHorizontal", Sun: "Sun", Moon: "Moon", Loader: "Update", Grid: "Grid", CreditCard: "IdCard", Mail: "EnvelopeClosed",
|
|
107
|
+
Calendar: "Calendar", Clock: "Clock", Eye: "EyeOpen", EyeOff: "EyeClosed", Lock: "LockClosed", LogOut: "Exit", Star: "Star", Send: "PaperPlane", Paperclip: "Link2",
|
|
108
|
+
Sparkles: "MagicWand", Link: "Link1", Refresh: "Reload", Globe: "Globe", File: "FileText", Folder: "Archive", Tag: "Bookmark", Inbox: "EnvelopeOpen",
|
|
109
|
+
Message: "ChatBubble", Code: "Code", Terminal: "Code", Chart: "BarChart", Layers: "Layers", Database: "Archive", Shield: "LockClosed", Drag: "DragHandleDots2",
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
material: {
|
|
113
|
+
title: "Material",
|
|
114
|
+
description: "Google's filled set, outlined where a hollow reads better. Heavier than the line sets; matches Material apps.",
|
|
115
|
+
module: "react-icons/md",
|
|
116
|
+
names: names("Md", {
|
|
117
|
+
Search: "Search", Close: "Close", Check: "Check", ChevronDown: "ExpandMore", ChevronUp: "ExpandLess", ChevronLeft: "ChevronLeft", ChevronRight: "ChevronRight",
|
|
118
|
+
ArrowUp: "ArrowUpward", ArrowDown: "ArrowDownward", ArrowLeft: "ArrowBack", ArrowRight: "ArrowForward", Plus: "Add", Minus: "Remove", Trash: "DeleteOutline", Edit: "Edit",
|
|
119
|
+
Settings: "Settings", Menu: "Menu", More: "MoreHoriz", Home: "Home", User: "Person", Users: "People", Bell: "Notifications", Info: "InfoOutline", Warning: "WarningAmber",
|
|
120
|
+
Error: "ErrorOutline", Success: "CheckCircleOutline", Help: "HelpOutline", Copy: "ContentCopy", ExternalLink: "OpenInNew", Download: "Download", Upload: "Upload",
|
|
121
|
+
Filter: "FilterList", Sun: "LightMode", Moon: "DarkMode", Loader: "Refresh", Grid: "GridView", CreditCard: "CreditCard", Mail: "MailOutline", Calendar: "CalendarToday",
|
|
122
|
+
Clock: "AccessTime", Eye: "Visibility", EyeOff: "VisibilityOff", Lock: "LockOutline", LogOut: "Logout", Star: "StarBorder", Send: "Send", Paperclip: "AttachFile",
|
|
123
|
+
Sparkles: "AutoAwesome", Link: "Link", Refresh: "Refresh", Globe: "Public", File: "InsertDriveFile", Folder: "Folder", Tag: "LabelOutline", Inbox: "Inbox",
|
|
124
|
+
Message: "ChatBubbleOutline", Code: "Code", Terminal: "Terminal", Chart: "BarChart", Layers: "Layers", Database: "Storage", Shield: "Shield", Drag: "DragIndicator",
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
bootstrap: {
|
|
128
|
+
title: "Bootstrap",
|
|
129
|
+
description: "Bootstrap's own set: fine 1px-ish strokes on a 16 grid, drawn small first. Quiet next to text.",
|
|
130
|
+
module: "react-icons/bs",
|
|
131
|
+
names: names("Bs", {
|
|
132
|
+
Search: "Search", Close: "X", Check: "Check", ChevronDown: "ChevronDown", ChevronUp: "ChevronUp", ChevronLeft: "ChevronLeft", ChevronRight: "ChevronRight",
|
|
133
|
+
ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", Plus: "Plus", Minus: "Dash", Trash: "Trash", Edit: "Pencil",
|
|
134
|
+
Settings: "Gear", Menu: "List", More: "ThreeDots", Home: "House", User: "Person", Users: "People", Bell: "Bell", Info: "InfoCircle", Warning: "ExclamationTriangle",
|
|
135
|
+
Error: "ExclamationCircle", Success: "CheckCircle", Help: "QuestionCircle", Copy: "Copy", ExternalLink: "BoxArrowUpRight", Download: "Download", Upload: "Upload",
|
|
136
|
+
Filter: "Funnel", Sun: "Sun", Moon: "Moon", Loader: "ArrowRepeat", Grid: "Grid", CreditCard: "CreditCard", Mail: "Envelope", Calendar: "Calendar", Clock: "Clock",
|
|
137
|
+
Eye: "Eye", EyeOff: "EyeSlash", Lock: "Lock", LogOut: "BoxArrowRight", Star: "Star", Send: "Send", Paperclip: "Paperclip", Sparkles: "Stars", Link: "Link45Deg",
|
|
138
|
+
Refresh: "ArrowClockwise", Globe: "Globe", File: "FileEarmark", Folder: "Folder", Tag: "Tag", Inbox: "Inbox", Message: "Chat", Code: "Code", Terminal: "Terminal",
|
|
139
|
+
Chart: "BarChart", Layers: "Layers", Database: "Database", Shield: "Shield", Drag: "GripVertical",
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
export const REACT_ICONS_VERSION = "^5.5.0";
|
|
144
|
+
/** src/lib/icons.tsx for a set: the same `Icon` object and types the default file exports, drawn by react-icons. */
|
|
145
|
+
export function renderIconsModule(name, set) {
|
|
146
|
+
const imports = [...new Set(Object.values(set.names))].sort();
|
|
147
|
+
const entries = ICON_NAMES.map((n) => ` ${n}: ${set.names[n]},`);
|
|
148
|
+
return `import type { IconBaseProps } from "react-icons";
|
|
149
|
+
import { ${imports.join(", ")} } from "${set.module}";
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The icon vocabulary, drawn by ${set.title} (${set.module}) since \`zengin icons ${name}\`. Components and app code
|
|
153
|
+
* draw by name: <Icon.Search />. Run \`zengin icons <set>\` again to change every icon at once, or edit a line
|
|
154
|
+
* here to swap one glyph. Sized by font-size (1em) unless \`size\` says otherwise, like every react-icons glyph.
|
|
155
|
+
*/
|
|
156
|
+
export const Icon = {
|
|
157
|
+
${entries.join("\n")}
|
|
158
|
+
} as const;
|
|
159
|
+
|
|
160
|
+
export type IconName = keyof typeof Icon;
|
|
161
|
+
export const iconNames = Object.keys(Icon) as IconName[];
|
|
162
|
+
export type IconProps = IconBaseProps;
|
|
163
|
+
export type IconComponent = (typeof Icon)[IconName];
|
|
164
|
+
export type IconSet = Partial<Record<IconName, IconComponent>>;
|
|
165
|
+
|
|
166
|
+
/** Kept for API parity with the default module; a project on a fixed set has nothing to swap at runtime. */
|
|
167
|
+
export function setIconSet(_set: IconSet): void {}
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
export async function listIconSets(source) {
|
|
171
|
+
const index = await source.index();
|
|
172
|
+
return index.items.filter((i) => i.type === "icons" && i.iconSet).map((i) => ({ name: i.name.replace(/^icons-/, ""), title: i.title, description: i.description, module: i.iconSet.module }));
|
|
173
|
+
}
|
|
174
|
+
/** Points the project's icon vocabulary at a set: rewrites src/lib/icons.tsx and records the react-icons dependency. */
|
|
175
|
+
export async function applyIcons(opts) {
|
|
176
|
+
const dir = resolve(opts.projectDir);
|
|
177
|
+
if (!existsSync(join(dir, "zengin.config.yaml")))
|
|
178
|
+
throw new Error(`${dir} has no zengin.config.yaml. Run zengin icons inside a project made by zengin create, or pass --dir.`);
|
|
179
|
+
const index = await opts.source.index();
|
|
180
|
+
const itemName = `icons-${opts.name.replace(/^icons-/, "")}`;
|
|
181
|
+
const summary = index.items.find((i) => i.name === itemName && i.type === "icons");
|
|
182
|
+
if (!summary) {
|
|
183
|
+
const sets = index.items.filter((i) => i.type === "icons").map((i) => i.name.replace(/^icons-/, ""));
|
|
184
|
+
throw new Error(`No icon set "${opts.name}". Sets: ${sets.join(", ")}.`);
|
|
185
|
+
}
|
|
186
|
+
const target = join(dir, LAYOUT.libDir, "icons.tsx");
|
|
187
|
+
const replaced = existsSync(target);
|
|
188
|
+
const items = await resolveItems(opts.source, [itemName]);
|
|
189
|
+
const r = installItems({ projectDir: dir, items, version: index.version, force: true });
|
|
190
|
+
const pkgPath = join(dir, "package.json");
|
|
191
|
+
if (existsSync(pkgPath)) {
|
|
192
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
193
|
+
pkg.dependencies = { ...pkg.dependencies, ...r.dependencies };
|
|
194
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
195
|
+
}
|
|
196
|
+
return { name: opts.name.replace(/^icons-/, ""), module: summary.iconSet.module, files: r.written, dependencies: r.dependencies, replaced };
|
|
197
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { buildRegistry, writeRegistry, kebab, pascal } from "./build.js";
|
|
2
|
+
export { openRegistry, registryFromMemory, DEFAULT_REGISTRY, type RegistrySource } from "./load.js";
|
|
3
|
+
export { resolveItems } from "./resolve.js";
|
|
4
|
+
export { installItems, withPragma, stripPragma, contentHash, type InstallResult } from "./install.js";
|
|
5
|
+
export { planUpgrade, applyUpgrade, diffLines, type UpgradePlan, type UpgradeEntry, type UpgradeState, type ApplyResult } from "./upgrade.js";
|
|
6
|
+
export { createProject, VERSIONS, type CreateOptions, type CreateResult } from "./create.js";
|
|
7
|
+
export { buildTokensCss, writeTokensCss } from "./tokens.js";
|
|
8
|
+
export { applyTheme, listThemes, type ApplyThemeResult, type ThemeSummary } from "./theme.js";
|
|
9
|
+
export { applyIcons, listIconSets, renderIconsModule, ICON_NAMES, ICON_SETS, type ApplyIconsResult, type IconsSummary, type IconSetSpec } from "./icons.js";
|
|
10
|
+
export { applyFonts, listFonts, applyPairingToCss, pairingCss, pairingFamilies, type ApplyFontsResult, type FontsSummary } from "./fonts.js";
|
|
11
|
+
export { brandProject, derivePalette, paletteContrast, renderBrandCss, primaryFromSvg, faviconSvg, type BrandOptions, type BrandResult, type BrandRadius, type Palette } from "./brand.js";
|
|
12
|
+
export { contrast, hexToOklch, oklchToHex, pushForContrast, type Oklch } from "./color.js";
|
|
13
|
+
export { patchIndexHtml, fontsHref, type HtmlPatch } from "./html.js";
|
|
14
|
+
export { LAYOUT, REGISTRY_SCHEMA, isRegistryIndex, type Registry, type RegistryIndex, type RegistryItem, type RegistryFile, type ItemType, type FileKind, type FontPairing, type FontRole } from "./schema.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { buildRegistry, writeRegistry, kebab, pascal } from "./build.js";
|
|
2
|
+
export { openRegistry, registryFromMemory, DEFAULT_REGISTRY } from "./load.js";
|
|
3
|
+
export { resolveItems } from "./resolve.js";
|
|
4
|
+
export { installItems, withPragma, stripPragma, contentHash } from "./install.js";
|
|
5
|
+
export { planUpgrade, applyUpgrade, diffLines } from "./upgrade.js";
|
|
6
|
+
export { createProject, VERSIONS } from "./create.js";
|
|
7
|
+
export { buildTokensCss, writeTokensCss } from "./tokens.js";
|
|
8
|
+
export { applyTheme, listThemes } from "./theme.js";
|
|
9
|
+
export { applyIcons, listIconSets, renderIconsModule, ICON_NAMES, ICON_SETS } from "./icons.js";
|
|
10
|
+
export { applyFonts, listFonts, applyPairingToCss, pairingCss, pairingFamilies } from "./fonts.js";
|
|
11
|
+
export { brandProject, derivePalette, paletteContrast, renderBrandCss, primaryFromSvg, faviconSvg } from "./brand.js";
|
|
12
|
+
export { contrast, hexToOklch, oklchToHex, pushForContrast } from "./color.js";
|
|
13
|
+
export { patchIndexHtml, fontsHref } from "./html.js";
|
|
14
|
+
export { LAYOUT, REGISTRY_SCHEMA, isRegistryIndex } from "./schema.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ComponentManifest } from "@zenginui/engine";
|
|
2
|
+
import { type RegistryItem } from "./schema.js";
|
|
3
|
+
export interface InstallResult {
|
|
4
|
+
/** Project-relative paths written, in order. */
|
|
5
|
+
written: string[];
|
|
6
|
+
/** Project-relative paths that existed and were left alone (pass `force` to overwrite). */
|
|
7
|
+
skipped: string[];
|
|
8
|
+
dependencies: Record<string, string>;
|
|
9
|
+
devDependencies: Record<string, string>;
|
|
10
|
+
/** Component names now in the manifest. */
|
|
11
|
+
components: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Writes resolved items into a project: component files with the owned pragma, the manifest entry merged
|
|
15
|
+
* into zengin/components.json with `export.from` pointing at the alias, the stylesheet import, the barrel
|
|
16
|
+
* export, and the story. Templates write their files as-is. Existing files are kept unless `force`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function installItems(opts: {
|
|
19
|
+
projectDir: string;
|
|
20
|
+
items: RegistryItem[];
|
|
21
|
+
version: string;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
}): InstallResult;
|
|
24
|
+
/**
|
|
25
|
+
* The pragma the engine reads: the file is owned by the project, forked from this system version, and this
|
|
26
|
+
* is the hash of what was copied, so `zengin upgrade` can tell a local edit from an upstream change.
|
|
27
|
+
*/
|
|
28
|
+
export declare function withPragma(content: string, component: string, version: string): string;
|
|
29
|
+
/** The file without its pragma line, line endings normalized, so hashes compare across platforms. */
|
|
30
|
+
export declare function stripPragma(content: string): string;
|
|
31
|
+
export declare function contentHash(body: string): string;
|
|
32
|
+
export declare function mergeManifest(projectDir: string, entry: ComponentManifest): void;
|
|
33
|
+
export declare const STYLES_INDEX_HEAD = "/* Order matters: tokens, then foundation, then components. `zengin add` appends component stylesheets here. */\n@import \"generated/tokens.css\";\n@import \"base.css\";\n@import \"chart.css\";\n@import \"motion.css\";\n";
|
|
34
|
+
export declare const BARREL_HEAD: string;
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { LAYOUT } from "./schema.js";
|
|
5
|
+
/**
|
|
6
|
+
* Writes resolved items into a project: component files with the owned pragma, the manifest entry merged
|
|
7
|
+
* into zengin/components.json with `export.from` pointing at the alias, the stylesheet import, the barrel
|
|
8
|
+
* export, and the story. Templates write their files as-is. Existing files are kept unless `force`.
|
|
9
|
+
*/
|
|
10
|
+
export function installItems(opts) {
|
|
11
|
+
const { projectDir, items, version } = opts;
|
|
12
|
+
const result = { written: [], skipped: [], dependencies: {}, devDependencies: {}, components: [] };
|
|
13
|
+
const write = (rel, content, always = false) => {
|
|
14
|
+
const abs = join(projectDir, rel);
|
|
15
|
+
if (existsSync(abs) && !opts.force && !always) {
|
|
16
|
+
result.skipped.push(rel);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
20
|
+
writeFileSync(abs, content);
|
|
21
|
+
result.written.push(rel);
|
|
22
|
+
};
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
Object.assign(result.dependencies, item.dependencies);
|
|
25
|
+
Object.assign(result.devDependencies, item.devDependencies);
|
|
26
|
+
for (const f of item.files) {
|
|
27
|
+
// A component's own files, the .tsx and its stylesheet, carry the pragma; a story is the project's from the start.
|
|
28
|
+
const owned = item.manifest && (f.kind === "component" || (f.kind === "style" && f.path.startsWith(LAYOUT.componentsDir)));
|
|
29
|
+
const content = owned ? withPragma(f.content, item.manifest.name, version) : f.content;
|
|
30
|
+
write(f.path, content);
|
|
31
|
+
}
|
|
32
|
+
if (item.manifest) {
|
|
33
|
+
mergeManifest(projectDir, { ...item.manifest, export: item.type === "component" ? { ...item.manifest.export, from: LAYOUT.alias } : item.manifest.export });
|
|
34
|
+
const css = item.files.find((f) => f.kind === "style" && f.path.startsWith(LAYOUT.componentsDir));
|
|
35
|
+
if (css)
|
|
36
|
+
addStyleImport(projectDir, css.path);
|
|
37
|
+
// Templates import Icon from the package alias, as they did from @zenginui/ui: the barrel re-exports the vocabulary.
|
|
38
|
+
if (item.name === "lib-icons")
|
|
39
|
+
addBarrelLine(projectDir, `export * from "../../lib/icons";`);
|
|
40
|
+
else
|
|
41
|
+
addBarrelExport(projectDir, item.name);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const manifestPath = join(projectDir, LAYOUT.definitionsDir, "components.json");
|
|
45
|
+
if (existsSync(manifestPath)) {
|
|
46
|
+
result.components = JSON.parse(readFileSync(manifestPath, "utf8")).map((m) => m.name);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The pragma the engine reads: the file is owned by the project, forked from this system version, and this
|
|
52
|
+
* is the hash of what was copied, so `zengin upgrade` can tell a local edit from an upstream change.
|
|
53
|
+
*/
|
|
54
|
+
export function withPragma(content, component, version) {
|
|
55
|
+
const body = stripPragma(content);
|
|
56
|
+
return `/* zengin-owned ${component}, forked from @zenginui/ui@${version}, sha ${contentHash(body)} */\n${body}`;
|
|
57
|
+
}
|
|
58
|
+
/** The file without its pragma line, line endings normalized, so hashes compare across platforms. */
|
|
59
|
+
export function stripPragma(content) {
|
|
60
|
+
return content.replace(/^\/\* zengin-owned[^\n]*\*\/\r?\n/, "").replace(/\r\n/g, "\n");
|
|
61
|
+
}
|
|
62
|
+
export function contentHash(body) {
|
|
63
|
+
return createHash("sha256").update(body.replace(/\r\n/g, "\n")).digest("hex").slice(0, 12);
|
|
64
|
+
}
|
|
65
|
+
export function mergeManifest(projectDir, entry) {
|
|
66
|
+
const p = join(projectDir, LAYOUT.definitionsDir, "components.json");
|
|
67
|
+
const current = existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : [];
|
|
68
|
+
const i = current.findIndex((m) => m.name === entry.name);
|
|
69
|
+
if (i === -1)
|
|
70
|
+
current.push(entry);
|
|
71
|
+
else
|
|
72
|
+
current[i] = entry;
|
|
73
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
74
|
+
writeFileSync(p, JSON.stringify(current, null, 2) + "\n");
|
|
75
|
+
}
|
|
76
|
+
/** Appends `@import "../components/ui/<x>/<x>.css";` to src/styles/index.css once. */
|
|
77
|
+
function addStyleImport(projectDir, cssPath) {
|
|
78
|
+
const p = join(projectDir, LAYOUT.stylesIndex);
|
|
79
|
+
const rel = "../" + cssPath.replace(/^src\//, "");
|
|
80
|
+
const line = `@import "${rel}";`;
|
|
81
|
+
const current = existsSync(p) ? readFileSync(p, "utf8") : STYLES_INDEX_HEAD;
|
|
82
|
+
if (current.includes(line))
|
|
83
|
+
return;
|
|
84
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
85
|
+
writeFileSync(p, current.trimEnd() + "\n" + line + "\n");
|
|
86
|
+
}
|
|
87
|
+
/** Appends `export * from "./<x>/<x>";` to src/components/ui/index.ts once. */
|
|
88
|
+
function addBarrelExport(projectDir, name) {
|
|
89
|
+
addBarrelLine(projectDir, `export * from "./${name}/${name}";`);
|
|
90
|
+
}
|
|
91
|
+
function addBarrelLine(projectDir, line) {
|
|
92
|
+
const p = join(projectDir, LAYOUT.componentsDir, "index.ts");
|
|
93
|
+
const current = existsSync(p) ? readFileSync(p, "utf8") : BARREL_HEAD;
|
|
94
|
+
if (current.includes(line))
|
|
95
|
+
return;
|
|
96
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
97
|
+
writeFileSync(p, current.trimEnd() + "\n" + line + "\n");
|
|
98
|
+
}
|
|
99
|
+
export const STYLES_INDEX_HEAD = `/* Order matters: tokens, then foundation, then components. \`zengin add\` appends component stylesheets here. */
|
|
100
|
+
@import "generated/tokens.css";
|
|
101
|
+
@import "base.css";
|
|
102
|
+
@import "chart.css";
|
|
103
|
+
@import "motion.css";
|
|
104
|
+
`;
|
|
105
|
+
export const BARREL_HEAD = `/* Every component the project owns. \`zengin add\` appends to this file; the engine treats "${LAYOUT.alias}" as the system. */
|
|
106
|
+
`;
|
package/dist/load.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Registry, type RegistryIndex, type RegistryItem } from "./schema.js";
|
|
2
|
+
/** A registry the client can list and fetch items from, whether it is a directory, a URL, or in memory. */
|
|
3
|
+
export interface RegistrySource {
|
|
4
|
+
readonly location: string;
|
|
5
|
+
index(): Promise<RegistryIndex>;
|
|
6
|
+
item(name: string): Promise<RegistryItem>;
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_REGISTRY = "https://zengin-marketing-site.vercel.app/r";
|
|
9
|
+
/** `source` is a directory written by writeRegistry, or the base URL such a directory is served from. */
|
|
10
|
+
export declare function openRegistry(source?: string): RegistrySource;
|
|
11
|
+
export declare function registryFromMemory(registry: Registry): RegistrySource;
|
package/dist/load.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isRegistryIndex } from "./schema.js";
|
|
4
|
+
export const DEFAULT_REGISTRY = "https://zengin-marketing-site.vercel.app/r";
|
|
5
|
+
/** `source` is a directory written by writeRegistry, or the base URL such a directory is served from. */
|
|
6
|
+
export function openRegistry(source = process.env["ZENGIN_REGISTRY"] ?? DEFAULT_REGISTRY) {
|
|
7
|
+
if (/^https?:\/\//.test(source))
|
|
8
|
+
return remote(source.replace(/\/$/, ""));
|
|
9
|
+
if (!existsSync(join(source, "index.json")))
|
|
10
|
+
throw new Error(`No registry at ${source}: index.json not found. Build one with \`zengin registry build --out <dir>\`.`);
|
|
11
|
+
return local(source);
|
|
12
|
+
}
|
|
13
|
+
export function registryFromMemory(registry) {
|
|
14
|
+
const byName = new Map(registry.items.map((i) => [i.name, i]));
|
|
15
|
+
return {
|
|
16
|
+
location: "memory",
|
|
17
|
+
async index() {
|
|
18
|
+
return { ...registry, items: registry.items.map(({ files: _f, manifest: _m, ...rest }) => rest) };
|
|
19
|
+
},
|
|
20
|
+
async item(name) {
|
|
21
|
+
const it = byName.get(name);
|
|
22
|
+
if (!it)
|
|
23
|
+
throw new Error(`Registry has no item "${name}".`);
|
|
24
|
+
return it;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function local(dir) {
|
|
29
|
+
return {
|
|
30
|
+
location: dir,
|
|
31
|
+
async index() {
|
|
32
|
+
const parsed = JSON.parse(readFileSync(join(dir, "index.json"), "utf8"));
|
|
33
|
+
if (!isRegistryIndex(parsed))
|
|
34
|
+
throw new Error(`${dir}/index.json is not a Zengin registry index.`);
|
|
35
|
+
return parsed;
|
|
36
|
+
},
|
|
37
|
+
async item(name) {
|
|
38
|
+
const p = join(dir, "items", `${name}.json`);
|
|
39
|
+
if (!existsSync(p))
|
|
40
|
+
throw new Error(`Registry at ${dir} has no item "${name}".`);
|
|
41
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function remote(base) {
|
|
46
|
+
const get = async (path) => {
|
|
47
|
+
let res;
|
|
48
|
+
try {
|
|
49
|
+
res = await fetch(`${base}/${path}`);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
throw new Error(`Could not reach the registry at ${base}: ${e.message}. Pass --registry <dir|url> or set ZENGIN_REGISTRY.`);
|
|
53
|
+
}
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
throw new Error(`Registry ${base}/${path} answered ${res.status}.`);
|
|
56
|
+
return res.json();
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
location: base,
|
|
60
|
+
async index() {
|
|
61
|
+
const parsed = await get("index.json");
|
|
62
|
+
if (!isRegistryIndex(parsed))
|
|
63
|
+
throw new Error(`${base}/index.json is not a Zengin registry index.`);
|
|
64
|
+
return parsed;
|
|
65
|
+
},
|
|
66
|
+
async item(name) {
|
|
67
|
+
return (await get(`items/${name}.json`));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RegistrySource } from "./load.js";
|
|
2
|
+
import type { RegistryItem } from "./schema.js";
|
|
3
|
+
/**
|
|
4
|
+
* The items named plus everything they depend on, dependencies first, each once. Unknown names fail with
|
|
5
|
+
* the list of what the registry does have, so a typo is a one-line fix.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveItems(source: RegistrySource, names: string[]): Promise<RegistryItem[]>;
|
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The items named plus everything they depend on, dependencies first, each once. Unknown names fail with
|
|
3
|
+
* the list of what the registry does have, so a typo is a one-line fix.
|
|
4
|
+
*/
|
|
5
|
+
export async function resolveItems(source, names) {
|
|
6
|
+
const index = await source.index();
|
|
7
|
+
const known = new Set(index.items.map((i) => i.name));
|
|
8
|
+
const unknown = names.filter((n) => !known.has(n));
|
|
9
|
+
if (unknown.length) {
|
|
10
|
+
throw new Error(`Registry at ${source.location} has no item${unknown.length > 1 ? "s" : ""} ${unknown.map((n) => `"${n}"`).join(", ")}. Available: ${[...known].sort().join(", ")}.`);
|
|
11
|
+
}
|
|
12
|
+
const ordered = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
const visiting = new Set();
|
|
15
|
+
async function visit(name, trail) {
|
|
16
|
+
if (seen.has(name))
|
|
17
|
+
return;
|
|
18
|
+
if (visiting.has(name))
|
|
19
|
+
throw new Error(`Registry dependency cycle: ${[...trail, name].join(" -> ")}`);
|
|
20
|
+
visiting.add(name);
|
|
21
|
+
const item = await source.item(name);
|
|
22
|
+
for (const dep of item.registryDependencies)
|
|
23
|
+
await visit(dep, [...trail, name]);
|
|
24
|
+
visiting.delete(name);
|
|
25
|
+
seen.add(name);
|
|
26
|
+
ordered.push(item);
|
|
27
|
+
}
|
|
28
|
+
for (const n of names)
|
|
29
|
+
await visit(n, []);
|
|
30
|
+
return ordered;
|
|
31
|
+
}
|