@vinena/vectoys-cli 0.1.0 → 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 (3) hide show
  1. package/SKILL.md +9 -9
  2. package/dist/index.js +217 -83
  3. package/package.json +1 -1
package/SKILL.md CHANGED
@@ -11,7 +11,7 @@ vectoys draws 3D objects as plain SVG paths. An item is a small React component
11
11
 
12
12
  1. `npx @vinena/vectoys-cli create <name>` writes `./<name>/` (or under `--dir <dir>`): `item.json`, `src/geometry.ts`, `src/<name>.tsx`, `src/index.ts`, a test, `package.json` and `tsconfig.json`. It renders a shaded, tilted slab. Under `registry/` of a vectoys checkout it uses the workspace's packages, which `bun install` links. Anywhere else it asks for `@vinena/vectoys` and `@vinena/vectoys-react` from npm by version: install them with `npm install` (or your package manager) in the item folder before you check it.
13
13
  2. Edit `src/geometry.ts` (parts, camera, poses) and `src/<name>.tsx` (looks, motion, interaction). Write a real `title` and `description` in `item.json`. List every source file in `files`.
14
- 3. `npx @vinena/vectoys-cli check <dir>` runs 8 checks: item.json is valid, the files are under 64 KB together, item.json names no brand, the component renders on the server with no props, the markup has no `NaN` or `Infinity`, two renders give the same markup, the rendered text names no brand, and the root has an accessible name. It needs bun. Fix every FAIL line.
14
+ 3. `npx @vinena/vectoys-cli check <dir>` runs 8 checks: item.json is valid, the files are under 96 KB together, item.json names no brand, the component renders on the server with no props, the markup has no `NaN` or `Infinity`, two renders give the same markup, the rendered text names no brand, and the root has an accessible name. It needs bun. Fix every FAIL line.
15
15
  4. `npx @vinena/vectoys-cli preview <dir>` writes `<dir>/preview.html` (`--open` opens it): the item rendered on the server in light and dark, and at a row of yaws when the component takes a `yaw` prop. Look at it. Faces drawn in the wrong order, gaps, and parts that float show here, not in the checks.
16
16
  5. Iterate on 2 to 4 until it looks right in both themes and at every yaw.
17
17
  6. `npx @vinena/vectoys-cli submit <dir>` runs the checks, then opens a pull request (it needs the GitHub CLI, `gh`) that adds the item to vinenastudio/vectoys. If you host your own registry, `npx @vinena/vectoys-cli submit --registry <url>` opens a pull request that lists it. Until submissions open at launch, submit prints what it would send and sends nothing. `--dry-run` does the same at any time.
@@ -20,7 +20,7 @@ vectoys draws 3D objects as plain SVG paths. An item is a small React component
20
20
 
21
21
  Units are scene units. The camera's `width` (100 by default) and `height` are the SVG viewBox. Pick a scale such as `const mm = (v: number) => v * 0.4` and write real sizes.
22
22
 
23
- Axes. A part's own plane is x right, y down, z out of its top face. The model is x across, y towards the front, z up. A part lying flat faces up. Stand it upright towards the eye with `rotation: rotateX(-90)`.
23
+ Axes. A part's own plane is x right, y down, z out of its top face. The model is x across, y toward the front, z up. A part lying flat faces up. Stand it upright toward the eye with `rotation: rotateX(-90)`.
24
24
 
25
25
  Parts (`@vinena/vectoys`):
26
26
 
@@ -35,7 +35,7 @@ Poses and joints. A `Pose` is `{ position: [x, y, z], rotation }`. Build rotatio
35
35
 
36
36
  Camera. `camera({ width, height, perspective, yaw, pitch, roll, anchorX, anchorY, back, originX, originY })`. `perspective` about 280 to 400 for a scene 100 wide. `yaw` turns the model about its up axis and `pitch` tips it to or from the eye. A device at rest usually sits near `yaw: -24, pitch: -16`.
37
37
 
38
- Drawing. `draw(scene, eye)` returns `{ faces, hulls }`: the faces that face the eye, far to near, and each part's silhouette. Parts sort by `order` first, then by depth. Leave `order` out unless two parts draw in the wrong order: parts that interlock, or one that swings past another's centre (a lid over its base). Then give the part in front a higher `order`.
38
+ Drawing. `draw(scene, eye)` returns `{ faces, hulls }`: the faces that face the eye, far to near, and each part's silhouette. Parts sort by `order` first, then by depth. Leave `order` out unless two parts draw in the wrong order: parts that interlock, or one that swings past another's center (a lid over its base). Then give the part in front a higher `order`.
39
39
 
40
40
  `footprint(scene, eye, { gap, spread, ahead })` is the shadow on the ground under the model. Draw it first, blurred if you like. `poses(scene)` maps each part's name to its pose in the scene, for laying a screen on a child part.
41
41
 
@@ -45,13 +45,13 @@ Keep `seen(...)` in `geometry.ts`: one pure function from the item's state (open
45
45
 
46
46
  `<Faces faces hulls styles shading />` draws what `draw` returned. `styles` maps a role to SVG path props: `top`, `side`, `bottom`, a plate's `role`, or `"part.role"` for one part only (`"body.top"`, `"knot.side"`). Write fills as `style: { fill }`.
47
47
 
48
- Take colours from the page's CSS variables, each with a fallback so the item shows before the page sets them:
48
+ Take colors from the page's CSS variables, each with a fallback so the item shows before the page sets them:
49
49
 
