@pauldvlp/vp-pkg-shadcn 0.4.1 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +7 -8
  2. package/dist/index.js +219 -99
  3. package/package.json +2 -4
package/README.md CHANGED
@@ -9,10 +9,10 @@ A [Vite+](https://viteplus.dev) **package generator** that drops a shared, fully
9
9
 
10
10
  - `packages/ui` — a shared shadcn design system (Tailwind v4, fonts, `lib/utils`, `components/ui/*`)
11
11
 
12
- It's a [Bingo](https://create.bingo) template, so options can be passed on the `vp create` command
13
- line (anything after `--`) and the **shadcn theme is materialized at create time** from the preset
14
- you choose. Unlike [`@pauldvlp/vp-react-ts-shadcn`](../vp-react-ts-shadcn) (which scaffolds a whole
15
- monorepo), this only emits `packages/ui` and leaves the rest of your repo untouched.
12
+ Options can be passed on the `vp create` command line (anything after `--`) or answered interactively,
13
+ and the **shadcn theme is materialized at create time** from the preset you choose. Unlike
14
+ [`@pauldvlp/vp-react-ts-shadcn`](../vp-react-ts-shadcn) (which scaffolds a whole monorepo), this only
15
+ emits `packages/ui` and leaves the rest of your repo untouched.
16
16
 
17
17
  ## Usage
18
18
 
@@ -27,9 +27,8 @@ vp create @pauldvlp:vp-pkg-shadcn -- \
27
27
  --scope @acme --base base --preset vega --iconLibrary lucide --components button,card,dialog
28
28
  ```
29
29
 
30
- > Only **string/enum** options parse reliably as `vp create -- --flag value`. **Boolean** options
31
- > (`--cssVariables`, `--rtl`, `--pointer`, `--install`) are best left to the interactive prompt
32
- > Bingo's CLI does not accept `--no-x` / `--x=false` cleanly. Omit them and answer the prompt.
30
+ > **Boolean** options (`--cssVariables`, `--rtl`, `--pointer`, `--install`) accept both forms: pass
31
+ > `--rtl` to enable or `--no-rtl` to disable. Omit any option to answer it at the interactive prompt.
33
32
 
34
33
  ### Requirements
35
34
 
@@ -40,7 +39,7 @@ generator does **not** edit your `pnpm-workspace.yaml` or any app — wiring a c
40
39
 
41
40
  ### Where it lands (the `--directory`)
42
41
 
43
- The generator emits the package contents at the **root** of its file tree, and Bingo writes them under
42
+ The generator emits the package contents at the **root** of its file tree, and the runtime writes them under
44
43
  the target directory — which **defaults to `ui`** (so within `vp create`'s monorepo flow it lands at
45
44
  `<chosen-parent>/ui`, e.g. `packages/ui`). Pass `--directory <path>` to place it elsewhere. `--scope`
46
45
  defaults to the **surrounding monorepo's scope** (read from the nearest `pnpm-workspace.yaml`'s
package/dist/index.js CHANGED
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/index.ts
4
- import fs2 from "node:fs";
5
- import path3 from "node:path";
6
- import * as prompts from "@clack/prompts";
4
+ import * as prompts2 from "@clack/prompts";
7
5
 
8
6
  // ../template-kit/src/index.ts
7
+ import { execFileSync } from "node:child_process";
9
8
  import fs from "node:fs";
10
9
  import path from "node:path";
10
+ import * as prompts from "@clack/prompts";
11
11
  var RENAME = {
12
- _gitignore: ".gitignore"
12
+ _gitignore: ".gitignore",
13
+ _dockerignore: ".dockerignore",
14
+ "_env.example": ".env.example"
13
15
  };
14
16
  var DEFAULT_PRESET = "b30557okNu";
15
17
  var ICON_LIBS = {
@@ -99,6 +101,16 @@ function addIconDeps(pkg, iconLibrary) {
99
101
  const iconDeps = ICON_LIBS[iconLibrary] ?? ICON_LIBS.hugeicons;
100
102
  pkg.dependencies = { ...pkg.dependencies, ...iconDeps };
101
103
  }
104
+ function validateScope(value) {
105
+ return /^@?[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(value.trim()) ? void 0 : "must be a package scope/name, e.g. @acme";
106
+ }
107
+ function validatePreset(value) {
108
+ return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value.trim()) ? void 0 : "must be a style name or ui.shadcn.com code";
109
+ }
110
+ function validateComponents(value) {
111
+ const parts = value.split(",").map((c) => c.trim()).filter(Boolean);
112
+ return parts.every((c) => /^[a-z0-9][a-z0-9-]*$/.test(c)) ? void 0 : "comma-separated component names, e.g. button,card,dialog";
113
+ }
102
114
  function withRequiredComponents(csv, required = ["button", "badge"]) {
103
115
  const requested = csv.split(",").map((c) => c.trim()).filter(Boolean);
104
116
  return Array.from(/* @__PURE__ */ new Set([...required, ...requested]));
@@ -123,105 +135,217 @@ function uiComponentsJson(theme) {
123
135
  }
124
136
  };
125
137
  }
126
- function shadcnInitFlags(opts) {
138
+ function shadcnInitArgs(opts) {
127
139
  return [
128
- `--base ${opts.base}`,
129
- `--preset ${opts.preset}`,
140
+ "--base",
141
+ opts.base,
142
+ "--preset",
143
+ opts.preset,
130
144
  opts.cssVariables ? "--css-variables" : "--no-css-variables",
131
145
  opts.rtl ? "--rtl" : "--no-rtl",
132
146
  opts.pointer ? "--pointer" : "--no-pointer",
133
147
  "--no-reinstall",
134
148
  "-y",
135
149
  "-f"
136
- ].join(" ");
150
+ ];
151
+ }
152
+ function defineTemplate(config) {
153
+ return config;
154
+ }
155
+ function writeTree(node, dir, root = path.resolve(dir)) {
156
+ for (const [name, value] of Object.entries(node)) {
157
+ const p = path.resolve(dir, name);
158
+ const rel = path.relative(root, p);
159
+ if (rel === "" || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
160
+ throw new Error(`Refusing to write outside the target directory: ${name}`);
161
+ }
162
+ if (typeof value === "string") {
163
+ fs.mkdirSync(path.dirname(p), { recursive: true });
164
+ fs.writeFileSync(p, value);
165
+ } else {
166
+ fs.mkdirSync(p, { recursive: true });
167
+ writeTree(value, p, root);
168
+ }
169
+ }
170
+ }
171
+ function parseArgv(argv2, descriptors) {
172
+ const booleanKeys = new Set(descriptors.filter((d) => d.type === "boolean").map((d) => d.key));
173
+ const values = {};
174
+ let directory;
175
+ for (let i = 0; i < argv2.length; i++) {
176
+ const arg = argv2[i];
177
+ if (!arg.startsWith("--")) continue;
178
+ let body = arg.slice(2);
179
+ if (body.startsWith("no-") && booleanKeys.has(body.slice(3))) {
180
+ values[body.slice(3)] = false;
181
+ continue;
182
+ }
183
+ let inline;
184
+ const eq = body.indexOf("=");
185
+ if (eq !== -1) {
186
+ inline = body.slice(eq + 1);
187
+ body = body.slice(0, eq);
188
+ }
189
+ if (body === "directory") {
190
+ directory = inline ?? argv2[++i];
191
+ continue;
192
+ }
193
+ if (booleanKeys.has(body)) {
194
+ values[body] = inline === void 0 ? true : inline !== "false";
195
+ continue;
196
+ }
197
+ values[body] = inline ?? argv2[++i] ?? "";
198
+ }
199
+ return { values, directory };
200
+ }
201
+ function resolveDefault(d, options, directory) {
202
+ return typeof d.default === "function" ? d.default({ options, directory }) : d.default;
203
+ }
204
+ async function promptForOption(d, def) {
205
+ if (d.type === "boolean") {
206
+ return prompts.confirm({ message: d.message, initialValue: def === void 0 ? false : Boolean(def) });
207
+ }
208
+ if (d.type === "select") {
209
+ return prompts.select({
210
+ message: d.message,
211
+ options: (d.choices ?? []).map((c) => ({ value: c, label: c })),
212
+ initialValue: def
213
+ });
214
+ }
215
+ const fallback = def === void 0 ? void 0 : String(def);
216
+ return prompts.text({
217
+ message: d.message,
218
+ placeholder: fallback,
219
+ defaultValue: fallback,
220
+ // When a default exists, an empty submit means "use the default" — don't run validation on it,
221
+ // otherwise a rule like the port check would reject pressing Enter to accept 3000.
222
+ validate: d.validate ? (value) => !value && fallback !== void 0 ? void 0 : d.validate(value ?? "") : void 0
223
+ });
224
+ }
225
+ function printHelp(template) {
226
+ const lines = [];
227
+ if (template.about?.name) lines.push(template.about.name);
228
+ if (template.about?.description) lines.push(template.about.description);
229
+ lines.push("", "Options:", " --directory <dir> Where to scaffold");
230
+ for (const d of template.options) {
231
+ const hint = d.type === "select" && d.choices ? ` <${d.choices.join("|")}>` : d.type === "boolean" ? ` / --no-${d.key}` : " <value>";
232
+ lines.push(` --${d.key}${hint} ${d.message}`);
233
+ }
234
+ console.log(lines.join("\n"));
235
+ }
236
+ function initGit(dir) {
237
+ try {
238
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir, stdio: "ignore" });
239
+ return;
240
+ } catch {
241
+ }
242
+ try {
243
+ execFileSync("git", ["init"], { cwd: dir, stdio: "ignore" });
244
+ execFileSync("git", ["add", "-A"], { cwd: dir, stdio: "ignore" });
245
+ let identity = [];
246
+ try {
247
+ execFileSync("git", ["config", "user.email"], { cwd: dir, stdio: "ignore" });
248
+ } catch {
249
+ identity = ["-c", "user.name=create", "-c", "user.email=create@users.noreply.github.com"];
250
+ }
251
+ execFileSync("git", [...identity, "commit", "-m", "feat: initialize project", "--no-gpg-sign"], { cwd: dir, stdio: "ignore" });
252
+ } catch {
253
+ }
254
+ }
255
+ async function runTemplateCLI(template) {
256
+ const argv2 = process.argv.slice(2);
257
+ if (argv2.includes("--help") || argv2.includes("-h")) {
258
+ printHelp(template);
259
+ return 0;
260
+ }
261
+ const interactive = Boolean(process.stdout.isTTY) && !argv2.includes("--no-interactive");
262
+ const { values, directory: dirArg } = parseArgv(argv2, template.options);
263
+ prompts.intro(template.about?.name ?? "create");
264
+ let directory = dirArg;
265
+ if (!directory) {
266
+ if (interactive) {
267
+ const answer = await prompts.text({ message: "Where should we create your project?", placeholder: "my-app", defaultValue: "my-app" });
268
+ if (prompts.isCancel(answer)) {
269
+ prompts.cancel("Cancelled.");
270
+ return 2;
271
+ }
272
+ directory = answer || "my-app";
273
+ } else {
274
+ directory = "my-app";
275
+ }
276
+ }
277
+ const options = {};
278
+ for (const d of template.options) {
279
+ const def = resolveDefault(d, options, directory);
280
+ if (d.key in values) {
281
+ const value = values[d.key];
282
+ if (d.validate && typeof value === "string") {
283
+ const error = d.validate(value);
284
+ if (error) {
285
+ prompts.cancel(`Invalid --${d.key}: ${error}`);
286
+ return 1;
287
+ }
288
+ }
289
+ options[d.key] = value;
290
+ continue;
291
+ }
292
+ if (interactive) {
293
+ const answer = await promptForOption(d, def);
294
+ if (prompts.isCancel(answer)) {
295
+ prompts.cancel("Cancelled.");
296
+ return 2;
297
+ }
298
+ options[d.key] = answer === "" && def !== void 0 ? def : answer;
299
+ } else {
300
+ options[d.key] = def;
301
+ }
302
+ }
303
+ const creation = await template.produce({ options });
304
+ writeTree(creation.files, directory);
305
+ if (creation.scripts?.length) {
306
+ for (const step of [...creation.scripts].sort((a, b) => a.phase - b.phase)) {
307
+ for (const [program, ...commandArgs] of step.commands) {
308
+ if (!program) continue;
309
+ execFileSync(program, commandArgs, { cwd: directory, stdio: "inherit" });
310
+ }
311
+ }
312
+ }
313
+ if (template.git !== false) initGit(directory);
314
+ if (creation.suggestions?.length) {
315
+ prompts.note(creation.suggestions.join("\n"), "Next steps");
316
+ }
317
+ prompts.outro("Done!");
318
+ return 0;
137
319
  }
138
-
139
- // bin/index.ts
140
- import { runTemplateCLI } from "bingo";
141
320
 
142
321
  // src/template.ts
322
+ import { readFileSync } from "node:fs";
143
323
  import path2 from "node:path";
144
324
  import { fileURLToPath } from "node:url";
145
- import { createTemplate } from "bingo";
146
- import { z } from "zod";
147
-
148
- // package.json
149
- var package_default = {
150
- name: "@pauldvlp/vp-pkg-shadcn",
151
- version: "0.4.1",
152
- description: "Add a shared, fully customizable shadcn UI package (packages/ui) into an existing Vite+ monorepo, themeable via shadcn presets.",
153
- author: "pauldvlp (https://github.com/pauldvlp/vp-templates)",
154
- license: "MIT",
155
- homepage: "https://github.com/pauldvlp/vp-templates",
156
- repository: {
157
- type: "git",
158
- url: "git+https://github.com/pauldvlp/vp-templates.git",
159
- directory: "packages/vp-pkg-shadcn"
160
- },
161
- bugs: "https://github.com/pauldvlp/vp-templates/issues",
162
- keywords: [
163
- "vite-plus-generator",
164
- "vite-plus",
165
- "shadcn",
166
- "react",
167
- "template"
168
- ],
169
- bin: "./dist/index.js",
170
- type: "module",
171
- files: [
172
- "dist",
173
- "template"
174
- ],
175
- scripts: {
176
- build: "esbuild bin/index.ts --bundle --outfile=dist/index.js --format=esm --platform=node --target=node22 --packages=external",
177
- dev: "node bin/index.ts",
178
- prepack: "pnpm run build"
179
- },
180
- dependencies: {
181
- "@clack/prompts": "^0.11.0",
182
- bingo: "^0.9.3",
183
- zod: "^3.25.76"
184
- },
185
- devDependencies: {
186
- "@pauldvlp/template-kit": "workspace:*",
187
- "@types/node": "^24",
188
- esbuild: "^0.25.0",
189
- typescript: "^5"
190
- },
191
- engines: {
192
- node: ">=22.18.0"
193
- }
194
- };
195
-
196
- // src/template.ts
197
325
  var TEMPLATE_DIR = path2.join(path2.dirname(fileURLToPath(import.meta.url)), "..", "template");
198
- var template_default = createTemplate({
326
+ var { name: pkgName, description: pkgDescription } = JSON.parse(
327
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
328
+ );
329
+ var template_default = defineTemplate({
199
330
  about: {
200
- name: package_default.name,
201
- description: package_default.description
202
- },
203
- options: {
204
- scope: z.string().describe("npm scope for the workspace package, e.g. @acme").default("@app"),
205
- // Unions of literals (not z.enum) so the value is settable on Bingo's CLI: bingo's arg parser
206
- // handles ZodUnion/ZodLiteral but not ZodEnum (a bare `--base radix` would otherwise mis-parse to
207
- // `base: true`). Both still render as an interactive `select`.
208
- base: z.union([z.literal("radix"), z.literal("base")]).describe("shadcn component library base (radix-ui or @base-ui)").default("radix"),
209
- preset: z.string().describe("shadcn preset: a style name (nova, vega, maia, lyra, mira, luma, sera, rhea) or a code from ui.shadcn.com").default(DEFAULT_PRESET),
210
- iconLibrary: z.union([z.literal("lucide"), z.literal("hugeicons"), z.literal("radix"), z.literal("tabler")]).describe("Icon library").default("hugeicons"),
211
- cssVariables: z.boolean().describe("Use CSS variables for theming").default(true),
212
- rtl: z.boolean().describe("Enable RTL support").default(false),
213
- pointer: z.boolean().describe("Use pointer cursor on interactive elements").default(false),
214
- components: z.string().describe("Comma-separated shadcn components to pre-install, e.g. button,card,dialog").default("button,badge"),
215
- install: z.boolean().describe("Install deps and apply the shadcn theme after scaffolding").default(true)
216
- },
217
- // Default the scope to the surrounding monorepo's scope (e.g. `@acme`) so it tracks the repo
218
- // instead of a fixed `@app`. Falls back to `@app` when run outside a pnpm workspace.
219
- prepare() {
220
- const monorepoScope = detectMonorepoScope();
221
- return {
222
- scope: () => monorepoScope ?? "@app"
223
- };
331
+ name: pkgName,
332
+ description: pkgDescription
224
333
  },
334
+ // This generator drops a package into an *existing* repo, so it must never create a git repo of its own.
335
+ git: false,
336
+ options: [
337
+ // Default the scope to the surrounding monorepo's scope (e.g. `@acme`) so it tracks the repo
338
+ // instead of a fixed `@app`. Falls back to `@app` when run outside a pnpm workspace.
339
+ { key: "scope", type: "string", message: "npm scope for the workspace package, e.g. @acme", default: () => detectMonorepoScope() ?? "@app", validate: validateScope },
340
+ { key: "base", type: "select", message: "shadcn component library base (radix-ui or @base-ui)", choices: ["radix", "base"], default: "radix" },
341
+ { key: "preset", type: "string", message: "shadcn preset: a style name (nova, vega, maia, lyra, mira, luma, sera, rhea) or a code from ui.shadcn.com", default: DEFAULT_PRESET, validate: validatePreset },
342
+ { key: "iconLibrary", type: "select", message: "Icon library", choices: ["lucide", "hugeicons", "radix", "tabler"], default: "hugeicons" },
343
+ { key: "cssVariables", type: "boolean", message: "Use CSS variables for theming", default: true },
344
+ { key: "rtl", type: "boolean", message: "Enable RTL support", default: false },
345
+ { key: "pointer", type: "boolean", message: "Use pointer cursor on interactive elements", default: false },
346
+ { key: "components", type: "string", message: "Comma-separated shadcn components to pre-install, e.g. button,card,dialog", default: "button,badge", validate: validateComponents },
347
+ { key: "install", type: "boolean", message: "Install deps and apply the shadcn theme after scaffolding", default: true }
348
+ ],
225
349
  async produce({ options }) {
226
350
  const scope = toScope(options.scope || "app");
227
351
  const files = readTree(TEMPLATE_DIR, (_rel, content) => content.split("@app").join(scope));
@@ -233,11 +357,11 @@ var template_default = createTemplate({
233
357
  `);
