lorre-blocks 0.1.1 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +334 -109
  2. package/package.json +12 -7
package/dist/index.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
- import { promises as fs2 } from "fs";
8
- import path2 from "path";
7
+ import { promises as fs4 } from "fs";
8
+ import path4 from "path";
9
9
  import * as p from "@clack/prompts";
10
10
  import color from "picocolors";
11
11
 
@@ -48,104 +48,12 @@ async function writeConfig(cwd, config) {
48
48
  );
49
49
  }
50
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
51
  // src/utils/package-manager.ts
144
- import { promises as fs3 } from "fs";
145
- import path3 from "path";
52
+ import { promises as fs2 } from "fs";
53
+ import path2 from "path";
146
54
  import { spawn } from "child_process";
147
55
  async function detectPackageManager(cwd) {
148
- const has = async (f) => fs3.access(path3.join(cwd, f)).then(() => true).catch(() => false);
56
+ const has = async (f) => fs2.access(path2.join(cwd, f)).then(() => true).catch(() => false);
149
57
  if (await has("pnpm-lock.yaml")) return "pnpm";
150
58
  if (await has("yarn.lock")) return "yarn";
151
59
  if (await has("bun.lockb")) return "bun";
@@ -181,34 +89,34 @@ async function installDependencies(cwd, pm, deps) {
181
89
  }
182
90
 
183
91
  // src/utils/paths.ts
184
- import { promises as fs4 } from "fs";
185
- import path4 from "path";
92
+ import { promises as fs3 } from "fs";
93
+ import path3 from "path";
186
94
  async function resolveBaseDir(cwd) {
187
95
  for (const file of ["tsconfig.json", "jsconfig.json"]) {
188
96
  try {
189
- const raw = await fs4.readFile(path4.join(cwd, file), "utf8");
97
+ const raw = await fs3.readFile(path3.join(cwd, file), "utf8");
190
98
  const json = JSON.parse(stripJsonComments(raw));
191
99
  const paths = json?.compilerOptions?.paths;
192
100
  const baseUrl = json?.compilerOptions?.baseUrl ?? ".";
193
101
  const target = paths?.["@/*"]?.[0];
194
102
  if (target) {
195
103
  const dir = target.replace(/\/\*$/, "");
196
- return path4.resolve(cwd, baseUrl, dir);
104
+ return path3.resolve(cwd, baseUrl, dir);
197
105
  }
198
106
  } catch {
199
107
  continue;
200
108
  }
201
109
  }
202
110
  try {
203
- const srcStat = await fs4.stat(path4.join(cwd, "src"));
204
- if (srcStat.isDirectory()) return path4.join(cwd, "src");
111
+ const srcStat = await fs3.stat(path3.join(cwd, "src"));
112
+ if (srcStat.isDirectory()) return path3.join(cwd, "src");
205
113
  } catch {
206
114
  }
207
115
  return cwd;
208
116
  }
209
117
  function aliasToDir(alias, baseDir) {
210
118
  const rel = alias.replace(/^@\//, "");
211
- return path4.join(baseDir, rel);
119
+ return path3.join(baseDir, rel);
212
120
  }
213
121
  function targetDirForType(type, aliases, baseDir) {
214
122
  switch (type) {
@@ -244,7 +152,211 @@ function stripJsonComments(input) {
244
152
  return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/,(\s*[}\]])/g, "$1");
245
153
  }
246
154
 
155
+ // src/utils/registry.ts
156
+ async function fetchRegistryItem(registry, name) {
157
+ const base = registry.replace(/\/$/, "");
158
+ const url = `${base}/r/${name}.json`;
159
+ let res;
160
+ try {
161
+ res = await fetch(url);
162
+ } catch (err) {
163
+ throw new Error(
164
+ `Could not reach the registry at ${url}. Is it running / correct?
165
+ ${err.message}`
166
+ );
167
+ }
168
+ if (res.status === 404) {
169
+ throw new Error(`Component "${name}" was not found in the registry (${url}).`);
170
+ }
171
+ if (!res.ok) {
172
+ throw new Error(`Failed to fetch ${url} (HTTP ${res.status}).`);
173
+ }
174
+ return await res.json();
175
+ }
176
+ async function fetchIndex(registry) {
177
+ const base = registry.replace(/\/$/, "");
178
+ const url = `${base}/r/index.json`;
179
+ const res = await fetch(url);
180
+ if (!res.ok) throw new Error(`Failed to fetch ${url} (HTTP ${res.status}).`);
181
+ return await res.json();
182
+ }
183
+ async function fetchTheme(registry) {
184
+ const base = registry.replace(/\/$/, "");
185
+ const url = `${base}/r/theme.json`;
186
+ const res = await fetch(url);
187
+ if (!res.ok) throw new Error(`Failed to fetch ${url} (HTTP ${res.status}).`);
188
+ const data = await res.json();
189
+ return data.css;
190
+ }
191
+ async function resolveTree(registry, names) {
192
+ const resolved = /* @__PURE__ */ new Map();
193
+ async function visit(name) {
194
+ if (resolved.has(name)) return;
195
+ const item = await fetchRegistryItem(registry, name);
196
+ for (const dep of item.registryDependencies ?? []) {
197
+ await visit(dep);
198
+ }
199
+ if (!resolved.has(name)) resolved.set(name, item);
200
+ }
201
+ for (const name of names) {
202
+ await visit(name);
203
+ }
204
+ return [...resolved.values()];
205
+ }
206
+
207
+ // src/utils/css.ts
208
+ var THEME_START = "/* lorre-blocks theme start */";
209
+ var THEME_END = "/* lorre-blocks theme end */";
210
+ var TAILWIND_IMPORT = '@import "tailwindcss";';
211
+ function hasTheme(cssContent) {
212
+ return cssContent.includes(THEME_START);
213
+ }
214
+ function injectThemeBlock(cssContent, themeCss) {
215
+ const block = `${THEME_START}
216
+ ${themeCss.trim()}
217
+ ${THEME_END}`;
218
+ if (hasTheme(cssContent)) {
219
+ return cssContent.replace(
220
+ new RegExp(
221
+ `${escapeRegExp2(THEME_START)}[\\s\\S]*?${escapeRegExp2(THEME_END)}`
222
+ ),
223
+ block
224
+ );
225
+ }
226
+ const trimmed = cssContent.trimEnd();
227
+ if (trimmed.includes(TAILWIND_IMPORT)) {
228
+ const lines = trimmed.split("\n");
229
+ const idx = lines.findIndex((l) => l.trim() === TAILWIND_IMPORT);
230
+ lines.splice(idx + 1, 0, "", block);
231
+ return lines.join("\n") + "\n";
232
+ }
233
+ const prefix = trimmed.length > 0 ? `${trimmed}
234
+
235
+ ` : "";
236
+ return `${TAILWIND_IMPORT}
237
+
238
+ ${prefix}${block}
239
+ `;
240
+ }
241
+ function escapeRegExp2(s) {
242
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
243
+ }
244
+
245
+ // src/commands/init.ts
246
+ var BASE_DEPENDENCIES = [
247
+ "clsx",
248
+ "tailwind-merge",
249
+ "class-variance-authority",
250
+ "tw-animate-css"
251
+ ];
252
+ var GLOBAL_CSS_CANDIDATES = [
253
+ "src/app/globals.css",
254
+ "app/globals.css",
255
+ "src/index.css",
256
+ "src/styles/globals.css",
257
+ "styles/globals.css"
258
+ ];
259
+ async function runInit(options) {
260
+ const { cwd } = options;
261
+ p.intro(color.bgCyan(color.black(" lorre-blocks init ")));
262
+ if (await configExists(cwd)) {
263
+ const overwrite = options.yes ? true : await p.confirm({
264
+ message: "components.json already exists. Overwrite it?",
265
+ initialValue: false
266
+ });
267
+ if (p.isCancel(overwrite) || !overwrite) {
268
+ p.cancel("Init aborted; existing components.json kept.");
269
+ return;
270
+ }
271
+ }
272
+ const tsx = await fileExists(path4.join(cwd, "tsconfig.json"));
273
+ const registry = options.registry ?? (options.yes ? DEFAULT_REGISTRY : await promptText("Registry URL", DEFAULT_REGISTRY));
274
+ const config = {
275
+ $schema: "https://lorre-blocks.dev/schema.json",
276
+ registry,
277
+ tsx,
278
+ aliases: { ...DEFAULT_ALIASES }
279
+ };
280
+ await writeConfig(cwd, config);
281
+ p.log.success("Wrote components.json");
282
+ const pm = await detectPackageManager(cwd);
283
+ const depSpinner = p.spinner();
284
+ depSpinner.start(`Installing base dependencies with ${pm}`);
285
+ try {
286
+ await installDependencies(cwd, pm, BASE_DEPENDENCIES);
287
+ depSpinner.stop(`Installed: ${BASE_DEPENDENCIES.join(", ")}`);
288
+ } catch (err) {
289
+ depSpinner.stop("Dependency install failed.");
290
+ p.cancel(err.message);
291
+ process.exit(1);
292
+ }
293
+ const baseDir = await resolveBaseDir(cwd);
294
+ try {
295
+ const utils = await fetchRegistryItem(registry, "utils");
296
+ for (const file of utils.files) {
297
+ const dir = targetDirForType(file.type, config.aliases, baseDir);
298
+ await fs4.mkdir(dir, { recursive: true });
299
+ const dest = path4.join(dir, path4.basename(file.path));
300
+ await fs4.writeFile(dest, rewriteImports(file.content, config), "utf8");
301
+ p.log.success(`Wrote ${path4.relative(cwd, dest)}`);
302
+ }
303
+ } catch (err) {
304
+ p.log.warn(`Could not write utils: ${err.message}`);
305
+ }
306
+ try {
307
+ const themeCss = await fetchTheme(registry);
308
+ const cssPath = await resolveGlobalCss(cwd);
309
+ const existing = await readIfExists(cssPath);
310
+ const next = injectThemeBlock(existing ?? "", themeCss);
311
+ await fs4.mkdir(path4.dirname(cssPath), { recursive: true });
312
+ await fs4.writeFile(cssPath, next, "utf8");
313
+ p.log.success(
314
+ `${existing === null ? "Created" : "Updated"} ${path4.relative(cwd, cssPath)} with the theme`
315
+ );
316
+ } catch (err) {
317
+ p.log.warn(`Could not set up the theme: ${err.message}`);
318
+ }
319
+ p.outro(
320
+ `${color.green("\u2714")} Ready. Now run ${color.cyan("lorre-blocks add button")}.`
321
+ );
322
+ }
323
+ async function resolveGlobalCss(cwd) {
324
+ for (const candidate of GLOBAL_CSS_CANDIDATES) {
325
+ if (await fileExists(path4.join(cwd, candidate))) {
326
+ return path4.join(cwd, candidate);
327
+ }
328
+ }
329
+ return path4.join(cwd, GLOBAL_CSS_CANDIDATES[0]);
330
+ }
331
+ async function readIfExists(file) {
332
+ try {
333
+ return await fs4.readFile(file, "utf8");
334
+ } catch {
335
+ return null;
336
+ }
337
+ }
338
+ async function promptText(message, initial) {
339
+ const value = await p.text({ message, placeholder: initial, defaultValue: initial });
340
+ if (p.isCancel(value)) {
341
+ p.cancel("Init cancelled.");
342
+ process.exit(0);
343
+ }
344
+ return value || initial;
345
+ }
346
+ async function fileExists(f) {
347
+ try {
348
+ await fs4.access(f);
349
+ return true;
350
+ } catch {
351
+ return false;
352
+ }
353
+ }
354
+
247
355
  // src/commands/add.ts
356
+ import { promises as fs5 } from "fs";
357
+ import path5 from "path";
358
+ import * as p2 from "@clack/prompts";
359
+ import color2 from "picocolors";
248
360
  async function runAdd(options) {
249
361
  const { cwd, components } = options;
250
362
  p2.intro(color2.bgCyan(color2.black(" lorre-blocks add ")));
@@ -259,17 +371,17 @@ async function runAdd(options) {
259
371
  p2.cancel("Specify at least one component, e.g. `lorre-blocks add button`.");
260
372
  process.exit(1);
261
373
  }
262
- const spinner2 = p2.spinner();
263
- spinner2.start("Resolving components from the registry");
374
+ const spinner3 = p2.spinner();
375
+ spinner3.start("Resolving components from the registry");
264
376
  let tree;
265
377
  try {
266
378
  tree = await resolveTree(config.registry, components);
267
379
  } catch (err) {
268
- spinner2.stop("Failed to resolve components.");
380
+ spinner3.stop("Failed to resolve components.");
269
381
  p2.cancel(err.message);
270
382
  process.exit(1);
271
383
  }
272
- spinner2.stop(
384
+ spinner3.stop(
273
385
  `Resolved ${tree.length} item(s): ${tree.map((t) => t.name).join(", ")}`
274
386
  );
275
387
  const npmDeps = [...new Set(tree.flatMap((t) => t.dependencies ?? []))];
@@ -324,9 +436,116 @@ async function fileExists2(f) {
324
436
  }
325
437
  }
326
438
 
439
+ // src/commands/list.ts
440
+ import * as p3 from "@clack/prompts";
441
+ import color3 from "picocolors";
442
+ async function runList(options) {
443
+ const config = await readConfig(options.cwd);
444
+ const registry = options.registry ?? config?.registry ?? DEFAULT_REGISTRY;
445
+ p3.intro(color3.bgCyan(color3.black(" lorre-blocks list ")));
446
+ let items;
447
+ try {
448
+ items = await fetchIndex(registry);
449
+ } catch (err) {
450
+ p3.cancel(err.message);
451
+ process.exit(1);
452
+ }
453
+ const width = Math.max(...items.map((i) => i.name.length), 4);
454
+ const lines = items.map(
455
+ (i) => ` ${color3.green(i.name.padEnd(width))} ${color3.dim(i.description ?? "")}`
456
+ );
457
+ p3.log.message(`${items.length} component(s):
458
+ ${lines.join("\n")}`);
459
+ p3.outro(`Add one with ${color3.cyan("lorre-blocks add <name>")}.`);
460
+ }
461
+
462
+ // src/commands/diff.ts
463
+ import { promises as fs6 } from "fs";
464
+ import path6 from "path";
465
+ import * as p4 from "@clack/prompts";
466
+ import color4 from "picocolors";
467
+ import { createTwoFilesPatch } from "diff";
468
+ async function runDiff(options) {
469
+ const { cwd } = options;
470
+ p4.intro(color4.bgCyan(color4.black(" lorre-blocks diff ")));
471
+ const config = await readConfig(cwd);
472
+ if (!config) {
473
+ p4.cancel("No components.json found. Run `lorre-blocks init` first.");
474
+ process.exit(1);
475
+ }
476
+ const baseDir = await resolveBaseDir(cwd);
477
+ let names = options.components;
478
+ if (names.length === 0) {
479
+ const uiDir = targetDirForType("registry:ui", config.aliases, baseDir);
480
+ const present = await listComponentNames(uiDir);
481
+ const index = await fetchIndex(config.registry).catch(() => []);
482
+ const known = new Set(index.map((i) => i.name));
483
+ names = present.filter((n) => known.has(n));
484
+ if (names.length === 0) {
485
+ p4.cancel("No known components found in your project to diff.");
486
+ process.exit(0);
487
+ }
488
+ }
489
+ let changed = 0;
490
+ for (const name of names) {
491
+ const item = await fetchRegistryItem(config.registry, name);
492
+ for (const file of item.files) {
493
+ const dir = targetDirForType(file.type, config.aliases, baseDir);
494
+ const dest = path6.join(dir, path6.basename(file.path));
495
+ const local = await readIfExists2(dest);
496
+ const registryContent = rewriteImports(file.content, config);
497
+ if (local === null) {
498
+ p4.log.warn(`${name}: ${path6.relative(cwd, dest)} not added yet`);
499
+ continue;
500
+ }
501
+ if (local === registryContent) {
502
+ p4.log.success(`${name}: up to date`);
503
+ continue;
504
+ }
505
+ changed++;
506
+ const patch = createTwoFilesPatch(
507
+ "your version",
508
+ "registry",
509
+ local,
510
+ registryContent,
511
+ "",
512
+ ""
513
+ );
514
+ p4.log.message(`${color4.bold(name)} \u2014 ${path6.relative(cwd, dest)}
515
+ ${colorizePatch(patch)}`);
516
+ }
517
+ }
518
+ p4.outro(
519
+ changed === 0 ? `${color4.green("\u2714")} Everything matches the registry.` : `${changed} file(s) differ from the registry.`
520
+ );
521
+ }
522
+ function colorizePatch(patch) {
523
+ return patch.split("\n").map((line) => {
524
+ if (line.startsWith("+") && !line.startsWith("+++")) return color4.green(line);
525
+ if (line.startsWith("-") && !line.startsWith("---")) return color4.red(line);
526
+ if (line.startsWith("@@")) return color4.cyan(line);
527
+ return color4.dim(line);
528
+ }).join("\n");
529
+ }
530
+ async function listComponentNames(dir) {
531
+ try {
532
+ const entries = await fs6.readdir(dir);
533
+ return entries.filter((f) => /\.(tsx|jsx|ts|js)$/.test(f)).map((f) => f.replace(/\.(tsx|jsx|ts|js)$/, ""));
534
+ } catch {
535
+ return [];
536
+ }
537
+ }
538
+ async function readIfExists2(file) {
539
+ try {
540
+ return await fs6.readFile(file, "utf8");
541
+ } catch {
542
+ return null;
543
+ }
544
+ }
545
+
327
546
  // src/index.ts
328
547
  var program = new Command();
329
- program.name("lorre-blocks").description("Add lorre-blocks components to your project by copying their source in.").version("0.1.0");
548
+ program.name("lorre-blocks").description("Add lorre-blocks components to your project by copying their source in.").version("0.2.0");
330
549
  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) => {
331
550
  await runInit({
332
551
  cwd: opts.cwd,
@@ -342,6 +561,12 @@ program.command("add").description("Add one or more components to your project."
342
561
  overwrite: opts.overwrite
343
562
  });
344
563
  });
564
+ program.command("list").description("List the components available in the registry.").option("-c, --cwd <path>", "working directory", process.cwd()).option("-r, --registry <url>", "registry base URL").action(async (opts) => {
565
+ await runList({ cwd: opts.cwd, registry: opts.registry });
566
+ });
567
+ program.command("diff").description("Show how your local components differ from the registry.").argument("[components...]", "component names; omit to diff all present").option("-c, --cwd <path>", "working directory", process.cwd()).action(async (components, opts) => {
568
+ await runDiff({ cwd: opts.cwd, components });
569
+ });
345
570
  program.parseAsync(process.argv).catch((err) => {
346
571
  console.error(err);
347
572
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lorre-blocks",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Add lorre-blocks components to your project by copying their source in.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,10 +9,6 @@
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
- "scripts": {
13
- "build": "tsup",
14
- "dev": "tsup --watch"
15
- },
16
12
  "keywords": [
17
13
  "cli",
18
14
  "components",
@@ -33,11 +29,20 @@
33
29
  "dependencies": {
34
30
  "@clack/prompts": "^0.9.1",
35
31
  "commander": "^13.0.0",
32
+ "diff": "^9.0.0",
36
33
  "picocolors": "^1.1.1"
37
34
  },
38
35
  "devDependencies": {
36
+ "@types/diff": "^7.0.0",
39
37
  "@types/node": "^22.10.5",
40
38
  "tsup": "^8.3.5",
41
- "typescript": "^5.7.2"
39
+ "typescript": "^5.7.2",
40
+ "vitest": "^4.1.10"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "dev": "tsup --watch",
45
+ "test": "vitest run",
46
+ "typecheck": "tsc --noEmit"
42
47
  }
43
- }
48
+ }