50
50
  - `--vectoys-finish`: the main material. Expose a `finish` prop that sets it.
51
51
  - `--vectoys-surface`, `--vectoys-hairline`, `--vectoys-shadow`, `--vectoys-accent`, `--vectoys-ink`, `--vectoys-bg`. Add an item variable such as `--vectoys-paper` only when none of these fits.
52
52
  - `--vectoys-focus` and `--vectoys-focus-width` for focus rings.
53
53
 
54
- Mix tones with `color-mix(in srgb, ...)` instead of new colours. `shading` darkens faces turned from the light and lifts the few turned right at it. The page tunes it with `--vectoys-shade-strength` (0.5), `--vectoys-shade-color`, `--vectoys-highlight-strength` (0.25) and `--vectoys-highlight`. Draw a hairline outline over the main body's hull with `vectorEffect: "non-scaling-stroke"`. Check the item on a dark page too.
54
+ Mix tones with `color-mix(in srgb, ...)` instead of new colors. `shading` darkens faces turned from the light and lifts the few turned right at it. The page tunes it with `--vectoys-shade-strength` (0.5), `--vectoys-shade-color`, `--vectoys-highlight-strength` (0.25) and `--vectoys-highlight`. Draw a hairline outline over the main body's hull with `vectorEffect: "non-scaling-stroke"`. Check the item on a dark page too.
55
55
 
56
56
  ## Live HTML on a face
57
57
 
@@ -75,21 +75,21 @@ The same props must give the same markup on the server and the client, every tim
75
75
  ## Accessibility
76
76
 
77
77
  - A picture: the root is `role="img"` with an `aria-label` (Stage takes `role` and `label`). With live content inside, use `role="group"` so its links stay reachable.
78
- - Interactive: use a real `<button>` or `<a href>` with a label, laid over the item's bounds. Give it a visible `:focus-visible` ring in `--vectoys-focus`. Enter and Space must do what a click does.
78
+ - Interactive: use a real `<button>` or `<a href>` with a label, laid over the item's bounds. Give it a visible `:focus-visible` ring in `--vectoys-focus`. A `<button>` must do on Enter and Space what a click does, and an `<a href>` on Enter: both already do this natively, so do not call `preventDefault` on those keys or the click is cancelled. A link does nothing on Space; if it acts as a button, make it a `<button>`.
79
79
  - Respect reduced motion: the state still changes, only without the motion.
80
80
  - Start hover effects only when `event.pointerType === "mouse"`, so a tap does not stick in the hover state.
81
81
 
82
82
  ## Rules
83
83
 
84
84
  - No brand, maker or product names (no trademarked device names) in item.json, the code or the rendered text. Make a generic phone, not a named one.
85
- - Keep the files under 64 KB together.
85
+ - Keep the files under 96 KB together. Most items need far less; the limit leaves room for a device item with a full 3D model and a hand-drawn front.
86
86
  - React 19. Depend only on `@vinena/vectoys`, `@vinena/vectoys-react` and React. No other packages.
87
87
  - TypeScript strict. One component per item, named after the item (`my-toy` exports `MyToy`).
88
88
  - Props: `label`, `finish`, `yaw` (and `pitch`) for a device that turns, and the state as controlled plus default (`open` and `defaultOpen` with `onOpenChange`), as the registry items do.
89
89
 
90
90
  ## Example
91
91
 
92
- A badge that lifts towards the mouse and shows live HTML on its face. It passes all 8 checks.
92
+ A badge that lifts toward the mouse and shows live HTML on its face. It passes all 8 checks.
93
93
 
94
94
  `src/geometry.ts`:
95
95
 
@@ -136,7 +136,7 @@ const SHADOW = "var(--vectoys-shadow, rgba(0, 0, 0, 0.08))";
136
136
  const fill = (color: string): Look => ({ style: { fill: color } });
137
137
  const STYLES: Record<string, Look> = { top: fill(FINISH), side: fill(FINISH), bottom: fill(FINISH), face: fill(SURFACE) };
138
138
 