234
358
  const adds = withRequiredComponents(options.components);
235
359
  const ui = `${scope}/ui`;
236
- const initFlags = shadcnInitFlags(options);
360
+ const initArgs = shadcnInitArgs(options);
237
361
  const scripts = options.install ? [
238
- { commands: ["pnpm install --silent"], phase: 0 },
239
- { commands: [`pnpm --filter ${ui} exec shadcn init ${initFlags}`], phase: 1 },
240
- { commands: [`pnpm --filter ${ui} exec shadcn add ${adds.join(" ")} -y`], phase: 2 }
362
+ { commands: [["pnpm", "install", "--silent"]], phase: 0 },
363
+ { commands: [["pnpm", "--filter", ui, "exec", "shadcn", "init", ...initArgs]], phase: 1 },
364
+ { commands: [["pnpm", "--filter", ui, "exec", "shadcn", "add", ...adds, "-y"]], phase: 2 }
241
365
  ] : [];
242
366
  return {
243
367
  files,
@@ -265,13 +389,9 @@ if (!getArg(argv, "directory") && !isHelp) {
265
389
  var status = await runTemplateCLI(template_default);
266
390
  process.exitCode = status;
267
391
  var finalArgv = process.argv.slice(2);
268
- var targetDir = getArg(finalArgv, "directory");
269
- if (!isHelp && targetDir && targetDir !== "." && path3.resolve(targetDir) !== process.cwd()) {
270
- fs2.rmSync(path3.join(targetDir, ".git"), { recursive: true, force: true });
271
- }
272
392
  if (!isHelp && (status === 0 || status === void 0)) {
273
393
  const ui = `${toScope(getArg(finalArgv, "scope") || detectMonorepoScope() || "app")}/ui`;
274
- prompts.note(
394
+ prompts2.note(
275
395
  [
276
396
  `1. Workspace glob the dir you chose (e.g. packages/*) so ${ui} is linked`,
277
397
  `2. Depend on it ${'"' + ui + '": "workspace:*"'} in your app's package.json`,
@@ -285,7 +405,7 @@ if (!isHelp && (status === 0 || status === void 0)) {
285
405
  `Wire ${ui} into your app`
286
406
  );
287
407
  if (isInteractive) {
288
- const ack = await prompts.confirm({ message: "Got it \u2014 continue?", initialValue: true });
289
- if (prompts.isCancel(ack)) prompts.cancel("Okay \u2014 see the docs link above when you\u2019re ready.");
408
+ const ack = await prompts2.confirm({ message: "Got it \u2014 continue?", initialValue: true });
409
+ if (prompts2.isCancel(ack)) prompts2.cancel("Okay \u2014 see the docs link above when you\u2019re ready.");
290
410
  }
291
411
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pauldvlp/vp-pkg-shadcn",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Add a shared, fully customizable shadcn UI package (packages/ui) into an existing Vite+ monorepo, themeable via shadcn presets.",
5
5
  "author": "pauldvlp (https://github.com/pauldvlp/vp-templates)",
6
6
  "license": "MIT",
@@ -24,9 +24,7 @@
24
24
  "template"
25
25
  ],
26
26
  "dependencies": {
27
- "@clack/prompts": "^0.11.0",
28
- "bingo": "^0.9.3",
29
- "zod": "^3.25.76"
27
+ "@clack/prompts": "^1.6.0"
30
28
  },
31
29
  "devDependencies": {
32
30
  "@types/node": "^24",