lorre-blocks 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 +53 -0
- package/dist/index.js +351 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# lorre-blocks
|
|
2
|
+
|
|
3
|
+
CLI for [lorre-blocks](https://github.com/zukazine/lorre-blocks) — a shadcn-style
|
|
4
|
+
component library. Instead of installing a package and importing from `node_modules`,
|
|
5
|
+
this CLI **copies component source directly into your project**, so you own and can
|
|
6
|
+
edit every line.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
In your React + Tailwind project:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 1. One-time setup — writes components.json
|
|
14
|
+
npx lorre-blocks init
|
|
15
|
+
|
|
16
|
+
# 2. Add components — copies their source in and installs their dependencies
|
|
17
|
+
npx lorre-blocks add button
|
|
18
|
+
npx lorre-blocks add input
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
### `init`
|
|
24
|
+
Creates a `components.json` in your project describing the registry URL and your
|
|
25
|
+
import aliases.
|
|
26
|
+
|
|
27
|
+
| Option | Description |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `-r, --registry <url>` | Registry base URL to pull components from |
|
|
30
|
+
| `-y, --yes` | Accept defaults, skip prompts |
|
|
31
|
+
| `-c, --cwd <path>` | Run in a different directory |
|
|
32
|
+
|
|
33
|
+
### `add [components...]`
|
|
34
|
+
Fetches each component from the registry, installs its npm dependencies with your
|
|
35
|
+
project's package manager, resolves any component dependencies, rewrites import
|
|
36
|
+
aliases to match your `components.json`, and writes the files into your project.
|
|
37
|
+
|
|
38
|
+
| Option | Description |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `-o, --overwrite` | Overwrite existing files without asking |
|
|
41
|
+
| `-y, --yes` | Skip confirmation prompts |
|
|
42
|
+
| `-c, --cwd <path>` | Run in a different directory |
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
Components are described in a JSON **registry** served as static files. The CLI reads
|
|
47
|
+
`components.json`, fetches `<registry>/r/<name>.json`, resolves dependencies, and writes
|
|
48
|
+
the source into the directories your aliases point at (detected from your `tsconfig.json`
|
|
49
|
+
`paths`).
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { promises as fs2 } from "fs";
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
import * as p from "@clack/prompts";
|
|
10
|
+
import color from "picocolors";
|
|
11
|
+
|
|
12
|
+
// src/utils/config.ts
|
|
13
|
+
import { promises as fs } from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
var CONFIG_FILE = "components.json";
|
|
16
|
+
var DEFAULT_REGISTRY = "http://localhost:3000";
|
|
17
|
+
var DEFAULT_ALIASES = {
|
|
18
|
+
components: "@/components",
|
|
19
|
+
ui: "@/components/ui",
|
|
20
|
+
lib: "@/lib",
|
|
21
|
+
hooks: "@/hooks",
|
|
22
|
+
utils: "@/lib/utils"
|
|
23
|
+
};
|
|
24
|
+
function configPath(cwd) {
|
|
25
|
+
return path.join(cwd, CONFIG_FILE);
|
|
26
|
+
}
|
|
27
|
+
async function configExists(cwd) {
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(configPath(cwd));
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function readConfig(cwd) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = await fs.readFile(configPath(cwd), "utf8");
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function writeConfig(cwd, config) {
|
|
44
|
+
await fs.writeFile(
|
|
45
|
+
configPath(cwd),
|
|
46
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
47
|
+
"utf8"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/commands/init.ts
|
|
52
|
+
async function runInit(options) {
|
|
53
|
+
const { cwd } = options;
|
|
54
|
+
p.intro(color.bgCyan(color.black(" lorre-blocks init ")));
|
|
55
|
+
if (await configExists(cwd)) {
|
|
56
|
+
const overwrite = options.yes ? true : await p.confirm({
|
|
57
|
+
message: "components.json already exists. Overwrite it?",
|
|
58
|
+
initialValue: false
|
|
59
|
+
});
|
|
60
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
61
|
+
p.cancel("Init aborted; existing components.json kept.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const tsx = await fileExists(path2.join(cwd, "tsconfig.json"));
|
|
66
|
+
const registry = options.registry ?? (options.yes ? DEFAULT_REGISTRY : await promptText(
|
|
67
|
+
"Registry URL",
|
|
68
|
+
DEFAULT_REGISTRY
|
|
69
|
+
));
|
|
70
|
+
const config = {
|
|
71
|
+
$schema: "https://lorre-blocks.dev/schema.json",
|
|
72
|
+
registry,
|
|
73
|
+
tsx,
|
|
74
|
+
aliases: { ...DEFAULT_ALIASES }
|
|
75
|
+
};
|
|
76
|
+
await writeConfig(cwd, config);
|
|
77
|
+
p.outro(
|
|
78
|
+
`${color.green("\u2714")} Wrote components.json. Now run ${color.cyan(
|
|
79
|
+
"lorre-blocks add button"
|
|
80
|
+
)}.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
async function promptText(message, initial) {
|
|
84
|
+
const value = await p.text({ message, placeholder: initial, defaultValue: initial });
|
|
85
|
+
if (p.isCancel(value)) {
|
|
86
|
+
p.cancel("Init cancelled.");
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
return value || initial;
|
|
90
|
+
}
|
|
91
|
+
async function fileExists(f) {
|
|
92
|
+
try {
|
|
93
|
+
await fs2.access(f);
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/commands/add.ts
|
|
101
|
+
import { promises as fs5 } from "fs";
|
|
102
|
+
import path5 from "path";
|
|
103
|
+
import * as p2 from "@clack/prompts";
|
|
104
|
+
import color2 from "picocolors";
|
|
105
|
+
|
|
106
|
+
// src/utils/registry.ts
|
|
107
|
+
async function fetchRegistryItem(registry, name) {
|
|
108
|
+
const base = registry.replace(/\/$/, "");
|
|
109
|
+
const url = `${base}/r/${name}.json`;
|
|
110
|
+
let res;
|
|
111
|
+
try {
|
|
112
|
+
res = await fetch(url);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Could not reach the registry at ${url}. Is it running / correct?
|
|
116
|
+
${err.message}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (res.status === 404) {
|
|
120
|
+
throw new Error(`Component "${name}" was not found in the registry (${url}).`);
|
|
121
|
+
}
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
throw new Error(`Failed to fetch ${url} (HTTP ${res.status}).`);
|
|
124
|
+
}
|
|
125
|
+
return await res.json();
|
|
126
|
+
}
|
|
127
|
+
async function resolveTree(registry, names) {
|
|
128
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
129
|
+
async function visit(name) {
|
|
130
|
+
if (resolved.has(name)) return;
|
|
131
|
+
const item = await fetchRegistryItem(registry, name);
|
|
132
|
+
for (const dep of item.registryDependencies ?? []) {
|
|
133
|
+
await visit(dep);
|
|
134
|
+
}
|
|
135
|
+
if (!resolved.has(name)) resolved.set(name, item);
|
|
136
|
+
}
|
|
137
|
+
for (const name of names) {
|
|
138
|
+
await visit(name);
|
|
139
|
+
}
|
|
140
|
+
return [...resolved.values()];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/utils/package-manager.ts
|
|
144
|
+
import { promises as fs3 } from "fs";
|
|
145
|
+
import path3 from "path";
|
|
146
|
+
import { spawn } from "child_process";
|
|
147
|
+
async function detectPackageManager(cwd) {
|
|
148
|
+
const has = async (f) => fs3.access(path3.join(cwd, f)).then(() => true).catch(() => false);
|
|
149
|
+
if (await has("pnpm-lock.yaml")) return "pnpm";
|
|
150
|
+
if (await has("yarn.lock")) return "yarn";
|
|
151
|
+
if (await has("bun.lockb")) return "bun";
|
|
152
|
+
return "npm";
|
|
153
|
+
}
|
|
154
|
+
function installArgs(pm, deps) {
|
|
155
|
+
switch (pm) {
|
|
156
|
+
case "pnpm":
|
|
157
|
+
return ["add", ...deps];
|
|
158
|
+
case "yarn":
|
|
159
|
+
return ["add", ...deps];
|
|
160
|
+
case "bun":
|
|
161
|
+
return ["add", ...deps];
|
|
162
|
+
case "npm":
|
|
163
|
+
return ["install", ...deps];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function installDependencies(cwd, pm, deps) {
|
|
167
|
+
if (deps.length === 0) return;
|
|
168
|
+
const args = installArgs(pm, deps);
|
|
169
|
+
await new Promise((resolve, reject) => {
|
|
170
|
+
const child = spawn(pm, args, {
|
|
171
|
+
cwd,
|
|
172
|
+
stdio: "inherit",
|
|
173
|
+
// On Windows, package managers are .cmd shims that need a shell.
|
|
174
|
+
shell: process.platform === "win32"
|
|
175
|
+
});
|
|
176
|
+
child.on("error", reject);
|
|
177
|
+
child.on("close", (code) => {
|
|
178
|
+
if (code === 0) resolve();
|
|
179
|
+
else reject(new Error(`${pm} ${args.join(" ")} exited with code ${code}`));
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/utils/paths.ts
|
|
185
|
+
import { promises as fs4 } from "fs";
|
|
186
|
+
import path4 from "path";
|
|
187
|
+
async function resolveBaseDir(cwd) {
|
|
188
|
+
for (const file of ["tsconfig.json", "jsconfig.json"]) {
|
|
189
|
+
try {
|
|
190
|
+
const raw = await fs4.readFile(path4.join(cwd, file), "utf8");
|
|
191
|
+
const json = JSON.parse(stripJsonComments(raw));
|
|
192
|
+
const paths = json?.compilerOptions?.paths;
|
|
193
|
+
const baseUrl = json?.compilerOptions?.baseUrl ?? ".";
|
|
194
|
+
const target = paths?.["@/*"]?.[0];
|
|
195
|
+
if (target) {
|
|
196
|
+
const dir = target.replace(/\/\*$/, "");
|
|
197
|
+
return path4.resolve(cwd, baseUrl, dir);
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const srcStat = await fs4.stat(path4.join(cwd, "src"));
|
|
204
|
+
if (srcStat.isDirectory()) return path4.join(cwd, "src");
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
return cwd;
|
|
208
|
+
}
|
|
209
|
+
function aliasToDir(alias, baseDir) {
|
|
210
|
+
const rel = alias.replace(/^@\//, "");
|
|
211
|
+
return path4.join(baseDir, rel);
|
|
212
|
+
}
|
|
213
|
+
function targetDirForType(type, aliases, baseDir) {
|
|
214
|
+
switch (type) {
|
|
215
|
+
case "registry:ui":
|
|
216
|
+
return aliasToDir(aliases.ui, baseDir);
|
|
217
|
+
case "registry:hook":
|
|
218
|
+
return aliasToDir(aliases.hooks, baseDir);
|
|
219
|
+
case "registry:lib":
|
|
220
|
+
return aliasToDir(aliases.lib, baseDir);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function rewriteImports(content, config) {
|
|
224
|
+
const { aliases } = config;
|
|
225
|
+
const replacements = [
|
|
226
|
+
["@/lib/utils", aliases.utils],
|
|
227
|
+
["@/components/ui", aliases.ui],
|
|
228
|
+
["@/components", aliases.components],
|
|
229
|
+
["@/hooks", aliases.hooks],
|
|
230
|
+
["@/lib", aliases.lib]
|
|
231
|
+
];
|
|
232
|
+
let result = content;
|
|
233
|
+
for (const [from, to] of replacements) {
|
|
234
|
+
if (from === to) continue;
|
|
235
|
+
const pattern = new RegExp(
|
|
236
|
+
`(["'\`])${escapeRegExp(from)}(?=["'\`/])`,
|
|
237
|
+
"g"
|
|
238
|
+
);
|
|
239
|
+
result = result.replace(pattern, `$1${to}`);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
function escapeRegExp(s) {
|
|
244
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
245
|
+
}
|
|
246
|
+
function stripJsonComments(input) {
|
|
247
|
+
return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/,(\s*[}\]])/g, "$1");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/commands/add.ts
|
|
251
|
+
async function runAdd(options) {
|
|
252
|
+
const { cwd, components } = options;
|
|
253
|
+
p2.intro(color2.bgCyan(color2.black(" lorre-blocks add ")));
|
|
254
|
+
const config = await readConfig(cwd);
|
|
255
|
+
if (!config) {
|
|
256
|
+
p2.cancel(
|
|
257
|
+
`No components.json found. Run ${color2.cyan("lorre-blocks init")} first.`
|
|
258
|
+
);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
if (components.length === 0) {
|
|
262
|
+
p2.cancel("Specify at least one component, e.g. `lorre-blocks add button`.");
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
const spinner2 = p2.spinner();
|
|
266
|
+
spinner2.start("Resolving components from the registry");
|
|
267
|
+
let tree;
|
|
268
|
+
try {
|
|
269
|
+
tree = await resolveTree(config.registry, components);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
spinner2.stop("Failed to resolve components.");
|
|
272
|
+
p2.cancel(err.message);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
spinner2.stop(
|
|
276
|
+
`Resolved ${tree.length} item(s): ${tree.map((t) => t.name).join(", ")}`
|
|
277
|
+
);
|
|
278
|
+
const npmDeps = [...new Set(tree.flatMap((t) => t.dependencies ?? []))];
|
|
279
|
+
if (npmDeps.length > 0) {
|
|
280
|
+
const pm = await detectPackageManager(cwd);
|
|
281
|
+
const depSpinner = p2.spinner();
|
|
282
|
+
depSpinner.start(`Installing ${npmDeps.length} dependency(ies) with ${pm}`);
|
|
283
|
+
try {
|
|
284
|
+
await installDependencies(cwd, pm, npmDeps);
|
|
285
|
+
depSpinner.stop(`Installed: ${npmDeps.join(", ")}`);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
depSpinner.stop("Dependency install failed.");
|
|
288
|
+
p2.cancel(err.message);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const baseDir = await resolveBaseDir(cwd);
|
|
293
|
+
const written = [];
|
|
294
|
+
for (const item of tree) {
|
|
295
|
+
for (const file of item.files) {
|
|
296
|
+
const dir = targetDirForType(file.type, config.aliases, baseDir);
|
|
297
|
+
const fileName = path5.basename(file.path);
|
|
298
|
+
const dest = path5.join(dir, fileName);
|
|
299
|
+
if (await fileExists2(dest)) {
|
|
300
|
+
const shouldOverwrite = options.overwrite || options.yes || await p2.confirm({
|
|
301
|
+
message: `${path5.relative(cwd, dest)} exists. Overwrite?`,
|
|
302
|
+
initialValue: false
|
|
303
|
+
});
|
|
304
|
+
if (p2.isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
305
|
+
p2.log.warn(`Skipped ${path5.relative(cwd, dest)}`);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
310
|
+
const content = rewriteImports(file.content, config);
|
|
311
|
+
await fs5.writeFile(dest, content, "utf8");
|
|
312
|
+
written.push(path5.relative(cwd, dest));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (written.length > 0) {
|
|
316
|
+
p2.log.success(`Added:
|
|
317
|
+
${written.map((f) => ` ${color2.green(f)}`).join("\n")}`);
|
|
318
|
+
}
|
|
319
|
+
p2.outro(`${color2.green("\u2714")} Done.`);
|
|
320
|
+
}
|
|
321
|
+
async function fileExists2(f) {
|
|
322
|
+
try {
|
|
323
|
+
await fs5.access(f);
|
|
324
|
+
return true;
|
|
325
|
+
} catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/index.ts
|
|
331
|
+
var program = new Command();
|
|
332
|
+
program.name("lorre-blocks").description("Add lorre-blocks components to your project by copying their source in.").version("0.1.0");
|
|
333
|
+
program.command("init").description("Create a components.json config in your project.").option("-c, --cwd <path>", "working directory", process.cwd()).option("-r, --registry <url>", "registry base URL").option("-y, --yes", "skip prompts and accept defaults", false).action(async (opts) => {
|
|
334
|
+
await runInit({
|
|
335
|
+
cwd: opts.cwd,
|
|
336
|
+
registry: opts.registry,
|
|
337
|
+
yes: opts.yes
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
program.command("add").description("Add one or more components to your project.").argument("[components...]", "component names, e.g. button").option("-c, --cwd <path>", "working directory", process.cwd()).option("-y, --yes", "skip confirmation prompts", false).option("-o, --overwrite", "overwrite existing files without asking", false).action(async (components, opts) => {
|
|
341
|
+
await runAdd({
|
|
342
|
+
cwd: opts.cwd,
|
|
343
|
+
components,
|
|
344
|
+
yes: opts.yes,
|
|
345
|
+
overwrite: opts.overwrite
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
349
|
+
console.error(err);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lorre-blocks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Add lorre-blocks components to your project by copying their source in.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lorre-blocks": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsup --watch"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"cli",
|
|
18
|
+
"components",
|
|
19
|
+
"react",
|
|
20
|
+
"tailwind",
|
|
21
|
+
"shadcn"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/zukazine/lorre-blocks.git",
|
|
27
|
+
"directory": "packages/cli"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/zukazine/lorre-blocks#readme",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@clack/prompts": "^0.9.1",
|
|
35
|
+
"commander": "^13.0.0",
|
|
36
|
+
"picocolors": "^1.1.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.10.5",
|
|
40
|
+
"tsup": "^8.3.5",
|
|
41
|
+
"typescript": "^5.7.2"
|
|
42
|
+
}
|
|
43
|
+
}
|