139
- /** A badge that lifts and turns towards a mouse over it, with live HTML on its face. */
139
+ /** A badge that lifts and turns toward a mouse over it, with live HTML on its face. */
140
140
  export function Badge({ children, label = "Badge" }: { children?: ReactNode; label?: string }) {
141
141
  const [over, setOver] = useState(false);
142
142
  // Springs to its target each frame; under reduced motion it jumps there.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { resolve as resolve6 } from "node:path";
4
+ import { resolve as resolve7 } from "node:path";
5
5
  // package.json
6
- var version = "0.1.0";
6
+ var version = "0.2.0";
7
7
 
8
8
  // src/args.ts
9
9
  var VALUES = new Map([
@@ -58,9 +58,9 @@ function parseArgs(argv) {
58
58
 
59
59
  // src/check.ts
60
60
  import { spawnSync as spawnSync3 } from "node:child_process";
61
- import { existsSync as existsSync5, lstatSync, mkdtempSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
61
+ import { existsSync as existsSync6, lstatSync, mkdtempSync, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
62
62
  import { tmpdir } from "node:os";
63
- import { basename, extname, join as join5, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
63
+ import { basename, extname, join as join6, relative as relative3, resolve as resolve4, sep as sep3 } from "node:path";
64
64
 
65
65
  // ../registry-build/src/index.ts
66
66
  function checkItem(value, file) {
@@ -76,8 +76,8 @@ function checkItem(value, file) {
76
76
 
77
77
  // src/commands.ts
78
78
  import { spawnSync } from "node:child_process";
79
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
80
- import { dirname, relative, resolve, sep } from "node:path";
79
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
80
+ import { dirname as dirname3, relative, resolve as resolve2, sep } from "node:path";
81
81
 
82
82
  // src/config.ts
83
83
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
@@ -149,9 +149,121 @@ function shared(dirs) {
149
149
  return first.slice(0, length).join("/") || ".";
150
150
  }
151
151
 
152
+ // src/imports.ts
153
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
154
+ import { dirname, isAbsolute, join as join2, posix as posix2, resolve } from "node:path";
155
+ var NODE = new Set(["node16", "node18", "node20", "nodenext"]);
156
+ function importExtension(cwd) {
157
+ const path = nearest(cwd, "tsconfig.json");
158
+ if (!path)
159
+ return;
160
+ const options = compilerOptions(path, new Set);
161
+ const resolution = lower(options.moduleResolution) ?? (NODE.has(lower(options.module) ?? "") ? "nodenext" : undefined);
162
+ if (!resolution || !NODE.has(resolution))
163
+ return;
164
+ return options.allowImportingTsExtensions === true ? "ts" : "js";
165
+ }
166
+ var SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(["'])(\.\.?\/[^"'\n]*)\2/g;
167
+ var SOURCE = /\.tsx?$/;
168
+ function withExtensions(files, extension) {
169
+ if (!extension)
170
+ return files;
171
+ const paths = new Set(files.map((file) => file.to));
172
+ return files.map((file) => {
173
+ if (!SOURCE.test(file.to))
174
+ return file;
175
+ const content = file.content.replace(SPECIFIER, (match, before, quote, specifier) => {
176
+ const base = posix2.join(posix2.dirname(file.to), specifier).replace(/\/$/, "");
177
+ if (paths.has(base))
178
+ return match;
179
+ const suffix = ["", "/index"].flatMap((index) => [`${index}.ts`, `${index}.tsx`]).find((candidate) => paths.has(base + candidate));
180
+ if (!suffix)
181
+ return match;
182
+ const named = extension === "ts" ? suffix : suffix.replace(SOURCE, ".js");
183
+ return `${before}${quote}${specifier.replace(/\/$/, "")}${named}${quote}`;
184
+ });
185
+ return { ...file, content };
186
+ });
187
+ }
188
+ function nearest(from, name) {
189
+ for (let dir = from;; dir = dirname(dir)) {
190
+ if (existsSync2(join2(dir, name)))
191
+ return join2(dir, name);
192
+ if (dirname(dir) === dir)
193
+ return;
194
+ }
195
+ }
196
+ function compilerOptions(path, seen) {
197
+ if (seen.has(path) || !existsSync2(path))
198
+ return {};
199
+ let config;
200
+ try {
201
+ config = JSON.parse(jsonc(readFileSync2(path, "utf8")));
202
+ } catch {
203
+ return {};
204
+ }
205
+ const parents = (Array.isArray(config.extends) ? config.extends : [config.extends]).filter((parent) => typeof parent === "string");
206
+ const inherited = parents.reduce((options, parent) => {
207
+ const file = extended(path, parent);
208
+ return file ? { ...options, ...compilerOptions(file, new Set([...seen, path])) } : options;
209
+ }, {});
210
+ return { ...inherited, ...config.compilerOptions };
211
+ }
212
+ function extended(path, parent) {
213
+ const named = (file) => [file, `${file}.json`].find((candidate) => existsSync2(candidate) && statSync(candidate).isFile());
214
+ if (/^\.\.?\//.test(parent) || isAbsolute(parent))
215
+ return named(resolve(dirname(path), parent));
216
+ for (let dir = dirname(path);; dir = dirname(dir)) {
217
+ const base = join2(dir, "node_modules", parent);
218
+ const found = named(base) ?? named(join2(base, packageConfig(base)));
219
+ if (found)
220
+ return found;
221
+ if (dirname(dir) === dir)
222
+ return;
223
+ }
224
+ }
225
+ function packageConfig(folder) {
226
+ try {
227
+ const { tsconfig } = JSON.parse(readFileSync2(join2(folder, "package.json"), "utf8"));
228
+ return typeof tsconfig === "string" ? tsconfig : "tsconfig.json";
229
+ } catch {
230
+ return "tsconfig.json";
231
+ }
232
+ }
233
+ function jsonc(text) {
234
+ let out = "";
235
+ for (let i = 0;i < text.length; i++) {
236
+ const char = text[i];
237
+ if (char === '"') {
238
+ let end = i + 1;
239
+ while (end < text.length && text[end] !== '"')
240
+ end += text[end] === "\\" ? 2 : 1;
241
+ out += text.slice(i, end + 1);
242
+ i = end;
243
+ } else if (char === "/" && text[i + 1] === "/") {
244
+ while (i < text.length && text[i] !== `
245
+ `)
246
+ i++;
247
+ out += `
248
+ `;
249
+ } else if (char === "/" && text[i + 1] === "*") {
250
+ const end = text.indexOf("*/", i + 2);
251
+ i = end === -1 ? text.length : end + 1;
252
+ } else if (char === "}" || char === "]") {
253
+ out = out.replace(/,\s*$/, "") + char;
254
+ } else {
255
+ out += char;
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+ function lower(value) {
261
+ return typeof value === "string" ? value.toLowerCase() : undefined;
262
+ }
263
+
152
264
  // src/pm.ts
153
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
154
- import { join as join2 } from "node:path";
265
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
266
+ import { dirname as dirname2, join as join3 } from "node:path";
155
267
  var LOCKFILES = [
156
268
  ["bun.lock", "bun"],
157
269
  ["bun.lockb", "bun"],
@@ -164,16 +276,37 @@ function isPackageSpec(value) {
164
276
  return typeof value === "string" && SPEC.test(value);
165
277
  }
166
278
  function detect(cwd) {
167
- return LOCKFILES.find(([file]) => existsSync2(join2(cwd, file)))?.[1] ?? "npm";
279
+ for (let dir = cwd;; dir = dirname2(dir)) {
280
+ const pkg = readPackage(join3(dir, "package.json"));
281
+ const named = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : undefined;
282
+ if (named === "bun" || named === "pnpm" || named === "yarn" || named === "npm")
283
+ return named;
284
+ const locked = LOCKFILES.find(([file]) => existsSync3(join3(dir, file)))?.[1];
285
+ if (locked)
286
+ return locked;
287
+ if (existsSync3(join3(dir, "pnpm-workspace.yaml")))
288
+ return "pnpm";
289
+ if (pkg?.packageManager || pkg?.workspaces || existsSync3(join3(dir, ".git")) || dirname2(dir) === dir)
290
+ return "npm";
291
+ }
292
+ }
293
+ function readPackage(path) {
294
+ if (!existsSync3(path))
295
+ return;
296
+ try {
297
+ return JSON.parse(readFileSync3(path, "utf8"));
298
+ } catch {
299
+ return;
300
+ }
168
301
  }
169
302
  function installCommand(pm, packages) {
170
303
  return [pm, [pm === "npm" ? "install" : "add", ...packages]];
171
304
  }
172
305
  function missing(cwd, wanted) {
173
- const path = join2(cwd, "package.json");
174
- if (!existsSync2(path))
306
+ const path = join3(cwd, "package.json");
307
+ if (!existsSync3(path))
175
308
  return wanted;
176
- const pkg = JSON.parse(readFileSync2(path, "utf8"));
309
+ const pkg = JSON.parse(readFileSync3(path, "utf8"));
177
310
  const have = new Set(["dependencies", "devDependencies", "peerDependencies"].flatMap((field) => Object.keys(pkg[field] ?? {})));
178
311
  return wanted.filter((spec) => !have.has(packageName(spec)));
179
312
  }
@@ -186,10 +319,10 @@ function packageName(spec) {
186
319
  import { createInterface } from "node:readline";
187
320
  function confirm(question) {
188
321
  const rl = createInterface({ input: process.stdin, output: process.stdout });
189
- return new Promise((resolve) => {
322
+ return new Promise((resolve2) => {
190
323
  rl.question(`${question} (y/N) `, (answer) => {
191
324
  rl.close();
192
- resolve(/^y(es)?$/i.test(answer.trim()));
325
+ resolve2(/^y(es)?$/i.test(answer.trim()));
193
326
  });
194
327
  });
195
328
  }
@@ -200,13 +333,13 @@ function itemUrl(spec, config, registry = config.registry) {
200
333
  return spec;
201
334
  const scoped = /^(@[^/]+)\/(.+)$/.exec(spec);
202
335
  if (scoped)
203
- return join3(registryFor(scoped[1], config), scoped[2]);
336
+ return join4(registryFor(scoped[1], config), scoped[2]);
204
337
  if (!/^[\w.-]+$/.test(spec))
205
338
  throw new Error(`"${spec}" is not an item name, an @scope/name or a URL.`);
206
- return join3(registry, spec);
339
+ return join4(registry, spec);
207
340
  }
208
341
  function indexUrl(scope, config, registry = config.registry) {
209
- return join3(scope ? registryFor(scope, config) : registry, "index");
342
+ return join4(scope ? registryFor(scope, config) : registry, "index");
210
343
  }
211
344
  function registryFor(scope, config) {
212
345
  const registry = config.registries[scope];
@@ -215,7 +348,7 @@ function registryFor(scope, config) {
215
348
  }
216
349
  return registry;
217
350
  }
218
- function join3(registry, name) {
351
+ function join4(registry, name) {
219
352
  if (registry.includes("{name}"))
220
353
  return registry.replace("{name}", name);
221
354
  return `${registry.replace(/\/+$/, "")}/${name}.json`;
@@ -244,11 +377,12 @@ async function add(context, specs) {
244
377
  const url = itemUrl(spec, config, context.registry);
245
378
  return { url, item: check(await fetchJson(url), url) };
246
379
  }));
247
- const root = resolve(context.cwd, config.dir);
380
+ const root = resolve2(context.cwd, config.dir);
381
+ const extension = importExtension(context.cwd);
248
382
  const writes = items.flatMap(({ url, item }) => {
249
- const folder = resolve(root, item.name);
250
- return place(item.files).map((file) => {
251
- const path = resolve(folder, file.to);
383
+ const folder = resolve2(root, item.name);
384
+ return withExtensions(place(item.files), extension).map((file) => {
385
+ const path = resolve2(folder, file.to);
252
386
  if (!folder.startsWith(root + sep) || !path.startsWith(folder + sep))
253
387
  throw new Error(`${url} has a file that would land outside ${config.dir}.`);
254
388
  return { path, content: file.content };
@@ -290,8 +424,8 @@ function check(value, url) {
290
424
  }
291
425
  async function write(context, path, content) {
292
426
  const shown = relative(context.cwd, path);
293
- if (existsSync3(path)) {
294
- if (readFileSync3(path, "utf8") === content) {
427
+ if (existsSync4(path)) {
428
+ if (readFileSync4(path, "utf8") === content) {
295
429
  context.log(`Same ${shown}`);
296
430
  return;
297
431
  }
@@ -301,7 +435,7 @@ async function write(context, path, content) {
301
435
  return;
302
436
  }
303
437
  }
304
- mkdirSync(dirname(path), { recursive: true });
438
+ mkdirSync(dirname3(path), { recursive: true });
305
439
  writeFileSync2(path, content);
306
440
  context.log(`Wrote ${shown}`);
307
441
  }
@@ -324,8 +458,8 @@ function installMissing(context, dependencies) {
324
458
 
325
459
  // src/create.ts
326
460
  import { spawnSync as spawnSync2 } from "node:child_process";
327
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
328
- import { dirname as dirname2, join as join4, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
461
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
462
+ import { dirname as dirname4, join as join5, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
329
463
  // ../../package.json
330
464
  var devDependencies = {
331
465
  "@happy-dom/global-registrator": "20.14.5",
@@ -334,7 +468,7 @@ var devDependencies = {
334
468
  };
335
469
 
336
470
  // ../../tsconfig.base.json
337
- var compilerOptions = {
471
+ var compilerOptions2 = {
338
472
  target: "ES2022",
339
473
  lib: ["ES2022", "DOM"],
340
474
  module: "ESNext",
@@ -360,19 +494,19 @@ function itemTsconfig(inRepo) {
360
494
  const include = ["src", "test"];
361
495
  if (inRepo)
362
496
  return { extends: BASE_TSCONFIG, compilerOptions: { jsx: "react-jsx" }, include };
363
- const { paths, rewriteRelativeImportExtensions, ...own } = compilerOptions;
497
+ const { paths, rewriteRelativeImportExtensions, ...own } = compilerOptions2;
364
498
  return { compilerOptions: { ...own, jsx: "react-jsx" }, include };
365
499
  }
366
500
  function inVectoysRepo(folder) {
367
- for (let dir = dirname2(folder);; dir = dirname2(dir)) {
368
- const pkg = join4(dir, "package.json");
369
- if (existsSync4(pkg) && existsSync4(join4(dir, "registry"))) {
370
- const value = JSON.parse(readFileSync4(pkg, "utf8"));
501
+ for (let dir = dirname4(folder);; dir = dirname4(dir)) {
502
+ const pkg = join5(dir, "package.json");
503
+ if (existsSync5(pkg) && existsSync5(join5(dir, "registry"))) {
504
+ const value = JSON.parse(readFileSync5(pkg, "utf8"));
371
505
  const named = typeof value === "object" && value !== null && "name" in value && value.name === "vectoys-workspace";
372
506
  if (named)
373
- return folder.startsWith(join4(dir, "registry") + sep2);
507
+ return folder.startsWith(join5(dir, "registry") + sep2);
374
508
  }
375
- if (dirname2(dir) === dir)
509
+ if (dirname4(dir) === dir)
376
510
  return false;
377
511
  }
378
512
  }
@@ -439,7 +573,7 @@ export const HEIGHT = 40;
439
573
  export const THICKNESS = 4;
440
574
  const INSET = 6;
441
575
 
442
- /** How the slab turns towards the eye at rest, in degrees. */
576
+ /** How the slab turns toward the eye at rest, in degrees. */
443
577
  export const TILT = { yaw: -24, pitch: -16 } as const;
444
578
 
445
579
  function parts(pose: Pose): Part[] {
@@ -470,7 +604,7 @@ export type View = ReturnType<typeof seen>;
470
604
  import { useMemo, type CSSProperties } from "react";
471
605
  import { TILT, seen } from "./geometry";
472
606
 
473
- // The page's colours, as CSS variables, each with a tone so the item shows as
607
+ // The page's colors, as CSS variables, each with a tone so the item shows as
474
608
  // meant before they are set.
475
609
  const HAIRLINE = "var(--vectoys-hairline, #8a8a8a)";
476
610
  const SURFACE = "var(--vectoys-surface, #f5f5f5)";
@@ -494,17 +628,17 @@ const OUTLINE: Look = {
494
628
  export type ${component}Props = {
495
629
  /** What the item shows, for assistive technology. */
496
630
  label?: string;
497
- /** The slab's colour; by default the page's \`--vectoys-finish\`. */
631
+ /** The slab's color; by default the page's \`--vectoys-finish\`. */
498
632
  finish?: string;
499
633
  /** How far it turns about its up axis, in degrees. */
500
634
  yaw?: number;
501
- /** How far it tips towards or away from the eye, in degrees. */
635
+ /** How far it tips toward or away from the eye, in degrees. */
502
636
  pitch?: number;
503
637
  className?: string;
504
638
  style?: CSSProperties;
505
639
  };
506
640
 
507
- /** ${title}: a slab tilted towards the eye, drawn the same on the server and the client. */
641
+ /** ${title}: a slab tilted toward the eye, drawn the same on the server and the client. */
508
642
  export function ${component}({ label = ${JSON.stringify(title)}, finish, yaw = TILT.yaw, pitch = TILT.pitch, className, style }: ${component}Props) {
509
643
  const view = useMemo(() => seen(yaw, pitch), [yaw, pitch]);
510
644
  const body = view.drawn.hulls.find((hull) => hull.part === "body");
@@ -540,13 +674,13 @@ function create(context, name, under) {
540
674
  throw new Error('Name the new item, as "vectoys create spinner".');
541
675
  if (!ITEM_NAME.test(name))
542
676
  throw new Error(`"${name}" is not an item name. Use letters, digits, dots, dashes and underscores.`);
543
- const folder = resolve2(context.cwd, under ?? ".", name);
677
+ const folder = resolve3(context.cwd, under ?? ".", name);
544
678
  const shown = relative2(context.cwd, folder) || ".";
545
- if (existsSync4(folder) && !context.overwrite)
679
+ if (existsSync5(folder) && !context.overwrite)
546
680
  throw new Error(`${shown} is already there. Pass --overwrite to write over it.`);
547
681
  for (const [path, content] of Object.entries(scaffold(name, gitAuthor(context.cwd), inVectoysRepo(folder)))) {
548
- const to = join4(folder, path);
549
- mkdirSync2(dirname2(to), { recursive: true });
682
+ const to = join5(folder, path);
683
+ mkdirSync2(dirname4(to), { recursive: true });
550
684
  writeFileSync3(to, content);
551
685
  context.log(`Wrote ${relative2(context.cwd, to)}`);
552
686
  }
@@ -573,7 +707,7 @@ var BRANDS = [
573
707
  "nintendo"
574
708
  ];
575
709
  var BRAND = new RegExp(`\\b(${BRANDS.map((brand) => brand.replace(" ", "\\s+")).join("|")})\\b`, "gi");
576
- var MAX_SOURCE = 64 * 1024;
710
+ var MAX_SOURCE = 96 * 1024;
577
711
  function brands(text) {
578
712
  return [...new Set([...text.matchAll(BRAND)].map((match) => match[0].toLowerCase().replace(/\s+/g, " ")))];
579
713
  }
@@ -605,9 +739,9 @@ function isRendered(value) {
605
739
  return "name" in value && typeof value.name === "string" && "html" in value && Array.isArray(value.html) && value.html.length === 2 && value.html.every((html) => typeof html === "string");
606
740
  }
607
741
  function runInBun(script, entry, dir, wanted) {
608
- const work = mkdtempSync(join5(tmpdir(), "vectoys-check-"));
742
+ const work = mkdtempSync(join6(tmpdir(), "vectoys-check-"));
609
743
  try {
610
- const runner = join5(work, "render.mjs");
744
+ const runner = join6(work, "render.mjs");
611
745
  writeFileSync4(runner, script);
612
746
  const result = spawnSync3("bun", [runner, entry, dir, wanted], { cwd: dir, encoding: "utf8", timeout: 60000, maxBuffer: 64 * 1024 * 1024 });
613
747
  if (result.error) {
@@ -661,12 +795,12 @@ function accessibleName(html) {
661
795
  function itemProblem(item, dir) {
662
796
  if (!ITEM_NAME.test(item.name))
663
797
  return `"${item.name}" is not an item name`;
664
- const file = item.files.find((path) => typeof path !== "string" || !resolve3(dir, path).startsWith(dir + sep3) || !existsSync5(resolve3(dir, path)));
798
+ const file = item.files.find((path) => typeof path !== "string" || !resolve4(dir, path).startsWith(dir + sep3) || !existsSync6(resolve4(dir, path)));
665
799
  if (file !== undefined)
666
800
  return `files lists ${JSON.stringify(file)}, which is not a file in the folder`;
667
- const other = item.files.find((path) => !lstatSync(resolve3(dir, path)).isFile());
801
+ const other = item.files.find((path) => !lstatSync(resolve4(dir, path)).isFile());
668
802
  if (other !== undefined) {
669
- const stat = lstatSync(resolve3(dir, other));
803
+ const stat = lstatSync(resolve4(dir, other));
670
804
  const kind = stat.isSymbolicLink() ? "a symlink, " : stat.isDirectory() ? "a folder, " : "";
671
805
  return `files lists ${JSON.stringify(other)}, which is ${kind}not a regular file`;
672
806
  }
@@ -676,12 +810,12 @@ function itemProblem(item, dir) {
676
810
  return;
677
811
  }
678
812
  function readItem(dir) {
679
- const path = join5(dir, "item.json");
680
- if (!existsSync5(path))
813
+ const path = join6(dir, "item.json");
814
+ if (!existsSync6(path))
681
815
  return "there is no item.json";
682
816
  let value;
683
817
  try {
684
- value = JSON.parse(readFileSync5(path, "utf8"));
818
+ value = JSON.parse(readFileSync6(path, "utf8"));
685
819
  } catch (error) {
686
820
  return `item.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`;
687
821
  }
@@ -702,7 +836,7 @@ function checks(dir) {
702
836
  if (typeof item === "string")
703
837
  return [{ pass: false, line: item }];
704
838
  const results = [{ pass: true, line: "item.json has what the registry needs" }];
705
- const size = item.files.reduce((sum, file) => sum + lstatSync(resolve3(dir, file)).size, 0);
839
+ const size = item.files.reduce((sum, file) => sum + lstatSync(resolve4(dir, file)).size, 0);
706
840
  const kb = (bytes) => `${(bytes / 1024).toFixed(1)} KB`;
707
841
  results.push({ pass: size < MAX_SOURCE, line: `the files are ${kb(size)}, ${size < MAX_SOURCE ? "under" : "over"} ${kb(MAX_SOURCE)}` });
708
842
  const named = brands([item.name, item.title, item.description].join(" "));
@@ -710,7 +844,7 @@ function checks(dir) {
710
844
  const component = componentFile(item);
711
845
  if (!component)
712
846
  return [...results, { pass: false, line: "files lists no .tsx component to render" }];
713
- const rendered = render(resolve3(dir, component), dir, componentName(item.name));
847
+ const rendered = render(resolve4(dir, component), dir, componentName(item.name));
714
848
  if ("error" in rendered)
715
849
  return [...results, { pass: false, line: `${component} does not render on the server: ${rendered.error}` }];
716
850
  const [html, again] = rendered.html;
@@ -725,7 +859,7 @@ function checks(dir) {
725
859
  return results;
726
860
  }
727
861
  function check2(context, given) {
728
- const dir = resolve3(context.cwd, given ?? ".");
862
+ const dir = resolve4(context.cwd, given ?? ".");
729
863
  context.log(`Checking ${relative3(context.cwd, dir) || "."}`);
730
864
  const results = checks(dir);
731
865
  for (const result of results)
@@ -739,7 +873,7 @@ function check2(context, given) {
739
873
  // src/preview.ts
740
874
  import { spawn } from "node:child_process";
741
875
  import { writeFileSync as writeFileSync5 } from "node:fs";
742
- import { join as join6, relative as relative4, resolve as resolve4 } from "node:path";
876
+ import { join as join7, relative as relative4, resolve as resolve5 } from "node:path";
743
877
  var YAWS = [-45, -20, 0, 20, 45];
744
878
  var RUNNER2 = bunScript(`const render = (props) => renderToStaticMarkup(createElement(mod[name], props));
745
879
  const yaws = ${JSON.stringify(YAWS)}.map((yaw) => ({ yaw, html: render({ yaw }) }));
@@ -810,19 +944,19 @@ function open(path) {
810
944
  spawn(command, args, { detached: true, stdio: "ignore" }).unref();
811
945
  }
812
946
  function preview(context, given, opening = false) {
813
- const dir = resolve4(context.cwd, given ?? ".");
947
+ const dir = resolve5(context.cwd, given ?? ".");
814
948
  const item = readItem(dir);
815
949
  if (typeof item === "string")
816
950
  throw new Error(`${relative4(context.cwd, dir) || "."} is not an item to preview: ${item}.`);
817
951
  const component = componentFile(item);
818
952
  if (!component)
819
953
  throw new Error("item.json lists no .tsx component to render.");
820
- const ran = runInBun(RUNNER2, resolve4(dir, component), dir, componentName(item.name));
954
+ const ran = runInBun(RUNNER2, resolve5(dir, component), dir, componentName(item.name));
821
955
  if ("error" in ran)
822
956
  throw new Error(`${component} does not render on the server: ${ran.error}`);
823
957
  if (!isPreview(ran.value))
824
958
  throw new Error("bun gave back something other than markup.");
825
- const path = join6(dir, "preview.html");
959
+ const path = join7(dir, "preview.html");
826
960
  writeFileSync5(path, previewPage(item.title, ran.value));
827
961
  context.log(path);
828
962
  if (opening)
@@ -831,20 +965,20 @@ function preview(context, given, opening = false) {
831
965
  }
832
966
 
833
967
  // src/skill.ts
834
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
968
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
835
969
  import { fileURLToPath } from "node:url";
836
970
  var SKILL = fileURLToPath(new URL("../SKILL.md", import.meta.url));
837
971
  function skill(context, path = SKILL) {
838
- if (!existsSync6(path))
972
+ if (!existsSync7(path))
839
973
  throw new Error(`${path} is missing. Reinstall vectoys.`);
840
- context.log(readFileSync6(path, "utf8").trimEnd());
974
+ context.log(readFileSync7(path, "utf8").trimEnd());
841
975
  }
842
976
 
843
977
  // src/submit.ts
844
978
  import { spawnSync as spawnSync4 } from "node:child_process";
845
- import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
979
+ import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
846
980
  import { tmpdir as tmpdir2 } from "node:os";
847
- import { dirname as dirname3, join as join7, relative as relative5, resolve as resolve5 } from "node:path";
981
+ import { dirname as dirname5, join as join8, relative as relative5, resolve as resolve6 } from "node:path";
848
982
  var SUBMISSIONS_OPEN = false;
849
983
  var UPSTREAM = "vinenastudio/vectoys";
850
984
  var CLOSED = "Submissions open when vectoys launches. Nothing was sent.";
@@ -871,9 +1005,9 @@ function openPullRequest(change, runner, cwd) {
871
1005
  const login = must(runner, "gh", ["api", "user", "--jq", ".login"], cwd);
872
1006
  must(runner, "gh", ["repo", "fork", UPSTREAM, "--clone=false"], cwd);
873
1007
  must(runner, "gh", ["repo", "sync", `${login}/vectoys`, "--branch", "main"], cwd);
874
- const work = mkdtempSync2(join7(tmpdir2(), "vectoys-submit-"));
1008
+ const work = mkdtempSync2(join8(tmpdir2(), "vectoys-submit-"));
875
1009
  try {
876
- const clone = join7(work, "vectoys");
1010
+ const clone = join8(work, "vectoys");
877
1011
  must(runner, "gh", ["repo", "clone", `${login}/vectoys`, clone, "--", "--depth", "1"], work);
878
1012
  must(runner, "git", ["checkout", "-b", change.branch], clone);
879
1013
  change.write(clone);
@@ -902,14 +1036,14 @@ function steps(change, own) {
902
1036
  ];
903
1037
  }
904
1038
  function itemFiles(dir, listed) {
905
- const extra = ["package.json", "tsconfig.json", "test"].filter((path) => existsSync7(join7(dir, path)));
1039
+ const extra = ["package.json", "tsconfig.json", "test"].filter((path) => existsSync8(join8(dir, path)));
906
1040
  return [...new Set(["item.json", ...listed, ...extra])];
907
1041
  }
908
1042
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
909
1043
  function inRepoForm(folder) {
910
- const pkgPath = join7(folder, "package.json");
911
- if (existsSync7(pkgPath)) {
912
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1044
+ const pkgPath = join8(folder, "package.json");
1045
+ if (existsSync8(pkgPath)) {
1046
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
913
1047
  if (!isRecord(pkg))
914
1048
  throw new Error("The item's package.json is not an object.");
915
1049
  for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
@@ -925,13 +1059,13 @@ function inRepoForm(folder) {
925
1059
  writeFileSync6(pkgPath, `${JSON.stringify(pkg, null, 2)}
926
1060
  `);
927
1061
  }
928
- const tsPath = join7(folder, "tsconfig.json");
929
- if (existsSync7(tsPath)) {
930
- const tsconfig = JSON.parse(readFileSync7(tsPath, "utf8"));
1062
+ const tsPath = join8(folder, "tsconfig.json");
1063
+ if (existsSync8(tsPath)) {
1064
+ const tsconfig = JSON.parse(readFileSync8(tsPath, "utf8"));
931
1065
  if (!isRecord(tsconfig))
932
1066
  throw new Error("The item's tsconfig.json is not an object.");
933
1067
  const own = isRecord(tsconfig.compilerOptions) ? tsconfig.compilerOptions : {};
934
- const kept = Object.fromEntries(Object.entries(own).filter(([key]) => !(key in compilerOptions)));
1068
+ const kept = Object.fromEntries(Object.entries(own).filter(([key]) => !(key in compilerOptions2)));
935
1069
  writeFileSync6(tsPath, `${JSON.stringify({ extends: BASE_TSCONFIG, ...tsconfig, compilerOptions: kept }, null, 2)}
936
1070
  `);
937
1071
  }
@@ -971,11 +1105,11 @@ function itemChange(context, dir) {
971
1105
  steps: steps({ branch, title }, [`Copy ${files.join(", ")} into registry/${item.name}/`]),
972
1106
  write: (clone) => {
973
1107
  for (const file of files) {
974
- const to = join7(clone, "registry", item.name, file);
975
- mkdirSync3(dirname3(to), { recursive: true });
976
- cpSync(join7(dir, file), to, { recursive: true });
1108
+ const to = join8(clone, "registry", item.name, file);
1109
+ mkdirSync3(dirname5(to), { recursive: true });
1110
+ cpSync(join8(dir, file), to, { recursive: true });
977
1111
  }
978
- inRepoForm(join7(clone, "registry", item.name));
1112
+ inRepoForm(join8(clone, "registry", item.name));
979
1113
  }
980
1114
  };
981
1115
  }
@@ -1031,8 +1165,8 @@ async function listingChange(context, given, runner) {
1031
1165
  body,
1032
1166
  steps: steps({ branch, title }, [`Add ${JSON.stringify(listing)} to registries.json, sorted by name`]),
1033
1167
  write: (clone) => {
1034
- const path = join7(clone, "registries.json");
1035
- const found = existsSync7(path) ? JSON.parse(readFileSync7(path, "utf8")) : [];
1168
+ const path = join8(clone, "registries.json");
1169
+ const found = existsSync8(path) ? JSON.parse(readFileSync8(path, "utf8")) : [];
1036
1170
  if (!isListings(found))
1037
1171
  throw new Error("registries.json in the fork is not a list of registries.");
1038
1172
  writeFileSync6(path, `${JSON.stringify(listed(found, listing), null, 2)}
@@ -1042,7 +1176,7 @@ async function listingChange(context, given, runner) {
1042
1176
  }
1043
1177
  async function submit(context, given, options) {
1044
1178
  const runner = options.run ?? run;
1045
- const change = options.registry ? await listingChange(context, options.registry, runner) : itemChange(context, resolve5(context.cwd, given ?? "."));
1179
+ const change = options.registry ? await listingChange(context, options.registry, runner) : itemChange(context, resolve6(context.cwd, given ?? "."));
1046
1180
  if (!(options.submissionsOpen ?? SUBMISSIONS_OPEN) || options.dryRun) {
1047
1181
  context.log("");
1048
1182
  context.log("submit would:");
@@ -1101,7 +1235,7 @@ async function main(argv) {
1101
1235
  if (flags.help || !command)
1102
1236
  return console.log(HELP);
1103
1237
  const context = {
1104
- cwd: resolve6(flags.cwd ?? process.cwd()),
1238
+ cwd: resolve7(flags.cwd ?? process.cwd()),
1105
1239
  registry: flags.registry ?? (process.env.VECTOYS_REGISTRY || undefined),
1106
1240
  registryFlag: flags.registry,
1107
1241
  yes: flags.yes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vinena/vectoys-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Copy vectoys devices and toys into your project.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/vinenastudio/vectoys#readme",