@dowel-ui/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/LICENSE +21 -0
- package/dist/build.d.ts +26 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +1012 -0
- package/dist/build.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/schema-ByqIpT47.js +101 -0
- package/dist/schema-ByqIpT47.js.map +1 -0
- package/dist/schema-Cc32v3R-.d.ts +130 -0
- package/dist/schema-Cc32v3R-.d.ts.map +1 -0
- package/package.json +39 -0
- package/src/build.test.ts +131 -0
- package/src/build.ts +206 -0
- package/src/hash.ts +12 -0
- package/src/index.ts +16 -0
- package/src/schema.ts +106 -0
package/src/build.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { readFileSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { blockMetas, componentMetas, type ComponentMeta } from "@dowel-ui/react/registry";
|
|
6
|
+
|
|
7
|
+
import { hashContent } from "./hash";
|
|
8
|
+
import {
|
|
9
|
+
REGISTRY_VERSION,
|
|
10
|
+
registryIndexSchema,
|
|
11
|
+
registryItemSchema,
|
|
12
|
+
type RegistryFile,
|
|
13
|
+
type RegistryIndexEntry,
|
|
14
|
+
type RegistryItem,
|
|
15
|
+
} from "./schema";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Emits the public registry.
|
|
19
|
+
*
|
|
20
|
+
* Everything the CLI installs originates here, from the same source files the
|
|
21
|
+
* library itself builds and tests. There is no second copy of a component to
|
|
22
|
+
* drift out of sync.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const uiSrc = join(here, "..", "..", "ui", "src");
|
|
27
|
+
const themesSrc = join(here, "..", "..", "themes", "src");
|
|
28
|
+
const uiPackageJson = JSON.parse(
|
|
29
|
+
readFileSync(join(here, "..", "..", "ui", "package.json"), "utf8"),
|
|
30
|
+
) as { name: string; version: string };
|
|
31
|
+
|
|
32
|
+
function read(path: string): string {
|
|
33
|
+
return readFileSync(path, "utf8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Turns a meta into a registry item.
|
|
38
|
+
*
|
|
39
|
+
* Components and blocks differ only in where their source lives and where it is
|
|
40
|
+
* installed, so one function covers both rather than two that drift.
|
|
41
|
+
*/
|
|
42
|
+
function buildSourceItem(meta: ComponentMeta): RegistryItem {
|
|
43
|
+
const isBlock = meta.kind === "block";
|
|
44
|
+
const sourceDir = join(uiSrc, isBlock ? "blocks" : "components", meta.name);
|
|
45
|
+
const group = isBlock ? "blocks" : "ui";
|
|
46
|
+
const fileType = isBlock ? "registry:block" : "registry:ui";
|
|
47
|
+
|
|
48
|
+
const files: RegistryFile[] = meta.files.map((file) => {
|
|
49
|
+
const content = read(join(sourceDir, file));
|
|
50
|
+
return {
|
|
51
|
+
path: `${group}/${file}`,
|
|
52
|
+
type: fileType,
|
|
53
|
+
content,
|
|
54
|
+
hash: hashContent(content),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return registryItemSchema.parse({
|
|
59
|
+
$schema: "./schema/registry-item.json",
|
|
60
|
+
registryVersion: REGISTRY_VERSION,
|
|
61
|
+
name: meta.name,
|
|
62
|
+
type: fileType,
|
|
63
|
+
title: meta.title,
|
|
64
|
+
description: meta.description,
|
|
65
|
+
category: meta.category,
|
|
66
|
+
status: meta.status,
|
|
67
|
+
dependencies: meta.dependencies,
|
|
68
|
+
registryDependencies: meta.registryDependencies,
|
|
69
|
+
files,
|
|
70
|
+
a11y: meta.a11y,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The utilities every component imports.
|
|
76
|
+
*
|
|
77
|
+
* Written by `init` rather than by `add`, because a project needs them before
|
|
78
|
+
* the first component and every component assumes they are present.
|
|
79
|
+
*/
|
|
80
|
+
function buildUtilsItem(): RegistryItem {
|
|
81
|
+
const files: RegistryFile[] = (["utils.ts", "styles.ts"] as const).map((file) => {
|
|
82
|
+
const content = read(join(uiSrc, "lib", file));
|
|
83
|
+
return {
|
|
84
|
+
path: `lib/${file}`,
|
|
85
|
+
type: "registry:lib",
|
|
86
|
+
content,
|
|
87
|
+
hash: hashContent(content),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return registryItemSchema.parse({
|
|
92
|
+
registryVersion: REGISTRY_VERSION,
|
|
93
|
+
name: "utils",
|
|
94
|
+
type: "registry:lib",
|
|
95
|
+
title: "Utilities",
|
|
96
|
+
description:
|
|
97
|
+
"The cn() class merger and the shared interaction-state fragments every component uses.",
|
|
98
|
+
category: "foundation",
|
|
99
|
+
status: "stable",
|
|
100
|
+
dependencies: ["clsx", "tailwind-merge"],
|
|
101
|
+
registryDependencies: [],
|
|
102
|
+
files,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The design tokens, as CSS to append to the project stylesheet.
|
|
108
|
+
*
|
|
109
|
+
* Appended rather than imported from a package: a consumer who cannot edit the
|
|
110
|
+
* tokens does not really own their design system, which is the whole premise.
|
|
111
|
+
*/
|
|
112
|
+
function buildThemeItem(): RegistryItem {
|
|
113
|
+
const content = [
|
|
114
|
+
"/* Design tokens. Safe to edit — this is your copy. */",
|
|
115
|
+
read(join(themesSrc, "tokens.css")),
|
|
116
|
+
read(join(themesSrc, "base.css")),
|
|
117
|
+
].join("\n\n");
|
|
118
|
+
|
|
119
|
+
return registryItemSchema.parse({
|
|
120
|
+
registryVersion: REGISTRY_VERSION,
|
|
121
|
+
name: "theme",
|
|
122
|
+
type: "registry:theme",
|
|
123
|
+
title: "Theme",
|
|
124
|
+
description:
|
|
125
|
+
"Design tokens: colour, radius, typography, elevation and motion, in light and dark.",
|
|
126
|
+
category: "foundation",
|
|
127
|
+
status: "stable",
|
|
128
|
+
dependencies: [],
|
|
129
|
+
registryDependencies: [],
|
|
130
|
+
files: [
|
|
131
|
+
{
|
|
132
|
+
path: "theme.css",
|
|
133
|
+
type: "registry:style",
|
|
134
|
+
content,
|
|
135
|
+
hash: hashContent(content),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function buildRegistry(): RegistryItem[] {
|
|
142
|
+
return [
|
|
143
|
+
buildUtilsItem(),
|
|
144
|
+
buildThemeItem(),
|
|
145
|
+
...componentMetas.map(buildSourceItem),
|
|
146
|
+
...blockMetas.map(buildSourceItem),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function buildIndex(items: RegistryItem[]) {
|
|
151
|
+
const entries: RegistryIndexEntry[] = items.map((item) => ({
|
|
152
|
+
name: item.name,
|
|
153
|
+
type: item.type,
|
|
154
|
+
title: item.title,
|
|
155
|
+
description: item.description,
|
|
156
|
+
category: item.category,
|
|
157
|
+
status: item.status,
|
|
158
|
+
dependencies: item.dependencies,
|
|
159
|
+
registryDependencies: item.registryDependencies,
|
|
160
|
+
fileCount: item.files.length,
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
return registryIndexSchema.parse({
|
|
164
|
+
$schema: "./schema/registry-index.json",
|
|
165
|
+
registryVersion: REGISTRY_VERSION,
|
|
166
|
+
// The package version, not a timestamp: the same source must always produce
|
|
167
|
+
// byte-identical output, or every build shows as a change.
|
|
168
|
+
generatedFrom: `${uiPackageJson.name}@${uiPackageJson.version}`,
|
|
169
|
+
items: entries,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function writeRegistry(outDir: string): { items: number; files: number } {
|
|
174
|
+
const items = buildRegistry();
|
|
175
|
+
const index = buildIndex(items);
|
|
176
|
+
|
|
177
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
178
|
+
mkdirSync(outDir, { recursive: true });
|
|
179
|
+
|
|
180
|
+
writeFileSync(join(outDir, "index.json"), `${JSON.stringify(index, null, 2)}\n`);
|
|
181
|
+
for (const item of items) {
|
|
182
|
+
writeFileSync(join(outDir, `${item.name}.json`), `${JSON.stringify(item, null, 2)}\n`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
items: items.length,
|
|
187
|
+
files: items.reduce((total, item) => total + item.files.length, 0),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Run directly: `tsx src/build.ts [--out <dir>]`
|
|
192
|
+
const invokedDirectly =
|
|
193
|
+
process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
194
|
+
|
|
195
|
+
if (invokedDirectly) {
|
|
196
|
+
const outIndex = process.argv.indexOf("--out");
|
|
197
|
+
const outDir =
|
|
198
|
+
outIndex === -1
|
|
199
|
+
? join(here, "..", "r")
|
|
200
|
+
: (process.argv[outIndex + 1] ?? join(here, "..", "r"));
|
|
201
|
+
|
|
202
|
+
const result = writeRegistry(outDir);
|
|
203
|
+
console.log(
|
|
204
|
+
`Registry written to ${outDir}: ${String(result.items)} items, ${String(result.files)} files.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Content hash used to detect local edits to an installed file.
|
|
5
|
+
*
|
|
6
|
+
* Line endings are normalised before hashing so a Windows checkout does not
|
|
7
|
+
* read as "every file modified", which would make `update` useless there.
|
|
8
|
+
*/
|
|
9
|
+
export function hashContent(content: string): string {
|
|
10
|
+
const normalised = content.replace(/\r\n/g, "\n");
|
|
11
|
+
return `sha256:${createHash("sha256").update(normalised, "utf8").digest("hex")}`;
|
|
12
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { hashContent } from "./hash";
|
|
2
|
+
export {
|
|
3
|
+
REGISTRY_VERSION,
|
|
4
|
+
registryFileSchema,
|
|
5
|
+
registryFileTypeSchema,
|
|
6
|
+
registryIndexEntrySchema,
|
|
7
|
+
registryIndexSchema,
|
|
8
|
+
registryItemSchema,
|
|
9
|
+
registryItemTypeSchema,
|
|
10
|
+
type RegistryFile,
|
|
11
|
+
type RegistryFileType,
|
|
12
|
+
type RegistryIndex,
|
|
13
|
+
type RegistryIndexEntry,
|
|
14
|
+
type RegistryItem,
|
|
15
|
+
type RegistryItemType,
|
|
16
|
+
} from "./schema";
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The public registry contract.
|
|
5
|
+
*
|
|
6
|
+
* This is the boundary between the library and every consumer's project, so it
|
|
7
|
+
* is validated on both sides: the build refuses to emit anything that does not
|
|
8
|
+
* satisfy it, and the CLI refuses to install anything that does not parse. A
|
|
9
|
+
* registry that serves malformed data breaks builds in someone else's
|
|
10
|
+
* repository, where it is hardest to diagnose.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Bumped only for a breaking change to the shape below. */
|
|
14
|
+
export const REGISTRY_VERSION = 1;
|
|
15
|
+
|
|
16
|
+
export const registryFileTypeSchema = z.enum([
|
|
17
|
+
/** A component. Installed under the `ui` alias. */
|
|
18
|
+
"registry:ui",
|
|
19
|
+
/** A shared utility. Installed under the `lib` alias. */
|
|
20
|
+
"registry:lib",
|
|
21
|
+
/** A hook. Installed under the `hooks` alias. */
|
|
22
|
+
"registry:hook",
|
|
23
|
+
/** A whole page section. Installed under the `blocks` alias. */
|
|
24
|
+
"registry:block",
|
|
25
|
+
/** CSS appended to the project stylesheet rather than written as a file. */
|
|
26
|
+
"registry:style",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export type RegistryFileType = z.infer<typeof registryFileTypeSchema>;
|
|
30
|
+
|
|
31
|
+
export const registryItemTypeSchema = z.enum([
|
|
32
|
+
"registry:ui",
|
|
33
|
+
"registry:lib",
|
|
34
|
+
"registry:hook",
|
|
35
|
+
"registry:theme",
|
|
36
|
+
"registry:block",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export type RegistryItemType = z.infer<typeof registryItemTypeSchema>;
|
|
40
|
+
|
|
41
|
+
export const registryFileSchema = z.object({
|
|
42
|
+
/**
|
|
43
|
+
* Logical path within the registry, e.g. `ui/button.tsx`, `lib/utils.ts`.
|
|
44
|
+
*
|
|
45
|
+
* The leading segment selects which of the consumer's aliases the file is
|
|
46
|
+
* written under. The registry deliberately does not know the destination —
|
|
47
|
+
* that depends on a project layout it has never seen.
|
|
48
|
+
*/
|
|
49
|
+
path: z.string().min(1),
|
|
50
|
+
type: registryFileTypeSchema,
|
|
51
|
+
content: z.string(),
|
|
52
|
+
/**
|
|
53
|
+
* `sha256:<hex>` of `content` as published.
|
|
54
|
+
*
|
|
55
|
+
* Recorded at install time so `update` can tell an untouched file from one
|
|
56
|
+
* the user has edited. This cannot be added later: an install that did not
|
|
57
|
+
* record a hash leaves no way to know what it originally wrote.
|
|
58
|
+
*/
|
|
59
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export type RegistryFile = z.infer<typeof registryFileSchema>;
|
|
63
|
+
|
|
64
|
+
export const registryItemSchema = z.object({
|
|
65
|
+
$schema: z.string().optional(),
|
|
66
|
+
registryVersion: z.literal(REGISTRY_VERSION),
|
|
67
|
+
name: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
68
|
+
type: registryItemTypeSchema,
|
|
69
|
+
title: z.string().min(1),
|
|
70
|
+
description: z.string().min(10),
|
|
71
|
+
category: z.string().min(1),
|
|
72
|
+
status: z.enum(["stable", "beta", "experimental"]),
|
|
73
|
+
/** npm packages to install alongside the files. */
|
|
74
|
+
dependencies: z.array(z.string()),
|
|
75
|
+
/** Other registry items to install first. */
|
|
76
|
+
registryDependencies: z.array(z.string()),
|
|
77
|
+
files: z.array(registryFileSchema).min(1),
|
|
78
|
+
a11y: z.string().optional(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export type RegistryItem = z.infer<typeof registryItemSchema>;
|
|
82
|
+
|
|
83
|
+
export const registryIndexEntrySchema = registryItemSchema
|
|
84
|
+
.pick({
|
|
85
|
+
name: true,
|
|
86
|
+
type: true,
|
|
87
|
+
title: true,
|
|
88
|
+
description: true,
|
|
89
|
+
category: true,
|
|
90
|
+
status: true,
|
|
91
|
+
dependencies: true,
|
|
92
|
+
registryDependencies: true,
|
|
93
|
+
})
|
|
94
|
+
.extend({ fileCount: z.number().int().positive() });
|
|
95
|
+
|
|
96
|
+
export type RegistryIndexEntry = z.infer<typeof registryIndexEntrySchema>;
|
|
97
|
+
|
|
98
|
+
export const registryIndexSchema = z.object({
|
|
99
|
+
$schema: z.string().optional(),
|
|
100
|
+
registryVersion: z.literal(REGISTRY_VERSION),
|
|
101
|
+
/** Version of the package the registry was generated from. */
|
|
102
|
+
generatedFrom: z.string().min(1),
|
|
103
|
+
items: z.array(registryIndexEntrySchema),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export type RegistryIndex = z.infer<typeof registryIndexSchema>;
|