create-bht-component-registry 0.0.0-stage → 0.2.1
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/README.md +117 -2
- package/dist/cli.js +1093 -0
- package/package.json +55 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Behive Tech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,118 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-bht-component-registry
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scaffold a [bht-component-registry](https://github.com/behivetech/bht-component-registry) — a
|
|
4
|
+
component registry that stays in git: docs generated from source, live examples,
|
|
5
|
+
versions via Changesets, a static site you can deploy anywhere — and keep it current with `update`.
|
|
6
|
+
|
|
7
|
+
Hosted & expanded version, guides and consulting: https://platform.behivetech.com
|
|
8
|
+
|
|
9
|
+
## Create a registry
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx create-bht-component-registry my-registry
|
|
13
|
+
# or
|
|
14
|
+
npm create bht-component-registry my-registry
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
You are asked for your npm scope (the `acme` in `@acme/*`), the registry you publish to and the
|
|
18
|
+
starter categories; pass them as flags to skip the questions. The command downloads the template
|
|
19
|
+
release matching its own version, renames the shipped `example` scope to yours, writes
|
|
20
|
+
`registry.config.json`, runs `pnpm install` and a first `pnpm build`, and prints what to do next.
|
|
21
|
+
|
|
22
|
+
The example packages are unpublishable in the template (`"private": true`, so the template repo's
|
|
23
|
+
own release workflow can never publish `@example/*`); yours are publishable — the rename sets
|
|
24
|
+
`"private": false` in every `registry/<scope>/<category>/<name>/package.json`.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx create-bht-component-registry my-registry --scope acme --yes
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Flag | Default | What it does |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `--scope <name>` | prompted | Your npm scope without the `@`. Required. Lowercase letters, digits and hyphens, 1–40 chars, not a reserved word. Create the matching org on npmjs before your first `pnpm release`. |
|
|
33
|
+
| `--registry <url>` | `https://registry.npmjs.org` | Where `pnpm release` publishes. GitHub Packages is `https://npm.pkg.github.com`. |
|
|
34
|
+
| `--categories <a,b,c>` | `atoms,molecules,organisms,templates,forms` | Starter folders under `registry/<scope>/`. Empty ones get a `.gitkeep`. |
|
|
35
|
+
| `--template-version <x.y.z>` | this CLI's version | Which template release to download. |
|
|
36
|
+
| `--no-install` | off | Skip `pnpm install` and `pnpm build`. |
|
|
37
|
+
| `-y`, `--yes` | off | Accept defaults instead of prompting (`--scope` is still required). |
|
|
38
|
+
| `--from <path>` | — | Dev mode, see below. |
|
|
39
|
+
|
|
40
|
+
The target directory must not exist or must be empty. If `pnpm install` or `pnpm build` fails, the
|
|
41
|
+
command prints which step failed and exits 1, leaving the directory in place for you to fix.
|
|
42
|
+
|
|
43
|
+
Some paths exist only in the template repo and are never materialised in yours: this CLI's own
|
|
44
|
+
source (`packages/create-bht-component-registry/**`) and the template repo's contributor files
|
|
45
|
+
(`.github/ISSUE_TEMPLATE/**`, `.github/PULL_REQUEST_TEMPLATE.md`). They are listed under
|
|
46
|
+
`templateOnly` in the template's `template.manifest.json`.
|
|
47
|
+
|
|
48
|
+
## Update a registry
|
|
49
|
+
|
|
50
|
+
Run inside a registry you created earlier:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx create-bht-component-registry update
|
|
54
|
+
npx create-bht-component-registry update --dry-run
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`update` needs a clean git tree, because it replaces whole files and `git diff` is how you review
|
|
58
|
+
the result. It reads `templateVersion` from `registry.config.json`, fetches the newer template,
|
|
59
|
+
and replaces only **template-owned** paths — the ones listed in the target release's
|
|
60
|
+
`template.manifest.json` (the docs app, `packages/**`, the generator, workflows, root config,
|
|
61
|
+
`docs/**`, …). It never touches:
|
|
62
|
+
|
|
63
|
+
- `registry/**` — your components
|
|
64
|
+
- `registry.config.json` — your settings (only `templateVersion` is bumped)
|
|
65
|
+
- `.changeset/*.md` — your pending changesets
|
|
66
|
+
- `README.md` below the line `<!-- your notes below this line are kept by update -->`; the part
|
|
67
|
+
above it is refreshed from the template. A README without that line is left untouched.
|
|
68
|
+
|
|
69
|
+
`templateOnly` paths (see above) are never copied, and any left in your repo by an older template
|
|
70
|
+
that still shipped them are removed and reported under "Removed". The check order is
|
|
71
|
+
`templateOnly`, then `never`, then `owned`.
|
|
72
|
+
|
|
73
|
+
Files that mention the template's `example` scope (the e2e spec, the docs app's dependency list)
|
|
74
|
+
get your scope re-applied after the copy, so the rename from `create` survives. Finally it prints
|
|
75
|
+
the added, replaced and removed files and tells you to review with `git diff` and run
|
|
76
|
+
`pnpm install && pnpm verify`.
|
|
77
|
+
|
|
78
|
+
| Flag | Default | What it does |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `--to <x.y.z>` | this CLI's version | Template version to update to. Nothing happens if you are already at or past it. |
|
|
81
|
+
| `--dry-run` | off | Print the plan and change nothing. |
|
|
82
|
+
| `-y`, `--yes` | off | Apply without the confirmation prompt. |
|
|
83
|
+
| `--from <path>` | — | Dev mode, see below. |
|
|
84
|
+
|
|
85
|
+
Because the root `package.json` is template-owned, add dependencies to the workspace package that
|
|
86
|
+
needs them (your component, `apps/docs`) rather than to the root.
|
|
87
|
+
|
|
88
|
+
## Dev mode: `--from <path>`
|
|
89
|
+
|
|
90
|
+
Both commands accept `--from <path>` pointing at a local checkout of the template repo. Nothing is
|
|
91
|
+
downloaded; the checkout is used as-is (minus `node_modules`, build output, `.git` and generated
|
|
92
|
+
data). This is how the template is developed and tested:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pnpm --filter create-bht-component-registry build
|
|
96
|
+
node packages/create-bht-component-registry/dist/cli.js create /tmp/demo --scope testco --from .
|
|
97
|
+
node packages/create-bht-component-registry/dist/cli.js update --from . --to 0.2.0 --dry-run
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
With `--from` and no `--to`, `update` takes the target version from the checkout's
|
|
101
|
+
`registry.config.json`.
|
|
102
|
+
|
|
103
|
+
## Where the template comes from
|
|
104
|
+
|
|
105
|
+
The template is the public repo itself, at the tag `changeset publish` creates for this package:
|
|
106
|
+
`https://github.com/behivetech/bht-component-registry/archive/refs/tags/create-bht-component-registry@<version>.tar.gz`.
|
|
107
|
+
If that tag does not exist yet the CLI falls back to the `main` branch with a warning. No separate
|
|
108
|
+
template store exists, so there is nothing else to keep in sync.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pnpm --filter create-bht-component-registry test # vitest, offline, no pnpm needed
|
|
114
|
+
pnpm --filter create-bht-component-registry build # tsup → dist/cli.js
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Tests inject the template fetcher and the pnpm runner through each command's options object, so
|
|
118
|
+
the suite never touches the network or spawns pnpm.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "util";
|
|
5
|
+
|
|
6
|
+
// src/commands/create.ts
|
|
7
|
+
import { existsSync as existsSync6 } from "fs";
|
|
8
|
+
import { mkdir as mkdir3, readdir as readdir3 } from "fs/promises";
|
|
9
|
+
import { join as join7, resolve as resolve2 } from "path";
|
|
10
|
+
|
|
11
|
+
// src/lib/config.ts
|
|
12
|
+
import { existsSync as existsSync2 } from "fs";
|
|
13
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
14
|
+
|
|
15
|
+
// ../scope/src/index.ts
|
|
16
|
+
var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
17
|
+
"www",
|
|
18
|
+
"app",
|
|
19
|
+
"api",
|
|
20
|
+
"admin",
|
|
21
|
+
"dashboard",
|
|
22
|
+
"docs",
|
|
23
|
+
"sites",
|
|
24
|
+
"scopes",
|
|
25
|
+
"registry",
|
|
26
|
+
"tokens",
|
|
27
|
+
"sign-in",
|
|
28
|
+
"sign-up",
|
|
29
|
+
"onboarding",
|
|
30
|
+
"static",
|
|
31
|
+
"assets",
|
|
32
|
+
"mail",
|
|
33
|
+
"status",
|
|
34
|
+
"node_modules",
|
|
35
|
+
"npm",
|
|
36
|
+
"pnpm",
|
|
37
|
+
"example"
|
|
38
|
+
]);
|
|
39
|
+
var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
40
|
+
function slugProblem(slug) {
|
|
41
|
+
if (!slug) return "empty";
|
|
42
|
+
if (!SLUG_PATTERN.test(slug)) return "format";
|
|
43
|
+
if (RESERVED_SLUGS.has(slug)) return "reserved";
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/lib/fs.ts
|
|
48
|
+
import { existsSync } from "fs";
|
|
49
|
+
import { copyFile, cp, mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
50
|
+
import { basename, dirname, extname, isAbsolute, join, relative, sep } from "path";
|
|
51
|
+
var SKIP_SEGMENTS = /* @__PURE__ */ new Set([
|
|
52
|
+
"node_modules",
|
|
53
|
+
".git",
|
|
54
|
+
".turbo",
|
|
55
|
+
".next",
|
|
56
|
+
"out",
|
|
57
|
+
"dist",
|
|
58
|
+
"coverage",
|
|
59
|
+
"test-results",
|
|
60
|
+
"playwright-report"
|
|
61
|
+
]);
|
|
62
|
+
var SKIP_PREFIXES = ["apps/docs/src/generated"];
|
|
63
|
+
var GENERATED_DOCS_JSON = /^registry\/.+\/docs\.json$/;
|
|
64
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
65
|
+
".json",
|
|
66
|
+
".ts",
|
|
67
|
+
".tsx",
|
|
68
|
+
".js",
|
|
69
|
+
".mjs",
|
|
70
|
+
".cjs",
|
|
71
|
+
".md",
|
|
72
|
+
".mdx",
|
|
73
|
+
".scss",
|
|
74
|
+
".css",
|
|
75
|
+
".yaml",
|
|
76
|
+
".yml",
|
|
77
|
+
".hbs",
|
|
78
|
+
".txt",
|
|
79
|
+
".html"
|
|
80
|
+
]);
|
|
81
|
+
var TEXT_BASENAMES = /* @__PURE__ */ new Set([".gitignore", ".npmrc", ".gitkeep", "LICENSE"]);
|
|
82
|
+
function toPosix(path) {
|
|
83
|
+
return path.split(sep).join("/");
|
|
84
|
+
}
|
|
85
|
+
function shouldSkip(relPath) {
|
|
86
|
+
if (relPath === "") return false;
|
|
87
|
+
if (relPath.split("/").some((segment) => SKIP_SEGMENTS.has(segment))) return true;
|
|
88
|
+
if (SKIP_PREFIXES.some((prefix) => relPath === prefix || relPath.startsWith(`${prefix}/`))) return true;
|
|
89
|
+
return GENERATED_DOCS_JSON.test(relPath);
|
|
90
|
+
}
|
|
91
|
+
function isTextFile(path) {
|
|
92
|
+
return TEXT_EXTENSIONS.has(extname(path).toLowerCase()) || TEXT_BASENAMES.has(basename(path));
|
|
93
|
+
}
|
|
94
|
+
async function listFiles(root) {
|
|
95
|
+
const found = [];
|
|
96
|
+
async function walk(dir) {
|
|
97
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
98
|
+
const abs = join(dir, entry.name);
|
|
99
|
+
const rel = toPosix(relative(root, abs));
|
|
100
|
+
if (shouldSkip(rel)) continue;
|
|
101
|
+
if (entry.isDirectory()) await walk(abs);
|
|
102
|
+
else if (entry.isFile() || entry.isSymbolicLink()) found.push(rel);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!existsSync(root)) return found;
|
|
106
|
+
await walk(root);
|
|
107
|
+
return found.sort();
|
|
108
|
+
}
|
|
109
|
+
async function copyTree(src, dest, skip) {
|
|
110
|
+
await cp(src, dest, {
|
|
111
|
+
recursive: true,
|
|
112
|
+
filter: (source) => {
|
|
113
|
+
const rel = toPosix(relative(src, source));
|
|
114
|
+
return !shouldSkip(rel) && !(skip?.(rel) ?? false);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function readText(path) {
|
|
119
|
+
return readFile(path, "utf8");
|
|
120
|
+
}
|
|
121
|
+
async function writeText(path, text) {
|
|
122
|
+
await mkdir(dirname(path), { recursive: true });
|
|
123
|
+
await writeFile(path, text, "utf8");
|
|
124
|
+
}
|
|
125
|
+
async function writeBytes(path, content) {
|
|
126
|
+
await mkdir(dirname(path), { recursive: true });
|
|
127
|
+
await writeFile(path, content);
|
|
128
|
+
}
|
|
129
|
+
async function isDirEmpty(path) {
|
|
130
|
+
if (!existsSync(path)) return true;
|
|
131
|
+
const info = await stat(path);
|
|
132
|
+
if (!info.isDirectory()) return false;
|
|
133
|
+
return (await readdir(path)).length === 0;
|
|
134
|
+
}
|
|
135
|
+
async function removeFile(root, relPath) {
|
|
136
|
+
await rm(join(root, relPath), { force: true });
|
|
137
|
+
let dir = dirname(join(root, relPath));
|
|
138
|
+
while (dir !== root && dir.startsWith(root)) {
|
|
139
|
+
if (!existsSync(dir) || (await readdir(dir)).length > 0) break;
|
|
140
|
+
await rm(dir, { recursive: true });
|
|
141
|
+
dir = dirname(dir);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function isInside(parent, child) {
|
|
145
|
+
const rel = relative(parent, child);
|
|
146
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/lib/config.ts
|
|
150
|
+
var CONFIG_FILE = "registry.config.json";
|
|
151
|
+
var VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
152
|
+
function isRecord(value) {
|
|
153
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
154
|
+
}
|
|
155
|
+
function requireString(record, key, source) {
|
|
156
|
+
const value = record[key];
|
|
157
|
+
if (typeof value !== "string" || value === "") {
|
|
158
|
+
throw new Error(`${source}: "${key}" must be a non-empty string.`);
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
function parseRegistryConfig(text, source = CONFIG_FILE) {
|
|
163
|
+
let raw;
|
|
164
|
+
try {
|
|
165
|
+
raw = JSON.parse(text);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new Error(`${source} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
|
+
}
|
|
169
|
+
if (!isRecord(raw)) throw new Error(`${source} must be a JSON object.`);
|
|
170
|
+
if (!isRecord(raw.site)) throw new Error(`${source}: "site" must be an object.`);
|
|
171
|
+
return {
|
|
172
|
+
...raw,
|
|
173
|
+
scope: requireString(raw, "scope", source),
|
|
174
|
+
npmRegistry: requireString(raw, "npmRegistry", source),
|
|
175
|
+
templateVersion: requireString(raw, "templateVersion", source),
|
|
176
|
+
site: { ...raw.site, title: requireString(raw.site, "title", `${source} site`) }
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async function readRegistryConfig(dir) {
|
|
180
|
+
return parseRegistryConfig(await readText(join2(dir, CONFIG_FILE)));
|
|
181
|
+
}
|
|
182
|
+
function serializeRegistryConfig(config) {
|
|
183
|
+
return `${JSON.stringify(config, null, 2)}
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
186
|
+
async function writeRegistryConfig(dir, config) {
|
|
187
|
+
await writeText(join2(dir, CONFIG_FILE), serializeRegistryConfig(config));
|
|
188
|
+
}
|
|
189
|
+
function findRegistryRoot(start) {
|
|
190
|
+
let dir = start;
|
|
191
|
+
for (; ; ) {
|
|
192
|
+
if (existsSync2(join2(dir, CONFIG_FILE))) return dir;
|
|
193
|
+
const parent = dirname2(dir);
|
|
194
|
+
if (parent === dir) return null;
|
|
195
|
+
dir = parent;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function scopeProblemMessage(scope) {
|
|
199
|
+
switch (slugProblem(scope)) {
|
|
200
|
+
case "empty":
|
|
201
|
+
return "Scope is required: the npm scope your components publish under, e.g. acme \u2192 @acme/*.";
|
|
202
|
+
case "format":
|
|
203
|
+
return "Scope must be lowercase letters, digits and hyphens, 1\u201340 chars, e.g. acme \u2192 @acme/*.";
|
|
204
|
+
case "reserved":
|
|
205
|
+
return `'${scope}' is reserved; pick another scope.`;
|
|
206
|
+
default:
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function titleCaseScope(scope) {
|
|
211
|
+
return scope.split("-").filter((part) => part !== "").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
212
|
+
}
|
|
213
|
+
function isVersion(value) {
|
|
214
|
+
return VERSION_PATTERN.test(value);
|
|
215
|
+
}
|
|
216
|
+
function compareVersions(a, b) {
|
|
217
|
+
for (const value of [a, b]) {
|
|
218
|
+
if (!isVersion(value)) throw new Error(`"${value}" is not a version of the form x.y.z.`);
|
|
219
|
+
}
|
|
220
|
+
const left = a.split(".").map(Number);
|
|
221
|
+
const right = b.split(".").map(Number);
|
|
222
|
+
for (let i = 0; i < 3; i += 1) {
|
|
223
|
+
const l = left[i] ?? 0;
|
|
224
|
+
const r = right[i] ?? 0;
|
|
225
|
+
if (l < r) return -1;
|
|
226
|
+
if (l > r) return 1;
|
|
227
|
+
}
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/lib/log.ts
|
|
232
|
+
import pc from "picocolors";
|
|
233
|
+
var log = {
|
|
234
|
+
info: (message) => console.log(` ${message}`),
|
|
235
|
+
success: (message) => console.log(` ${pc.green("\u2713")} ${message}`),
|
|
236
|
+
warn: (message) => console.warn(` ${pc.yellow("!")} ${message}`),
|
|
237
|
+
error: (message) => console.error(` ${pc.red("\u2717")} ${message}`),
|
|
238
|
+
blank: () => console.log()
|
|
239
|
+
};
|
|
240
|
+
function errorMessage(error) {
|
|
241
|
+
return error instanceof Error ? error.message : String(error);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/lib/manifest.ts
|
|
245
|
+
import { join as join3 } from "path";
|
|
246
|
+
var MANIFEST_FILE = "template.manifest.json";
|
|
247
|
+
var DEFAULT_README_MARKER = "<!-- your notes below this line are kept by update -->";
|
|
248
|
+
var DEFAULT_MANIFEST = {
|
|
249
|
+
owned: [
|
|
250
|
+
"apps/docs/**",
|
|
251
|
+
"packages/**",
|
|
252
|
+
"scripts/**",
|
|
253
|
+
// Shipped by pre-release templates (their task runner). Still owned so `update`
|
|
254
|
+
// removes them from repos created from those.
|
|
255
|
+
"turbo/generators/**",
|
|
256
|
+
".github/workflows/**",
|
|
257
|
+
".changeset/config.json",
|
|
258
|
+
"turbo.json",
|
|
259
|
+
"pnpm-workspace.yaml",
|
|
260
|
+
"package.json",
|
|
261
|
+
"registry.config.schema.json",
|
|
262
|
+
"template.manifest.json",
|
|
263
|
+
"template.manifest.schema.json",
|
|
264
|
+
".gitignore",
|
|
265
|
+
"Dockerfile",
|
|
266
|
+
"nginx.conf",
|
|
267
|
+
"CLAUDE.md",
|
|
268
|
+
"CONTRIBUTING.md",
|
|
269
|
+
"SECURITY.md",
|
|
270
|
+
"LICENSE",
|
|
271
|
+
"docs/**"
|
|
272
|
+
],
|
|
273
|
+
never: ["registry/**", "registry.config.json", ".changeset/*.md", "apps/docs/src/generated/**"],
|
|
274
|
+
templateOnly: ["packages/create-bht-component-registry/**", ".github/ISSUE_TEMPLATE/**", ".github/PULL_REQUEST_TEMPLATE.md"],
|
|
275
|
+
readmeMarker: DEFAULT_README_MARKER,
|
|
276
|
+
rewriteScope: ["apps/docs/e2e/public.spec.ts", "apps/docs/package.json"]
|
|
277
|
+
};
|
|
278
|
+
function stringArray(value, key, source) {
|
|
279
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
280
|
+
throw new Error(`${source}: "${key}" must be an array of strings.`);
|
|
281
|
+
}
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
function parseManifest(text, source = MANIFEST_FILE) {
|
|
285
|
+
let raw;
|
|
286
|
+
try {
|
|
287
|
+
raw = JSON.parse(text);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
throw new Error(`${source} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
290
|
+
}
|
|
291
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
292
|
+
throw new Error(`${source} must be a JSON object.`);
|
|
293
|
+
}
|
|
294
|
+
const record = raw;
|
|
295
|
+
const marker = record.readmeMarker ?? DEFAULT_README_MARKER;
|
|
296
|
+
if (typeof marker !== "string" || marker.trim() === "") {
|
|
297
|
+
throw new Error(`${source}: "readmeMarker" must be a non-empty string.`);
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
owned: stringArray(record.owned, "owned", source),
|
|
301
|
+
never: stringArray(record.never ?? [], "never", source),
|
|
302
|
+
templateOnly: stringArray(record.templateOnly ?? [], "templateOnly", source),
|
|
303
|
+
readmeMarker: marker,
|
|
304
|
+
rewriteScope: stringArray(record.rewriteScope ?? [], "rewriteScope", source)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async function readManifest(dir) {
|
|
308
|
+
return parseManifest(await readText(join3(dir, MANIFEST_FILE)));
|
|
309
|
+
}
|
|
310
|
+
function globToRegExp(glob) {
|
|
311
|
+
let pattern = "";
|
|
312
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
313
|
+
const char = glob.charAt(i);
|
|
314
|
+
if (char === "*") {
|
|
315
|
+
if (glob.charAt(i + 1) === "*") {
|
|
316
|
+
if (glob.charAt(i + 2) === "/") {
|
|
317
|
+
pattern += "(?:.*/)?";
|
|
318
|
+
i += 2;
|
|
319
|
+
} else {
|
|
320
|
+
pattern += ".*";
|
|
321
|
+
i += 1;
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
pattern += "[^/]*";
|
|
325
|
+
}
|
|
326
|
+
} else if (char === "?") {
|
|
327
|
+
pattern += "[^/]";
|
|
328
|
+
} else {
|
|
329
|
+
pattern += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return new RegExp(`^${pattern}$`);
|
|
333
|
+
}
|
|
334
|
+
function matchesAny(path, globs) {
|
|
335
|
+
return globs.some((glob) => globToRegExp(glob).test(path));
|
|
336
|
+
}
|
|
337
|
+
function isTemplateOnly(path, manifest) {
|
|
338
|
+
return matchesAny(path, manifest.templateOnly);
|
|
339
|
+
}
|
|
340
|
+
function isTemplateOwned(path, manifest) {
|
|
341
|
+
if (isTemplateOnly(path, manifest)) return false;
|
|
342
|
+
return matchesAny(path, manifest.owned) && !matchesAny(path, manifest.never);
|
|
343
|
+
}
|
|
344
|
+
function splitReadme(text, marker) {
|
|
345
|
+
const index = text.indexOf(marker);
|
|
346
|
+
if (index === -1) return null;
|
|
347
|
+
return { above: text.slice(0, index), below: text.slice(index) };
|
|
348
|
+
}
|
|
349
|
+
function mergeReadme(local, target, marker) {
|
|
350
|
+
const localParts = splitReadme(local, marker);
|
|
351
|
+
if (!localParts) return null;
|
|
352
|
+
const targetParts = splitReadme(target, marker);
|
|
353
|
+
const above = targetParts ? targetParts.above : `${target.trimEnd()}
|
|
354
|
+
|
|
355
|
+
`;
|
|
356
|
+
return above + localParts.below;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/lib/prompt.ts
|
|
360
|
+
import prompts from "prompts";
|
|
361
|
+
var interactivePrompter = {
|
|
362
|
+
async text({ message, initial, validate }) {
|
|
363
|
+
let cancelled2 = false;
|
|
364
|
+
const answers = await prompts(
|
|
365
|
+
{
|
|
366
|
+
type: "text",
|
|
367
|
+
name: "value",
|
|
368
|
+
message,
|
|
369
|
+
initial,
|
|
370
|
+
validate: validate ? (value2) => validate(value2) : void 0
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
onCancel: () => {
|
|
374
|
+
cancelled2 = true;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
const value = answers.value;
|
|
379
|
+
return cancelled2 || typeof value !== "string" ? void 0 : value;
|
|
380
|
+
},
|
|
381
|
+
async confirm({ message, initial = false }) {
|
|
382
|
+
let cancelled2 = false;
|
|
383
|
+
const answers = await prompts(
|
|
384
|
+
{ type: "confirm", name: "value", message, initial },
|
|
385
|
+
{
|
|
386
|
+
onCancel: () => {
|
|
387
|
+
cancelled2 = true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
const value = answers.value;
|
|
392
|
+
return cancelled2 || typeof value !== "boolean" ? void 0 : value;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// src/lib/rename.ts
|
|
397
|
+
import { existsSync as existsSync3 } from "fs";
|
|
398
|
+
import { readdir as readdir2, rename } from "fs/promises";
|
|
399
|
+
import { join as join4 } from "path";
|
|
400
|
+
var TEMPLATE_SCOPE = "example";
|
|
401
|
+
function scopeRename(toScope, fromScope = TEMPLATE_SCOPE) {
|
|
402
|
+
return { fromScope, toScope, fromName: titleCaseScope(fromScope), toName: titleCaseScope(toScope) };
|
|
403
|
+
}
|
|
404
|
+
function escapeRegExp(text) {
|
|
405
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
406
|
+
}
|
|
407
|
+
function rewritePackageRefs(text, r) {
|
|
408
|
+
if (r.fromScope === r.toScope) return text;
|
|
409
|
+
const pathPattern = new RegExp(`registry/${escapeRegExp(r.fromScope)}(?![A-Za-z0-9-])`, "g");
|
|
410
|
+
return text.replaceAll(`@${r.fromScope}/`, `@${r.toScope}/`).replace(pathPattern, `registry/${r.toScope}`);
|
|
411
|
+
}
|
|
412
|
+
function rewriteScopeMentions(text, r) {
|
|
413
|
+
if (r.fromScope === r.toScope) return text;
|
|
414
|
+
const slug = new RegExp(`(?<![A-Za-z0-9_-])${escapeRegExp(r.fromScope)}(?![A-Za-z0-9_-])`, "g");
|
|
415
|
+
const name = new RegExp(
|
|
416
|
+
`(?<![\\w<])(?<!\\b(?:const|let|var|function|class|type|interface|enum)\\s+)${escapeRegExp(r.fromName)}(?!\\w)`,
|
|
417
|
+
"g"
|
|
418
|
+
);
|
|
419
|
+
return text.replace(slug, r.toScope).replace(name, r.toName);
|
|
420
|
+
}
|
|
421
|
+
function markPublishable(text) {
|
|
422
|
+
const pkg = JSON.parse(text);
|
|
423
|
+
if (pkg.private === false) return text;
|
|
424
|
+
if ("private" in pkg) {
|
|
425
|
+
pkg.private = false;
|
|
426
|
+
return `${JSON.stringify(pkg, null, 2)}
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
429
|
+
const ordered = {};
|
|
430
|
+
const anchor = "version" in pkg ? "version" : "name";
|
|
431
|
+
let inserted = false;
|
|
432
|
+
for (const [key, value] of Object.entries(pkg)) {
|
|
433
|
+
ordered[key] = value;
|
|
434
|
+
if (key === anchor) {
|
|
435
|
+
ordered.private = false;
|
|
436
|
+
inserted = true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (!inserted) ordered.private = false;
|
|
440
|
+
return `${JSON.stringify(ordered, null, 2)}
|
|
441
|
+
`;
|
|
442
|
+
}
|
|
443
|
+
function isComponentPackageJson(relToScope) {
|
|
444
|
+
const parts = relToScope.split("/");
|
|
445
|
+
return parts.length === 3 && parts[2] === "package.json";
|
|
446
|
+
}
|
|
447
|
+
async function renameScopeTree(root, r) {
|
|
448
|
+
const from = join4(root, "registry", r.fromScope);
|
|
449
|
+
const to = join4(root, "registry", r.toScope);
|
|
450
|
+
if (!existsSync3(from) || r.fromScope === r.toScope) return [];
|
|
451
|
+
if (existsSync3(to) && (await readdir2(to)).length > 0) {
|
|
452
|
+
throw new Error(`registry/${r.toScope} already exists in the template; cannot rename registry/${r.fromScope} onto it.`);
|
|
453
|
+
}
|
|
454
|
+
await rename(from, to);
|
|
455
|
+
const changed = [];
|
|
456
|
+
for (const rel of await listFiles(to)) {
|
|
457
|
+
if (!isTextFile(rel)) continue;
|
|
458
|
+
const path = join4(to, rel);
|
|
459
|
+
const before = await readText(path);
|
|
460
|
+
let after = rewritePackageRefs(before, r);
|
|
461
|
+
if (isComponentPackageJson(rel)) after = markPublishable(after);
|
|
462
|
+
if (after !== before) {
|
|
463
|
+
await writeText(path, after);
|
|
464
|
+
changed.push(`registry/${r.toScope}/${rel}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const scopeJson = join4(to, "scope.json");
|
|
468
|
+
const meta = existsSync3(scopeJson) ? JSON.parse(await readText(scopeJson)) : {};
|
|
469
|
+
await writeText(scopeJson, `${JSON.stringify({ ...meta, name: r.toName }, null, 2)}
|
|
470
|
+
`);
|
|
471
|
+
changed.push(`registry/${r.toScope}/scope.json`);
|
|
472
|
+
return changed;
|
|
473
|
+
}
|
|
474
|
+
async function rewriteChangesets(root, r) {
|
|
475
|
+
const dir = join4(root, ".changeset");
|
|
476
|
+
if (!existsSync3(dir)) return [];
|
|
477
|
+
const changed = [];
|
|
478
|
+
for (const entry of await readdir2(dir)) {
|
|
479
|
+
if (!entry.endsWith(".md")) continue;
|
|
480
|
+
const path = join4(dir, entry);
|
|
481
|
+
const before = await readText(path);
|
|
482
|
+
const after = rewritePackageRefs(before, r);
|
|
483
|
+
if (after !== before) {
|
|
484
|
+
await writeText(path, after);
|
|
485
|
+
changed.push(`.changeset/${entry}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return changed;
|
|
489
|
+
}
|
|
490
|
+
async function rewriteLockfile(root, r) {
|
|
491
|
+
const path = join4(root, "pnpm-lock.yaml");
|
|
492
|
+
if (!existsSync3(path)) return [];
|
|
493
|
+
const before = await readText(path);
|
|
494
|
+
const after = rewritePackageRefs(before, r);
|
|
495
|
+
if (after === before) return [];
|
|
496
|
+
await writeText(path, after);
|
|
497
|
+
return ["pnpm-lock.yaml"];
|
|
498
|
+
}
|
|
499
|
+
async function rewriteScopeFiles(root, files, r) {
|
|
500
|
+
const changed = [];
|
|
501
|
+
for (const rel of files) {
|
|
502
|
+
const path = join4(root, rel);
|
|
503
|
+
if (!existsSync3(path)) continue;
|
|
504
|
+
const before = await readText(path);
|
|
505
|
+
const after = rewriteScopeMentions(before, r);
|
|
506
|
+
if (after !== before) {
|
|
507
|
+
await writeText(path, after);
|
|
508
|
+
changed.push(rel);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return changed;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/lib/run.ts
|
|
515
|
+
import { spawn } from "child_process";
|
|
516
|
+
var runPnpm = (args, cwd) => new Promise((resolve3, reject) => {
|
|
517
|
+
const child = spawn("pnpm", args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
518
|
+
child.on("error", (error) => {
|
|
519
|
+
reject(
|
|
520
|
+
error.code === "ENOENT" ? new Error(
|
|
521
|
+
"pnpm was not found on your PATH. Enable it with `corepack enable` (ships with Node) or see https://pnpm.io/installation."
|
|
522
|
+
) : error
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
child.on("close", (code) => resolve3(code ?? 1));
|
|
526
|
+
});
|
|
527
|
+
function capture(command, args, cwd) {
|
|
528
|
+
return new Promise((resolve3, reject) => {
|
|
529
|
+
const child = spawn(command, args, {
|
|
530
|
+
cwd,
|
|
531
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
532
|
+
shell: process.platform === "win32"
|
|
533
|
+
});
|
|
534
|
+
let stdout = "";
|
|
535
|
+
let stderr = "";
|
|
536
|
+
child.stdout?.on("data", (chunk) => {
|
|
537
|
+
stdout += chunk.toString();
|
|
538
|
+
});
|
|
539
|
+
child.stderr?.on("data", (chunk) => {
|
|
540
|
+
stderr += chunk.toString();
|
|
541
|
+
});
|
|
542
|
+
child.on("error", reject);
|
|
543
|
+
child.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async function isGitTreeClean(cwd) {
|
|
547
|
+
try {
|
|
548
|
+
const result = await capture("git", ["status", "--porcelain"], cwd);
|
|
549
|
+
return result.code === 0 && result.stdout.trim() === "";
|
|
550
|
+
} catch {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/lib/template.ts
|
|
556
|
+
import { existsSync as existsSync4 } from "fs";
|
|
557
|
+
import { mkdir as mkdir2, mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
558
|
+
import { tmpdir } from "os";
|
|
559
|
+
import { join as join5, resolve } from "path";
|
|
560
|
+
import { extract } from "tar";
|
|
561
|
+
var TEMPLATE_REPO = "behivetech/bht-component-registry";
|
|
562
|
+
var TEMPLATE_PACKAGE = "create-bht-component-registry";
|
|
563
|
+
function templateTag(version) {
|
|
564
|
+
return `${TEMPLATE_PACKAGE}@${version}`;
|
|
565
|
+
}
|
|
566
|
+
function tagTarballUrl(version) {
|
|
567
|
+
return `https://github.com/${TEMPLATE_REPO}/archive/refs/tags/${encodeURIComponent(templateTag(version))}.tar.gz`;
|
|
568
|
+
}
|
|
569
|
+
function mainTarballUrl() {
|
|
570
|
+
return `https://github.com/${TEMPLATE_REPO}/archive/refs/heads/main.tar.gz`;
|
|
571
|
+
}
|
|
572
|
+
async function download(version, log2) {
|
|
573
|
+
let url = tagTarballUrl(version);
|
|
574
|
+
log2.info(`Downloading template ${templateTag(version)} from GitHub...`);
|
|
575
|
+
let response = await fetch(url, { redirect: "follow" });
|
|
576
|
+
if (response.status === 404) {
|
|
577
|
+
log2.warn(`No release tag ${templateTag(version)} on GitHub; falling back to the main branch.`);
|
|
578
|
+
url = mainTarballUrl();
|
|
579
|
+
response = await fetch(url, { redirect: "follow" });
|
|
580
|
+
}
|
|
581
|
+
if (!response.ok) {
|
|
582
|
+
throw new Error(`Template download failed: ${url} returned HTTP ${response.status}.`);
|
|
583
|
+
}
|
|
584
|
+
return { bytes: Buffer.from(await response.arrayBuffer()), url };
|
|
585
|
+
}
|
|
586
|
+
var fetchTemplate = async (request, log2) => {
|
|
587
|
+
if (request.from !== void 0) {
|
|
588
|
+
const dir = resolve(request.from);
|
|
589
|
+
if (!existsSync4(join5(dir, CONFIG_FILE))) {
|
|
590
|
+
throw new Error(`--from ${request.from} is not a bht-component-registry checkout (no ${CONFIG_FILE} in it).`);
|
|
591
|
+
}
|
|
592
|
+
log2.info(`Using local template checkout ${dir}`);
|
|
593
|
+
return { dir, source: dir, cleanup: async () => void 0 };
|
|
594
|
+
}
|
|
595
|
+
const tmp = await mkdtemp(join5(tmpdir(), "bht-template-"));
|
|
596
|
+
const cleanup = () => rm2(tmp, { recursive: true, force: true });
|
|
597
|
+
try {
|
|
598
|
+
const { bytes, url } = await download(request.version, log2);
|
|
599
|
+
const tarball = join5(tmp, "template.tar.gz");
|
|
600
|
+
await writeFile2(tarball, bytes);
|
|
601
|
+
const dir = join5(tmp, "template");
|
|
602
|
+
await mkdir2(dir);
|
|
603
|
+
await extract({ file: tarball, cwd: dir, strip: 1 });
|
|
604
|
+
await rm2(tarball, { force: true });
|
|
605
|
+
if (!existsSync4(join5(dir, CONFIG_FILE))) {
|
|
606
|
+
throw new Error(`The downloaded template has no ${CONFIG_FILE}; ${url} does not look like a bht-component-registry release.`);
|
|
607
|
+
}
|
|
608
|
+
return { dir, source: url, cleanup };
|
|
609
|
+
} catch (error) {
|
|
610
|
+
await cleanup();
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// src/lib/version.ts
|
|
616
|
+
import { existsSync as existsSync5, readFileSync } from "fs";
|
|
617
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
618
|
+
import { fileURLToPath } from "url";
|
|
619
|
+
var CLI_NAME = "create-bht-component-registry";
|
|
620
|
+
function readOwnVersion() {
|
|
621
|
+
let dir = dirname3(fileURLToPath(import.meta.url));
|
|
622
|
+
for (let depth = 0; depth < 5; depth += 1) {
|
|
623
|
+
const candidate = join6(dir, "package.json");
|
|
624
|
+
if (existsSync5(candidate)) {
|
|
625
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf8"));
|
|
626
|
+
if (pkg.name === CLI_NAME && typeof pkg.version === "string") return pkg.version;
|
|
627
|
+
}
|
|
628
|
+
const parent = dirname3(dir);
|
|
629
|
+
if (parent === dir) break;
|
|
630
|
+
dir = parent;
|
|
631
|
+
}
|
|
632
|
+
throw new Error(`Cannot find ${CLI_NAME}'s package.json to read its version.`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/commands/create.ts
|
|
636
|
+
var DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
637
|
+
var DEFAULT_CATEGORIES = ["atoms", "molecules", "organisms", "templates", "forms"];
|
|
638
|
+
var DEFAULT_DIR = "my-registry";
|
|
639
|
+
var CATEGORY_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
640
|
+
function defaultDeps() {
|
|
641
|
+
return { fetchTemplate, runPnpm, prompt: interactivePrompter, cwd: process.cwd(), log };
|
|
642
|
+
}
|
|
643
|
+
async function resolveDir(options, deps) {
|
|
644
|
+
if (options.dir) return options.dir;
|
|
645
|
+
if (options.yes) return DEFAULT_DIR;
|
|
646
|
+
const value = await deps.prompt.text({
|
|
647
|
+
message: "Directory for the new registry",
|
|
648
|
+
initial: DEFAULT_DIR,
|
|
649
|
+
validate: (v) => v.trim() === "" ? "A directory name is required." : true
|
|
650
|
+
});
|
|
651
|
+
return value?.trim();
|
|
652
|
+
}
|
|
653
|
+
async function resolveScope(options, deps) {
|
|
654
|
+
if (options.scope !== void 0) {
|
|
655
|
+
const scope = options.scope.trim();
|
|
656
|
+
const problem = scopeProblemMessage(scope);
|
|
657
|
+
if (problem) {
|
|
658
|
+
deps.log.error(problem);
|
|
659
|
+
return void 0;
|
|
660
|
+
}
|
|
661
|
+
return scope;
|
|
662
|
+
}
|
|
663
|
+
if (options.yes) {
|
|
664
|
+
deps.log.error(`--yes needs --scope. ${scopeProblemMessage("")}`);
|
|
665
|
+
return void 0;
|
|
666
|
+
}
|
|
667
|
+
const value = await deps.prompt.text({
|
|
668
|
+
message: "npm scope, without the @ (your components publish as @<scope>/<category>.<name>)",
|
|
669
|
+
validate: (v) => scopeProblemMessage(v.trim()) ?? true
|
|
670
|
+
});
|
|
671
|
+
return value?.trim();
|
|
672
|
+
}
|
|
673
|
+
function registryProblem(value) {
|
|
674
|
+
try {
|
|
675
|
+
const url = new URL(value);
|
|
676
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return "The registry must be an http(s) URL.";
|
|
677
|
+
return null;
|
|
678
|
+
} catch {
|
|
679
|
+
return `"${value}" is not a URL. Example: ${DEFAULT_REGISTRY}`;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function resolveRegistry(options, deps) {
|
|
683
|
+
if (options.registry !== void 0) {
|
|
684
|
+
const problem = registryProblem(options.registry);
|
|
685
|
+
if (problem) {
|
|
686
|
+
deps.log.error(problem);
|
|
687
|
+
return void 0;
|
|
688
|
+
}
|
|
689
|
+
return options.registry.replace(/\/+$/, "");
|
|
690
|
+
}
|
|
691
|
+
if (options.yes) return DEFAULT_REGISTRY;
|
|
692
|
+
const value = await deps.prompt.text({
|
|
693
|
+
message: "npm registry to publish to",
|
|
694
|
+
initial: DEFAULT_REGISTRY,
|
|
695
|
+
validate: (v) => registryProblem(v.trim()) ?? true
|
|
696
|
+
});
|
|
697
|
+
return value?.trim().replace(/\/+$/, "");
|
|
698
|
+
}
|
|
699
|
+
function parseCategories(value) {
|
|
700
|
+
const list = Array.isArray(value) ? value : value.split(",");
|
|
701
|
+
return [...new Set(list.map((item) => item.trim()).filter((item) => item !== ""))];
|
|
702
|
+
}
|
|
703
|
+
function categoriesProblem(categories) {
|
|
704
|
+
if (categories.length === 0) return "At least one category is required, e.g. atoms.";
|
|
705
|
+
const bad = categories.find((category) => !CATEGORY_PATTERN.test(category));
|
|
706
|
+
return bad ? `"${bad}" is not a valid category: lowercase letters, digits and hyphens only.` : null;
|
|
707
|
+
}
|
|
708
|
+
async function resolveCategories(options, deps) {
|
|
709
|
+
if (options.categories !== void 0) {
|
|
710
|
+
const categories = parseCategories(options.categories);
|
|
711
|
+
const problem = categoriesProblem(categories);
|
|
712
|
+
if (problem) {
|
|
713
|
+
deps.log.error(problem);
|
|
714
|
+
return void 0;
|
|
715
|
+
}
|
|
716
|
+
return categories;
|
|
717
|
+
}
|
|
718
|
+
if (options.yes) return DEFAULT_CATEGORIES;
|
|
719
|
+
const value = await deps.prompt.text({
|
|
720
|
+
message: "Starter categories, comma-separated",
|
|
721
|
+
initial: DEFAULT_CATEGORIES.join(","),
|
|
722
|
+
validate: (v) => categoriesProblem(parseCategories(v)) ?? true
|
|
723
|
+
});
|
|
724
|
+
return value === void 0 ? void 0 : parseCategories(value);
|
|
725
|
+
}
|
|
726
|
+
async function writeConfig(target, choices) {
|
|
727
|
+
const path = join7(target, CONFIG_FILE);
|
|
728
|
+
const base = existsSync6(path) ? parseRegistryConfig(await readText(path)) : {
|
|
729
|
+
$schema: "./registry.config.schema.json",
|
|
730
|
+
scope: choices.scope,
|
|
731
|
+
npmRegistry: choices.registry,
|
|
732
|
+
templateVersion: choices.templateVersion,
|
|
733
|
+
site: { title: "" }
|
|
734
|
+
};
|
|
735
|
+
await writeRegistryConfig(target, {
|
|
736
|
+
...base,
|
|
737
|
+
scope: choices.scope,
|
|
738
|
+
npmRegistry: choices.registry,
|
|
739
|
+
templateVersion: choices.templateVersion,
|
|
740
|
+
site: {
|
|
741
|
+
...base.site,
|
|
742
|
+
title: `${titleCaseScope(choices.scope)} Component Registry`,
|
|
743
|
+
links: (base.site.links ?? []).filter((link) => link.label !== "GitHub")
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
async function ensureCategories(target, scope, categories) {
|
|
748
|
+
for (const category of categories) {
|
|
749
|
+
const dir = join7(target, "registry", scope, category);
|
|
750
|
+
await mkdir3(dir, { recursive: true });
|
|
751
|
+
if ((await readdir3(dir)).length === 0) await writeText(join7(dir, ".gitkeep"), "");
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
async function readManifestOrDefault(dir) {
|
|
755
|
+
return existsSync6(join7(dir, MANIFEST_FILE)) ? readManifest(dir) : DEFAULT_MANIFEST;
|
|
756
|
+
}
|
|
757
|
+
function skipTemplateOnly(manifest) {
|
|
758
|
+
return (relPath) => isTemplateOnly(relPath, manifest) || isTemplateOnly(`${relPath}/`, manifest);
|
|
759
|
+
}
|
|
760
|
+
async function applyScope(target, choices) {
|
|
761
|
+
const rename2 = scopeRename(choices.scope);
|
|
762
|
+
await renameScopeTree(target, rename2);
|
|
763
|
+
await rewriteChangesets(target, rename2);
|
|
764
|
+
await rewriteLockfile(target, rename2);
|
|
765
|
+
await ensureCategories(target, choices.scope, choices.categories);
|
|
766
|
+
await writeConfig(target, choices);
|
|
767
|
+
const manifest = await readManifestOrDefault(target);
|
|
768
|
+
await rewriteScopeFiles(target, manifest.rewriteScope, rename2);
|
|
769
|
+
}
|
|
770
|
+
function printNextSteps(dir, scope, installed, log2) {
|
|
771
|
+
log2.blank();
|
|
772
|
+
log2.success(`Created ${dir} for @${scope}/*`);
|
|
773
|
+
log2.blank();
|
|
774
|
+
log2.info("Next steps:");
|
|
775
|
+
log2.blank();
|
|
776
|
+
log2.info(` cd ${dir}`);
|
|
777
|
+
if (!installed) log2.info(" pnpm install");
|
|
778
|
+
log2.info(" pnpm dev");
|
|
779
|
+
log2.blank();
|
|
780
|
+
log2.info(`Create the npm org for @${scope} at https://www.npmjs.com/org/create before your first \`pnpm release\`.`);
|
|
781
|
+
log2.info("Guides: docs/getting-started.md");
|
|
782
|
+
log2.info("Hosted & expanded version: https://platform.behivetech.com/expanded");
|
|
783
|
+
log2.blank();
|
|
784
|
+
}
|
|
785
|
+
async function create(options, overrides = {}) {
|
|
786
|
+
const deps = { ...defaultDeps(), ...overrides };
|
|
787
|
+
const { log: log2 } = deps;
|
|
788
|
+
try {
|
|
789
|
+
const dir = await resolveDir(options, deps);
|
|
790
|
+
if (dir === void 0) return cancelled(log2);
|
|
791
|
+
const target = resolve2(deps.cwd, dir);
|
|
792
|
+
if (!await isDirEmpty(target)) {
|
|
793
|
+
log2.error(`${dir} already exists and is not empty. Pick a new directory.`);
|
|
794
|
+
return 1;
|
|
795
|
+
}
|
|
796
|
+
const scope = await resolveScope(options, deps);
|
|
797
|
+
if (scope === void 0) return options.scope === void 0 && !options.yes ? cancelled(log2) : 1;
|
|
798
|
+
const registry = await resolveRegistry(options, deps);
|
|
799
|
+
if (registry === void 0) return options.registry === void 0 && !options.yes ? cancelled(log2) : 1;
|
|
800
|
+
const categories = await resolveCategories(options, deps);
|
|
801
|
+
if (categories === void 0) return options.categories === void 0 && !options.yes ? cancelled(log2) : 1;
|
|
802
|
+
const templateVersion = options.templateVersion ?? readOwnVersion();
|
|
803
|
+
if (!isVersion(templateVersion)) {
|
|
804
|
+
log2.error(`--template-version must be x.y.z, got "${templateVersion}".`);
|
|
805
|
+
return 1;
|
|
806
|
+
}
|
|
807
|
+
log2.blank();
|
|
808
|
+
log2.info(`Creating ${dir} for @${scope}/* ...`);
|
|
809
|
+
const template = await deps.fetchTemplate({ version: templateVersion, from: options.from }, log2);
|
|
810
|
+
try {
|
|
811
|
+
if (isInside(template.dir, target)) {
|
|
812
|
+
log2.error(`${dir} is inside the template checkout; pick a directory outside ${template.dir}.`);
|
|
813
|
+
return 1;
|
|
814
|
+
}
|
|
815
|
+
const manifest = await readManifestOrDefault(template.dir);
|
|
816
|
+
await copyTree(template.dir, target, skipTemplateOnly(manifest));
|
|
817
|
+
} finally {
|
|
818
|
+
await template.cleanup();
|
|
819
|
+
}
|
|
820
|
+
await applyScope(target, { scope, registry, categories, templateVersion });
|
|
821
|
+
log2.success(`Scaffolded registry/${scope} and registry.config.json`);
|
|
822
|
+
if (options.install) {
|
|
823
|
+
for (const step of ["install", "build"]) {
|
|
824
|
+
log2.blank();
|
|
825
|
+
log2.info(`Running pnpm ${step}...`);
|
|
826
|
+
const code = await deps.runPnpm([step], target);
|
|
827
|
+
if (code !== 0) {
|
|
828
|
+
log2.blank();
|
|
829
|
+
log2.error(`pnpm ${step} failed (exit code ${code}). The project is in ${dir}; fix the error above and rerun \`pnpm ${step}\` there.`);
|
|
830
|
+
return 1;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
printNextSteps(dir, scope, options.install, log2);
|
|
835
|
+
return 0;
|
|
836
|
+
} catch (error) {
|
|
837
|
+
log2.error(errorMessage(error));
|
|
838
|
+
return 1;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function cancelled(log2) {
|
|
842
|
+
log2.info("Cancelled; nothing was created.");
|
|
843
|
+
return 1;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/commands/update.ts
|
|
847
|
+
import { existsSync as existsSync7 } from "fs";
|
|
848
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
849
|
+
import { join as join8 } from "path";
|
|
850
|
+
function defaultDeps2() {
|
|
851
|
+
return { fetchTemplate, isTreeClean: isGitTreeClean, prompt: interactivePrompter, cwd: process.cwd(), log };
|
|
852
|
+
}
|
|
853
|
+
function sameBytes(local, content) {
|
|
854
|
+
return typeof content === "string" ? local.equals(Buffer.from(content, "utf8")) : local.equals(content);
|
|
855
|
+
}
|
|
856
|
+
async function planReadme(root, templateDir, marker) {
|
|
857
|
+
const targetPath = join8(templateDir, "README.md");
|
|
858
|
+
if (!existsSync7(targetPath)) return { status: "absent" };
|
|
859
|
+
const target = await readText(targetPath);
|
|
860
|
+
const localPath = join8(root, "README.md");
|
|
861
|
+
if (!existsSync7(localPath)) return { status: "added", content: target };
|
|
862
|
+
const local = await readText(localPath);
|
|
863
|
+
if (!splitReadme(local, marker)) return { status: "no-marker" };
|
|
864
|
+
const merged = mergeReadme(local, target, marker);
|
|
865
|
+
if (merged === null || merged === local) return { status: "unchanged" };
|
|
866
|
+
return { status: "merged", content: merged };
|
|
867
|
+
}
|
|
868
|
+
async function planUpdate(root, templateDir, manifest, rename2) {
|
|
869
|
+
const owned = (file) => file !== "README.md" && isTemplateOwned(file, manifest);
|
|
870
|
+
const targetFiles = (await listFiles(templateDir)).filter(owned);
|
|
871
|
+
const allLocal = await listFiles(root);
|
|
872
|
+
const localFiles = allLocal.filter(owned);
|
|
873
|
+
const strayTemplateOnly = allLocal.filter((file) => isTemplateOnly(file, manifest));
|
|
874
|
+
const targetSet = new Set(targetFiles);
|
|
875
|
+
const localSet = new Set(localFiles);
|
|
876
|
+
const rewriteSet = new Set(manifest.rewriteScope);
|
|
877
|
+
const added = [];
|
|
878
|
+
const replaced = [];
|
|
879
|
+
const rewritten = [];
|
|
880
|
+
for (const file of targetFiles) {
|
|
881
|
+
let content = await readFile2(join8(templateDir, file));
|
|
882
|
+
if (rewriteSet.has(file)) {
|
|
883
|
+
const original = content.toString("utf8");
|
|
884
|
+
content = rewriteScopeMentions(original, rename2);
|
|
885
|
+
if (content !== original) rewritten.push(file);
|
|
886
|
+
}
|
|
887
|
+
if (!localSet.has(file)) {
|
|
888
|
+
added.push({ path: file, content });
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (!sameBytes(await readFile2(join8(root, file)), content)) replaced.push({ path: file, content });
|
|
892
|
+
}
|
|
893
|
+
const removed = [...localFiles.filter((file) => !targetSet.has(file)), ...strayTemplateOnly].sort();
|
|
894
|
+
const readme = await planReadme(root, templateDir, manifest.readmeMarker);
|
|
895
|
+
return { added, replaced, removed, readme, rewritten };
|
|
896
|
+
}
|
|
897
|
+
async function applyPlan(root, plan) {
|
|
898
|
+
for (const change of [...plan.added, ...plan.replaced]) {
|
|
899
|
+
await writeBytes(join8(root, change.path), change.content);
|
|
900
|
+
}
|
|
901
|
+
for (const file of plan.removed) await removeFile(root, file);
|
|
902
|
+
if (plan.readme.content !== void 0) await writeText(join8(root, "README.md"), plan.readme.content);
|
|
903
|
+
}
|
|
904
|
+
function printList(title, files, log2) {
|
|
905
|
+
if (files.length === 0) return;
|
|
906
|
+
log2.info(`${title} (${files.length}):`);
|
|
907
|
+
for (const file of files) log2.info(` ${file}`);
|
|
908
|
+
log2.blank();
|
|
909
|
+
}
|
|
910
|
+
function planIsEmpty(plan) {
|
|
911
|
+
return plan.added.length === 0 && plan.replaced.length === 0 && plan.removed.length === 0 && plan.readme.content === void 0;
|
|
912
|
+
}
|
|
913
|
+
function printPlan(plan, manifest, scope, log2) {
|
|
914
|
+
printList("Added", plan.added.map((change) => change.path), log2);
|
|
915
|
+
printList("Replaced", plan.replaced.map((change) => change.path), log2);
|
|
916
|
+
printList("Removed", plan.removed, log2);
|
|
917
|
+
switch (plan.readme.status) {
|
|
918
|
+
case "added":
|
|
919
|
+
log2.info("README.md: added from the template.");
|
|
920
|
+
break;
|
|
921
|
+
case "merged":
|
|
922
|
+
log2.info("README.md: the part above the marker is replaced; your notes below it are kept.");
|
|
923
|
+
break;
|
|
924
|
+
case "no-marker":
|
|
925
|
+
log2.warn(`README.md has no "${manifest.readmeMarker}" line, so it is left untouched. Put that line above your own notes to let update refresh the template's part.`);
|
|
926
|
+
break;
|
|
927
|
+
default:
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
if (plan.rewritten.length > 0) {
|
|
931
|
+
log2.info(`Scope "${scope}" re-applied in: ${plan.rewritten.join(", ")}`);
|
|
932
|
+
}
|
|
933
|
+
if (planIsEmpty(plan)) log2.info("Every template-owned file already matches the target.");
|
|
934
|
+
}
|
|
935
|
+
function isBehind(current, target, log2) {
|
|
936
|
+
if (compareVersions(current, target) >= 0) {
|
|
937
|
+
log2.info(`Already at template ${current} (target ${target}); nothing to do.`);
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
async function update(options, overrides = {}) {
|
|
943
|
+
const deps = { ...defaultDeps2(), ...overrides };
|
|
944
|
+
const { log: log2 } = deps;
|
|
945
|
+
try {
|
|
946
|
+
const root = findRegistryRoot(deps.cwd);
|
|
947
|
+
if (!root) {
|
|
948
|
+
log2.error(`No ${CONFIG_FILE} found in ${deps.cwd} or any parent directory. Run update inside a registry created with create-bht-component-registry.`);
|
|
949
|
+
return 1;
|
|
950
|
+
}
|
|
951
|
+
if (!await deps.isTreeClean(root)) {
|
|
952
|
+
log2.error("Commit or stash your changes first; update replaces template-owned files and you'll want a clean diff.");
|
|
953
|
+
return 1;
|
|
954
|
+
}
|
|
955
|
+
const config = await readRegistryConfig(root);
|
|
956
|
+
const requested = options.to ?? readOwnVersion();
|
|
957
|
+
if (!isVersion(requested)) {
|
|
958
|
+
log2.error(`--to must be x.y.z, got "${requested}".`);
|
|
959
|
+
return 1;
|
|
960
|
+
}
|
|
961
|
+
if (options.from === void 0 && !isBehind(config.templateVersion, requested, log2)) return 0;
|
|
962
|
+
const template = await deps.fetchTemplate({ version: requested, from: options.from }, log2);
|
|
963
|
+
try {
|
|
964
|
+
const targetVersion = options.from !== void 0 && options.to === void 0 ? (await readRegistryConfig(template.dir)).templateVersion : requested;
|
|
965
|
+
if (options.from !== void 0 && !isBehind(config.templateVersion, targetVersion, log2)) return 0;
|
|
966
|
+
const manifest = await readManifest(template.dir);
|
|
967
|
+
const rename2 = scopeRename(config.scope);
|
|
968
|
+
const plan = await planUpdate(root, template.dir, manifest, rename2);
|
|
969
|
+
const prefix = options.dryRun ? "[dry run] " : "";
|
|
970
|
+
log2.blank();
|
|
971
|
+
log2.info(`${prefix}Updating template ${config.templateVersion} \u2192 ${targetVersion} in ${root}`);
|
|
972
|
+
log2.blank();
|
|
973
|
+
printPlan(plan, manifest, config.scope, log2);
|
|
974
|
+
if (options.dryRun) {
|
|
975
|
+
log2.info("Dry run: nothing was changed.");
|
|
976
|
+
return 0;
|
|
977
|
+
}
|
|
978
|
+
if (!options.yes) {
|
|
979
|
+
const ok = await deps.prompt.confirm({ message: "Apply these changes?", initial: true });
|
|
980
|
+
if (!ok) {
|
|
981
|
+
log2.info("Cancelled; nothing was changed.");
|
|
982
|
+
return 1;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
await applyPlan(root, plan);
|
|
986
|
+
await writeRegistryConfig(root, { ...config, templateVersion: targetVersion });
|
|
987
|
+
log2.blank();
|
|
988
|
+
log2.success(`Updated to template ${targetVersion}.`);
|
|
989
|
+
log2.info("Review with `git diff`, then run `pnpm install && pnpm verify`.");
|
|
990
|
+
log2.blank();
|
|
991
|
+
return 0;
|
|
992
|
+
} finally {
|
|
993
|
+
await template.cleanup();
|
|
994
|
+
}
|
|
995
|
+
} catch (error) {
|
|
996
|
+
log2.error(errorMessage(error));
|
|
997
|
+
return 1;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// src/cli.ts
|
|
1002
|
+
var HELP = `
|
|
1003
|
+
${CLI_NAME} \u2014 scaffold a component registry that stays in git, and keep it current.
|
|
1004
|
+
|
|
1005
|
+
Usage
|
|
1006
|
+
npx ${CLI_NAME} <dir> [options] scaffold a new registry
|
|
1007
|
+
npx ${CLI_NAME} create <dir> [options] same, spelled out
|
|
1008
|
+
npx ${CLI_NAME} update [options] run inside an existing registry
|
|
1009
|
+
|
|
1010
|
+
create options
|
|
1011
|
+
--scope <name> npm scope without the @ (required; prompted if missing)
|
|
1012
|
+
--registry <url> registry to publish to (default: https://registry.npmjs.org)
|
|
1013
|
+
--categories <a,b,c> starter categories (default: atoms,molecules,organisms,templates,forms)
|
|
1014
|
+
--template-version <x.y.z> template release to download (default: this CLI's version)
|
|
1015
|
+
--no-install skip \`pnpm install\` and \`pnpm build\`
|
|
1016
|
+
-y, --yes accept defaults instead of prompting
|
|
1017
|
+
|
|
1018
|
+
update options
|
|
1019
|
+
--to <x.y.z> template version to update to (default: this CLI's version)
|
|
1020
|
+
--dry-run list what would change and change nothing
|
|
1021
|
+
-y, --yes apply without asking
|
|
1022
|
+
|
|
1023
|
+
both
|
|
1024
|
+
--from <path> dev mode: use a local checkout of the template instead of downloading
|
|
1025
|
+
-h, --help show this help
|
|
1026
|
+
-v, --version print the version
|
|
1027
|
+
|
|
1028
|
+
Hosted & expanded version: https://platform.behivetech.com
|
|
1029
|
+
`;
|
|
1030
|
+
async function main(argv) {
|
|
1031
|
+
let parsed;
|
|
1032
|
+
try {
|
|
1033
|
+
parsed = parseArgs({
|
|
1034
|
+
args: argv,
|
|
1035
|
+
allowPositionals: true,
|
|
1036
|
+
options: {
|
|
1037
|
+
scope: { type: "string" },
|
|
1038
|
+
registry: { type: "string" },
|
|
1039
|
+
categories: { type: "string" },
|
|
1040
|
+
"template-version": { type: "string" },
|
|
1041
|
+
to: { type: "string" },
|
|
1042
|
+
from: { type: "string" },
|
|
1043
|
+
"no-install": { type: "boolean" },
|
|
1044
|
+
"dry-run": { type: "boolean" },
|
|
1045
|
+
yes: { type: "boolean", short: "y" },
|
|
1046
|
+
help: { type: "boolean", short: "h" },
|
|
1047
|
+
version: { type: "boolean", short: "v" }
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
log.error(`${errorMessage(error)} Run with --help for usage.`);
|
|
1052
|
+
return 1;
|
|
1053
|
+
}
|
|
1054
|
+
const { values, positionals } = parsed;
|
|
1055
|
+
const string = (key) => {
|
|
1056
|
+
const value = values[key];
|
|
1057
|
+
return typeof value === "string" ? value : void 0;
|
|
1058
|
+
};
|
|
1059
|
+
const flag = (key) => values[key] === true;
|
|
1060
|
+
if (flag("version")) {
|
|
1061
|
+
console.log(readOwnVersion());
|
|
1062
|
+
return 0;
|
|
1063
|
+
}
|
|
1064
|
+
if (flag("help") || positionals.length === 0) {
|
|
1065
|
+
console.log(HELP);
|
|
1066
|
+
return flag("help") ? 0 : 1;
|
|
1067
|
+
}
|
|
1068
|
+
const [command, ...rest] = positionals;
|
|
1069
|
+
if (command === "update") {
|
|
1070
|
+
return update({ to: string("to"), from: string("from"), dryRun: flag("dry-run"), yes: flag("yes") });
|
|
1071
|
+
}
|
|
1072
|
+
const dir = command === "create" ? rest[0] : command;
|
|
1073
|
+
const categories = string("categories");
|
|
1074
|
+
return create({
|
|
1075
|
+
dir,
|
|
1076
|
+
scope: string("scope"),
|
|
1077
|
+
registry: string("registry"),
|
|
1078
|
+
categories: categories === void 0 ? void 0 : categories.split(","),
|
|
1079
|
+
templateVersion: string("template-version"),
|
|
1080
|
+
from: string("from"),
|
|
1081
|
+
install: !flag("no-install"),
|
|
1082
|
+
yes: flag("yes")
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
main(process.argv.slice(2)).then(
|
|
1086
|
+
(code) => {
|
|
1087
|
+
process.exitCode = code;
|
|
1088
|
+
},
|
|
1089
|
+
(error) => {
|
|
1090
|
+
log.error(errorMessage(error));
|
|
1091
|
+
process.exitCode = 1;
|
|
1092
|
+
}
|
|
1093
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-bht-component-registry",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Scaffold a bht-component-registry — a component registry that stays in git — and keep it up to date with `update`.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"component-registry",
|
|
7
|
+
"design-system",
|
|
8
|
+
"create",
|
|
9
|
+
"scaffold",
|
|
10
|
+
"nextjs",
|
|
11
|
+
"pnpm-workspace",
|
|
12
|
+
"changesets"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://platform.behivetech.com",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/behivetech/bht-component-registry.git",
|
|
18
|
+
"directory": "packages/create-bht-component-registry"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"create-bht-component-registry": "./dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"picocolors": "^1.1.1",
|
|
33
|
+
"prompts": "^2.4.2",
|
|
34
|
+
"tar": "^7.5.22"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.20.4",
|
|
38
|
+
"@types/prompts": "^2.4.9",
|
|
39
|
+
"eslint": "^9.39.5",
|
|
40
|
+
"tsup": "^8.5.1",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.1.10",
|
|
43
|
+
"@bht-component-registry/eslint-config": "0.1.0",
|
|
44
|
+
"@bht-component-registry/scope": "0.1.0",
|
|
45
|
+
"@bht-component-registry/typescript-config": "0.0.2"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"registry": "https://registry.npmjs.org"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"dev": "tsup --watch",
|
|
54
|
+
"lint": "eslint . --max-warnings 0",
|
|
55
|
+
"check-types": "tsc --noEmit",
|
|
56
|
+
"test": "vitest run"
|
|
57
|
+
}
|
|
6
58
|
}
|