@vinena/vectoys-cli 0.2.1 → 0.3.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/SKILL.md +4 -4
- package/dist/index.js +368 -42
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -11,10 +11,10 @@ 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
|
|
14
|
+
3. `npx @vinena/vectoys-cli check <dir>` runs 10 checks: item.json is valid, the files are under 96 KB together, item.json names no brand, the code has none of the unsafe patterns check knows, the component renders on the server with no props, the markup has no `NaN` or `Infinity`, two renders give the same markup, the markup has no scripts or outside links, the rendered text names no brand, and the root has an accessible name. `check` catches common unsafe patterns, and a maintainer reviews every submission before it is listed. It fails any code file in the folder, tests included, that uses `Bun`, `process`, `require`, `globalThis`, `window`, `self`, `opener` or `defaultView`, `<script>`, frames, objects, embeds or forms, `innerHTML` or `dangerouslySetInnerHTML`, `eval` or `Function`, string timers, network calls, workers, `postMessage`, the clipboard, `window.open` or `location`, dynamic `import()`, browser storage or cookies, or outside or `javascript:` URLs. The files item.json lists import only each other, `react`, `@vinena/vectoys` and `@vinena/vectoys-react`, which are also the only dependencies item.json may list; tests may also import `bun:test` and `react-dom/server`. Inside a vectoys checkout, tests under `registry/<item>/test/` also use the helpers in `packages/react/test/dom.tsx` (`installGlobal`, `htmlOf`, `fireOnWindow`, and `readSibling`, which reads only files under `registry/`). An item made outside the repository has no such helpers, so keep its tests to rendering with `react-dom/server`. Keep `package.json` and `tsconfig.json` as `create` writes them, and add no dot files but `.gitignore`. 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
|
-
6. `npx @vinena/vectoys-cli submit <dir>` runs the checks, then opens a pull request (it needs the GitHub CLI, `gh
|
|
17
|
+
6. `npx @vinena/vectoys-cli submit <dir>` runs the checks, then opens a pull request (it needs the GitHub CLI, `gh`, and bun 1.3.2 or newer) that adds the item to vinenastudio/vectoys: the files `item.json` lists, its `package.json`, `tsconfig.json`, `src/index.ts` and `test/`, and the item's package in `bun.lock`. 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.
|
|
18
18
|
|
|
19
19
|
## The model
|
|
20
20
|
|
|
@@ -89,7 +89,7 @@ The same props must give the same markup on the server and the client, every tim
|
|
|
89
89
|
|
|
90
90
|
## Example
|
|
91
91
|
|
|
92
|
-
A badge that lifts toward the mouse and shows live HTML on its face. It passes all
|
|
92
|
+
A badge that lifts toward the mouse and shows live HTML on its face. It passes all 10 checks.
|
|
93
93
|
|
|
94
94
|
`src/geometry.ts`:
|
|
95
95
|
|
|
@@ -165,7 +165,7 @@ export function Badge({ children, label = "Badge" }: { children?: ReactNode; lab
|
|
|
165
165
|
|
|
166
166
|
## Before you submit
|
|
167
167
|
|
|
168
|
-
- `npx @vinena/vectoys-cli check <dir>` passes all
|
|
168
|
+
- `npx @vinena/vectoys-cli check <dir>` passes all 10 checks.
|
|
169
169
|
- The preview looks right in light and dark and at every yaw.
|
|
170
170
|
- Pointer, touch and keyboard all work, and reduced motion keeps the state changes.
|
|
171
171
|
- `item.json` has a plain title and a description that says what the item is and what it does.
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { resolve as resolve7 } from "node:path";
|
|
5
5
|
// package.json
|
|
6
|
-
var version = "0.
|
|
6
|
+
var version = "0.3.0";
|
|
7
7
|
|
|
8
8
|
// src/args.ts
|
|
9
9
|
var VALUES = new Map([
|
|
@@ -58,10 +58,15 @@ function parseArgs(argv) {
|
|
|
58
58
|
|
|
59
59
|
// src/check.ts
|
|
60
60
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
61
|
-
import { existsSync as existsSync6, lstatSync, mkdtempSync, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
61
|
+
import { existsSync as existsSync6, lstatSync, mkdtempSync, readdirSync, readFileSync as readFileSync6, realpathSync, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
62
62
|
import { tmpdir } from "node:os";
|
|
63
|
-
import { basename, extname, join as join6, relative as relative3, resolve as resolve4, sep as sep3 } from "node:path";
|
|
63
|
+
import { basename, extname, join as join6, posix as posix3, relative as relative3, resolve as resolve4, sep as sep3 } from "node:path";
|
|
64
64
|
|
|
65
|
+
// ../registry-build/src/names.ts
|
|
66
|
+
function componentName(name) {
|
|
67
|
+
const joined = name.split(/[^a-zA-Z0-9]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
68
|
+
return /^[A-Za-z]/.test(joined) ? joined : `Item${joined}`;
|
|
69
|
+
}
|
|
65
70
|
// ../registry-build/src/index.ts
|
|
66
71
|
function checkItem(value, file) {
|
|
67
72
|
const item = value;
|
|
@@ -71,8 +76,13 @@ function checkItem(value, file) {
|
|
|
71
76
|
}
|
|
72
77
|
if (!Array.isArray(item.files) || item.files.length === 0)
|
|
73
78
|
throw new Error(`${file} needs "files"`);
|
|
79
|
+
if (item.author !== undefined && !isOneLine(item.author))
|
|
80
|
+
throw new Error(`${file} "author" must be one line of text like "Name (https://site)"`);
|
|
81
|
+
if (item.credit !== undefined && !isOneLine(item.credit))
|
|
82
|
+
throw new Error(`${file} "credit" must be one line of text`);
|
|
74
83
|
return item;
|
|
75
84
|
}
|
|
85
|
+
var isOneLine = (value) => typeof value === "string" && value.trim() !== "" && !/[\r\n\u2028\u2029]/.test(value) && !value.includes("*/");
|
|
76
86
|
|
|
77
87
|
// src/commands.ts
|
|
78
88
|
import { spawnSync } from "node:child_process";
|
|
@@ -307,8 +317,48 @@ function missing(cwd, wanted) {
|
|
|
307
317
|
if (!existsSync3(path))
|
|
308
318
|
return wanted;
|
|
309
319
|
const pkg = JSON.parse(readFileSync3(path, "utf8"));
|
|
310
|
-
const
|
|
311
|
-
return wanted.filter((spec) =>
|
|
320
|
+
const listed = Object.assign({}, ...["peerDependencies", "devDependencies", "dependencies"].map((field) => pkg[field] ?? {}));
|
|
321
|
+
return wanted.filter((spec) => {
|
|
322
|
+
const name = packageName(spec);
|
|
323
|
+
if (!Object.hasOwn(listed, name))
|
|
324
|
+
return true;
|
|
325
|
+
const want = base(spec.slice(name.length + 1));
|
|
326
|
+
const range = listed[name];
|
|
327
|
+
if (!want || typeof range === "string" && range.startsWith("workspace:"))
|
|
328
|
+
return false;
|
|
329
|
+
const installed = readPackage(join3(cwd, "node_modules", name, "package.json"))?.version;
|
|
330
|
+
const have = [installed, range].flatMap((version2) => typeof version2 === "string" ? base(version2) ?? [] : []);
|
|
331
|
+
return !have.some((version2) => compare(version2, want) >= 0);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
var base = (range) => {
|
|
335
|
+
const found = /^[\^~]?=?v?(\d+)(?:\.(\d+)(?:\.(\d+)(?:-([\w.-]+))?)?)?(?:\+[\w.-]*)?$/.exec(range.trim());
|
|
336
|
+
return found ? { parts: [found[1], found[2], found[3]].map((part) => Number(part ?? 0)), pre: found[4]?.split(".") ?? [] } : undefined;
|
|
337
|
+
};
|
|
338
|
+
function compare(a, b) {
|
|
339
|
+
for (const [at, part] of a.parts.entries()) {
|
|
340
|
+
const other = b.parts[at];
|
|
341
|
+
if (other === undefined)
|
|
342
|
+
return 1;
|
|
343
|
+
if (part !== other)
|
|
344
|
+
return part - other;
|
|
345
|
+
}
|
|
346
|
+
if (a.pre.length === 0 || b.pre.length === 0)
|
|
347
|
+
return b.pre.length - a.pre.length;
|
|
348
|
+
for (const [at, id] of a.pre.entries()) {
|
|
349
|
+
const other = b.pre[at];
|
|
350
|
+
if (other === undefined)
|
|
351
|
+
return 1;
|
|
352
|
+
if (id === other)
|
|
353
|
+
continue;
|
|
354
|
+
const [x, y] = [/^\d+$/.test(id), /^\d+$/.test(other)];
|
|
355
|
+
if (x && y)
|
|
356
|
+
return Number(id) - Number(other);
|
|
357
|
+
if (x !== y)
|
|
358
|
+
return x ? -1 : 1;
|
|
359
|
+
return id < other ? -1 : 1;
|
|
360
|
+
}
|
|
361
|
+
return a.pre.length - b.pre.length;
|
|
312
362
|
}
|
|
313
363
|
function packageName(spec) {
|
|
314
364
|
const at = spec.indexOf("@", 1);
|
|
@@ -463,6 +513,7 @@ import { dirname as dirname4, join as join5, relative as relative2, resolve as r
|
|
|
463
513
|
// ../../package.json
|
|
464
514
|
var devDependencies = {
|
|
465
515
|
"@happy-dom/global-registrator": "20.14.5",
|
|
516
|
+
"@playwright/test": "1.63.0",
|
|
466
517
|
"@types/bun": "1.3.14",
|
|
467
518
|
typescript: "7.0.2"
|
|
468
519
|
};
|
|
@@ -510,10 +561,6 @@ function inVectoysRepo(folder) {
|
|
|
510
561
|
return false;
|
|
511
562
|
}
|
|
512
563
|
}
|
|
513
|
-
function componentName(name) {
|
|
514
|
-
const joined = name.split(/[^a-zA-Z0-9]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
515
|
-
return /^[A-Za-z]/.test(joined) ? joined : `Item${joined}`;
|
|
516
|
-
}
|
|
517
564
|
function titleOf(name) {
|
|
518
565
|
const words = name.replace(/[-_.]+/g, " ").trim() || "item";
|
|
519
566
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
@@ -522,24 +569,13 @@ function gitAuthor(cwd) {
|
|
|
522
569
|
const result = spawnSync2("git", ["config", "user.name"], { cwd, encoding: "utf8" });
|
|
523
570
|
return result.status === 0 ? result.stdout.trim() || undefined : undefined;
|
|
524
571
|
}
|
|
525
|
-
function
|
|
526
|
-
const component = componentName(name);
|
|
527
|
-
const title = titleOf(name);
|
|
528
|
-
const item = {
|
|
529
|
-
name,
|
|
530
|
-
title,
|
|
531
|
-
description: `${title}: a tilted slab drawn as SVG paths. Say what it is and what it does.`,
|
|
532
|
-
files: ["src/geometry.ts", `src/${name}.tsx`],
|
|
533
|
-
dependencies: ["@vinena/vectoys", "@vinena/vectoys-react"],
|
|
534
|
-
license: "MIT",
|
|
535
|
-
...author ? { author } : {}
|
|
536
|
-
};
|
|
572
|
+
function itemPackage(name, description, inRepo) {
|
|
537
573
|
const vectoys = inRepo ? "workspace:*" : `^${version}`;
|
|
538
|
-
|
|
574
|
+
return {
|
|
539
575
|
name: `@vectoys/${name}`,
|
|
540
576
|
version: "0.0.0",
|
|
541
577
|
private: true,
|
|
542
|
-
description
|
|
578
|
+
description,
|
|
543
579
|
type: "module",
|
|
544
580
|
sideEffects: false,
|
|
545
581
|
exports: { ".": "./src/index.ts" },
|
|
@@ -554,6 +590,20 @@ function scaffold(name, author, inRepo = false) {
|
|
|
554
590
|
"react-dom": "19.0.0"
|
|
555
591
|
}
|
|
556
592
|
};
|
|
593
|
+
}
|
|
594
|
+
function scaffold(name, author, inRepo = false) {
|
|
595
|
+
const component = componentName(name);
|
|
596
|
+
const title = titleOf(name);
|
|
597
|
+
const item = {
|
|
598
|
+
name,
|
|
599
|
+
title,
|
|
600
|
+
description: `${title}: a tilted slab drawn as SVG paths. Say what it is and what it does.`,
|
|
601
|
+
files: ["src/geometry.ts", `src/${name}.tsx`],
|
|
602
|
+
dependencies: ["@vinena/vectoys", "@vinena/vectoys-react"],
|
|
603
|
+
license: "MIT",
|
|
604
|
+
...author ? { author } : {}
|
|
605
|
+
};
|
|
606
|
+
const pkg = itemPackage(name, `${title} drawn with the vectoys engine.`, inRepo);
|
|
557
607
|
const tsconfig = itemTsconfig(inRepo);
|
|
558
608
|
const json = (value) => `${JSON.stringify(value, null, 2)}
|
|
559
609
|
`;
|
|
@@ -652,9 +702,12 @@ export function ${component}({ label = ${JSON.stringify(title)}, finish, yaw = T
|
|
|
652
702
|
</div>
|
|
653
703
|
);
|
|
654
704
|
}
|
|
705
|
+
|
|
706
|
+
/** How the vectoys.com gallery shows the item on its card, with no props. Change it to show the item at its best. */
|
|
707
|
+
export const Demo = () => <${component} />;
|
|
655
708
|
`,
|
|
656
709
|
"src/index.ts": `export { HEIGHT, THICKNESS, TILT, WIDTH, seen, type View } from "./geometry";
|
|
657
|
-
export { ${component}, type ${component}Props } from "./${name}";
|
|
710
|
+
export { Demo, ${component}, type ${component}Props } from "./${name}";
|
|
658
711
|
`,
|
|
659
712
|
[`test/${name}.test.tsx`]: `import { expect, test } from "bun:test";
|
|
660
713
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
@@ -743,7 +796,7 @@ function runInBun(script, entry, dir, wanted) {
|
|
|
743
796
|
try {
|
|
744
797
|
const runner = join6(work, "render.mjs");
|
|
745
798
|
writeFileSync4(runner, script);
|
|
746
|
-
const result = spawnSync3("bun", [runner, entry, dir, wanted], { cwd:
|
|
799
|
+
const result = spawnSync3("bun", [runner, entry, dir, wanted], { cwd: work, env: { PATH: process.env.PATH ?? "" }, encoding: "utf8", timeout: 60000, maxBuffer: 64 * 1024 * 1024 });
|
|
747
800
|
if (result.error) {
|
|
748
801
|
const missing2 = "code" in result.error && result.error.code === "ENOENT";
|
|
749
802
|
return { error: missing2 ? "it needs bun to load the item's TSX: https://bun.sh" : result.error.message };
|
|
@@ -804,11 +857,242 @@ function itemProblem(item, dir) {
|
|
|
804
857
|
const kind = stat.isSymbolicLink() ? "a symlink, " : stat.isDirectory() ? "a folder, " : "";
|
|
805
858
|
return `files lists ${JSON.stringify(other)}, which is ${kind}not a regular file`;
|
|
806
859
|
}
|
|
860
|
+
const outside = item.files.find((path) => !realpathSync(resolve4(dir, path)).startsWith(realpathSync(dir) + sep3));
|
|
861
|
+
if (outside !== undefined)
|
|
862
|
+
return `files lists ${JSON.stringify(outside)}, which is outside the folder`;
|
|
807
863
|
if (item.dependencies !== undefined && !(Array.isArray(item.dependencies) && item.dependencies.every(isPackageSpec))) {
|
|
808
864
|
return "dependencies are not all plain npm package names";
|
|
809
865
|
}
|
|
810
866
|
return;
|
|
811
867
|
}
|
|
868
|
+
var ALLOWED_DEPENDENCIES = ["@vinena/vectoys", "@vinena/vectoys-react"];
|
|
869
|
+
var ALLOWED_IMPORTS = [...ALLOWED_DEPENDENCIES, "react"];
|
|
870
|
+
var ALLOWED_TEST_IMPORTS = ["bun:test", "react-dom/server"];
|
|
871
|
+
var TEST_HELPERS = [/^\.\.\/\.\.\/\.\.\/packages\/react\/test\/dom$/, /^\.\.\/\.\.\/[a-z0-9][a-z0-9._-]*\/src$/];
|
|
872
|
+
var SCANNER = `
|
|
873
|
+
const [, dir, list] = process.argv.slice(2);
|
|
874
|
+
const { join } = await import("node:path");
|
|
875
|
+
const code = (path) => /\\.[cm]?[jt]sx?$/.test(path);
|
|
876
|
+
const sources = [];
|
|
877
|
+
for (const path of JSON.parse(list)) {
|
|
878
|
+
const text = await Bun.file(join(dir, path)).text();
|
|
879
|
+
if (!code(path)) { sources.push({ path, text, code: text, imports: [] }); continue; }
|
|
880
|
+
try {
|
|
881
|
+
const transpiler = new Bun.Transpiler({ loader: path.endsWith("x") ? "tsx" : "ts", deadCodeElimination: false });
|
|
882
|
+
sources.push({ path, text, code: transpiler.transformSync(text), imports: transpiler.scan(text).imports });
|
|
883
|
+
} catch (error) {
|
|
884
|
+
sources.push({ path, error: error instanceof Error ? error.message : String(error) });
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
process.stdout.write("\\n${MARK}" + JSON.stringify(sources));
|
|
888
|
+
`;
|
|
889
|
+
function isSource(value) {
|
|
890
|
+
if (typeof value !== "object" || value === null || !("path" in value) || typeof value.path !== "string")
|
|
891
|
+
return false;
|
|
892
|
+
if ("error" in value)
|
|
893
|
+
return typeof value.error === "string";
|
|
894
|
+
return "text" in value && typeof value.text === "string" && "code" in value && typeof value.code === "string" && "imports" in value && Array.isArray(value.imports) && value.imports.every((entry) => typeof entry === "object" && entry !== null && ("path" in entry) && typeof entry.path === "string" && ("kind" in entry) && typeof entry.kind === "string");
|
|
895
|
+
}
|
|
896
|
+
function sources(dir, files) {
|
|
897
|
+
const ran = runInBun(SCANNER, dir, dir, JSON.stringify(files));
|
|
898
|
+
if ("error" in ran)
|
|
899
|
+
return ran.error;
|
|
900
|
+
return Array.isArray(ran.value) && ran.value.every(isSource) ? ran.value : "bun gave back something other than the files";
|
|
901
|
+
}
|
|
902
|
+
var entries = (dir, under) => readdirSync(join6(dir, under), { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
903
|
+
function walk(dir, under = "") {
|
|
904
|
+
return entries(dir, under).flatMap((entry) => {
|
|
905
|
+
const path = under ? `${under}/${entry.name}` : entry.name;
|
|
906
|
+
if (entry.isDirectory())
|
|
907
|
+
return entry.name === "node_modules" ? [] : walk(dir, path);
|
|
908
|
+
return entry.isFile() ? [path] : [];
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
var CODE = /\.[cm]?[jt]sx?$/;
|
|
912
|
+
function codeFiles(dir) {
|
|
913
|
+
return walk(dir).filter((path) => CODE.test(path));
|
|
914
|
+
}
|
|
915
|
+
var STRING = "[\"'`]";
|
|
916
|
+
var GLOBALS = ["opener", "defaultView", "fetch", "eval", "Function", "XMLHttpRequest", "WebSocket", "EventSource", "sendBeacon", "importScripts", "localStorage", "sessionStorage", "indexedDB", "cookie", "constructor", "open", "location", "postMessage", "clipboard"];
|
|
917
|
+
var ELEMENTS = "script|iframe|frame|object|embed|form";
|
|
918
|
+
var UNSAFE = [
|
|
919
|
+
[/\b(Bun|process|Deno|module|exports|require|__dirname|__filename|globalThis)\b/, "uses the server runtime"],
|
|
920
|
+
[/\bdefaultView\b|(?<![.\w$])(window|self|opener)(?=\s*[?.[(=;,)\]}])/, "reaches the window object"],
|
|
921
|
+
[/<script\b/i, "has a <script> tag"],
|
|
922
|
+
[/<(iframe|frame|object|embed|form)\b/i, "has a frame, object, embed or form tag"],
|
|
923
|
+
[new RegExp(`\\b(jsx\\w*|createElement)\\s*\\(\\s*${STRING}(${ELEMENTS})${STRING}`, "i"), "makes a script, frame, object, embed or form element"],
|
|
924
|
+
[new RegExp(`${STRING}(script|iframe|frame|embed|form)${STRING}`, "i"), "names a script, frame, embed or form element in a string"],
|
|
925
|
+
[/\bsrcdoc\b/i, "sets srcdoc"],
|
|
926
|
+
[/\b(dangerouslySetInnerHTML|innerHTML|outerHTML|insertAdjacentHTML)\b/, "sets HTML from a string"],
|
|
927
|
+
[/\bdocument\s*\.\s*write/, "writes into the document"],
|
|
928
|
+
[/\beval\b/, "uses eval"],
|
|
929
|
+
[/\bFunction\b|\.\s*constructor\b|\bconstructor\s*[:,}]/, "reaches the Function constructor"],
|
|
930
|
+
[new RegExp(`\\bset(Timeout|Interval)\\s*\\(\\s*${STRING}`), "passes a string to setTimeout or setInterval"],
|
|
931
|
+
[/\bfetch\b/, "uses fetch"],
|
|
932
|
+
[/\b(XMLHttpRequest|WebSocket|EventSource|sendBeacon|importScripts)\b/, "makes a network call"],
|
|
933
|
+
[/\b(Worker|SharedWorker|serviceWorker|postMessage)\b/, "talks to a worker or another window"],
|
|
934
|
+
[/\bclipboard\b/, "uses the clipboard"],
|
|
935
|
+
[/(?<![\w$])open\s*\(/, "opens a window"],
|
|
936
|
+
[/\breadSibling\s*\((?!\s*import\.meta\.url\s*,)/, "reads a file from somewhere other than its own test"],
|
|
937
|
+
[/\blocation\s*(\.\s*(href|assign|replace|reload)\b|=(?!=))|\b(window|document)\s*\.\s*location\b/, "moves the page"],
|
|
938
|
+
[/\bimport\s*\(/, "has a dynamic import"],
|
|
939
|
+
[/\b(localStorage|sessionStorage|indexedDB)\b|\bdocument\s*\.\s*cookie\b/, "uses browser storage or cookies"],
|
|
940
|
+
[new RegExp(`${STRING}(${GLOBALS.join("|")})${STRING}`), "names a blocked browser feature in a string"],
|
|
941
|
+
[/javascript\s*:/i, "has a javascript: URL"],
|
|
942
|
+
[new RegExp(`\\bhttps?://(?!www\\.w3\\.org/)|[${"\"'`"}}]\\s*//[a-z0-9]|${STRING}\\s*data:\\s*text/html`, "i"), "has an outside URL"],
|
|
943
|
+
[/["'`}]\s*(?:\/\\\\|\\\\[\/\\])/, "has an outside URL"],
|
|
944
|
+
[/url\(\s*["']?\s*(https?:|\/\/)|@import\b/i, "loads a style from outside"]
|
|
945
|
+
];
|
|
946
|
+
var UNSAFE_MARKUP = [
|
|
947
|
+
[/<script\b/i, "has a <script> tag"],
|
|
948
|
+
[/<(iframe|frame|object|embed|form)\b/i, "has a frame, object, embed or form tag"],
|
|
949
|
+
[/\ssrcdoc=/i, "sets srcdoc"],
|
|
950
|
+
[/javascript\s*:/i, "has a javascript: URL"],
|
|
951
|
+
[/\s(href|xlink:href|src|srcset|action|formaction)="\s*(https?:|[\/\\]{2}|data:\s*text\/html)/i, "links outside"],
|
|
952
|
+
[/url\(\s*("|'|["'])?\s*(https?:|\/\/)/i, "loads a style from outside"]
|
|
953
|
+
];
|
|
954
|
+
var allowed = (name, list2) => list2.some((pkg) => name === pkg || name.startsWith(`${pkg}/`));
|
|
955
|
+
function names(specifier, from, files) {
|
|
956
|
+
const path = posix3.join(posix3.dirname(from), specifier);
|
|
957
|
+
const base2 = path.replace(CODE, "");
|
|
958
|
+
return files.some((file) => file === path || CODE.test(file) && [base2, `${base2}/index`].includes(file.replace(CODE, "")));
|
|
959
|
+
}
|
|
960
|
+
function lineOf(text, found) {
|
|
961
|
+
const lines = text.split(`
|
|
962
|
+
`);
|
|
963
|
+
for (const word of [found, ...(found.match(/[@A-Za-z_$][\w$]*|\/\/|<\w+/g) ?? []).sort((a, b) => b.length - a.length)]) {
|
|
964
|
+
const at = lines.findIndex((line) => line.includes(word));
|
|
965
|
+
if (at !== -1)
|
|
966
|
+
return `:${at + 1}`;
|
|
967
|
+
}
|
|
968
|
+
return "";
|
|
969
|
+
}
|
|
970
|
+
function safety(files, item) {
|
|
971
|
+
const problems = (item.dependencies ?? []).filter((name) => !ALLOWED_DEPENDENCIES.includes(name)).map((name) => `item.json depends on ${name}, which is not ${ALLOWED_DEPENDENCIES.join(" or ")}`);
|
|
972
|
+
const all = files.map((file) => file.path);
|
|
973
|
+
for (const file of files) {
|
|
974
|
+
if ("error" in file) {
|
|
975
|
+
problems.push(`${file.path} does not parse: ${file.error}`);
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
const shipped = item.files.includes(file.path);
|
|
979
|
+
const test = !shipped && file.path.startsWith("test/");
|
|
980
|
+
const reachable = shipped ? item.files : test ? all : all.filter((path) => !path.startsWith("test/"));
|
|
981
|
+
const packages = shipped ? ALLOWED_IMPORTS : [...ALLOWED_IMPORTS, ...ALLOWED_TEST_IMPORTS];
|
|
982
|
+
for (const entry of file.imports) {
|
|
983
|
+
if (entry.kind !== "import-statement")
|
|
984
|
+
continue;
|
|
985
|
+
const needed = ALLOWED_DEPENDENCIES.find((pkg) => entry.path === pkg || entry.path.startsWith(`${pkg}/`));
|
|
986
|
+
if (shipped && needed && !(item.dependencies ?? []).includes(needed))
|
|
987
|
+
problems.push(`${file.path}${lineOf(file.text, JSON.stringify(entry.path))} imports ${entry.path}, but item.json's dependencies do not list ${needed}`);
|
|
988
|
+
const fine = entry.path.startsWith(".") ? names(entry.path, file.path, reachable) || test && TEST_HELPERS.some((helper) => helper.test(entry.path)) : allowed(entry.path, packages);
|
|
989
|
+
const where = shipped ? "one of its files" : test ? "a file in its folder" : "a file in its folder outside test/";
|
|
990
|
+
if (!fine)
|
|
991
|
+
problems.push(`${file.path}${lineOf(file.text, JSON.stringify(entry.path))} imports ${entry.path}, which is not ${where} or ${packages.join(", ")}`);
|
|
992
|
+
}
|
|
993
|
+
const pragma = /@jsx\w*/.exec(file.text);
|
|
994
|
+
if (pragma)
|
|
995
|
+
problems.push(`${file.path}${lineOf(file.text, pragma[0])} sets the JSX runtime in a comment: ${pragma[0]}`);
|
|
996
|
+
for (const [pattern, what] of UNSAFE) {
|
|
997
|
+
const found = pattern.exec(file.code);
|
|
998
|
+
if (found)
|
|
999
|
+
problems.push(`${file.path}${lineOf(file.text, found[0])} ${what}: ${found[0].trim()}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return problems;
|
|
1003
|
+
}
|
|
1004
|
+
function markupSafety(html) {
|
|
1005
|
+
return UNSAFE_MARKUP.flatMap(([pattern, what]) => {
|
|
1006
|
+
const found = pattern.exec(html);
|
|
1007
|
+
return found ? [`the markup ${what}: ${found[0].trim()}`] : [];
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
function configFiles(dir, under = "") {
|
|
1011
|
+
return entries(dir, under).flatMap((entry) => {
|
|
1012
|
+
const path = under ? `${under}/${entry.name}` : entry.name;
|
|
1013
|
+
if (entry.name === "node_modules")
|
|
1014
|
+
return [];
|
|
1015
|
+
if (entry.name === "bunfig.toml" || entry.isSymbolicLink() || entry.name.startsWith(".") && entry.name !== ".gitignore")
|
|
1016
|
+
return [path];
|
|
1017
|
+
return entry.isDirectory() ? configFiles(dir, path) : [];
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1021
|
+
var TSCONFIG = /^[tj]sconfig.*\.json$/;
|
|
1022
|
+
var TSCONFIG_KEYS = ["extends", "compilerOptions", "include", "exclude"];
|
|
1023
|
+
var TSCONFIG_OPTIONS = Object.keys(itemTsconfig(false).compilerOptions);
|
|
1024
|
+
function tsconfigProblems(dir) {
|
|
1025
|
+
return walk(dir).filter((path) => TSCONFIG.test(basename(path))).flatMap((file) => {
|
|
1026
|
+
let config;
|
|
1027
|
+
try {
|
|
1028
|
+
config = JSON.parse(readFileSync6(join6(dir, file), "utf8"));
|
|
1029
|
+
} catch {
|
|
1030
|
+
return [`${file} is not plain JSON, so check cannot tell what it sets`];
|
|
1031
|
+
}
|
|
1032
|
+
if (typeof config !== "object" || config === null || Array.isArray(config))
|
|
1033
|
+
return [`${file} is not an object`];
|
|
1034
|
+
const problems = [];
|
|
1035
|
+
if ("extends" in config && (file !== "tsconfig.json" || config.extends !== BASE_TSCONFIG))
|
|
1036
|
+
problems.push(`${file} extends ${JSON.stringify(config.extends)}, which is not ${BASE_TSCONFIG} from the folder's tsconfig.json`);
|
|
1037
|
+
for (const key of Object.keys(config))
|
|
1038
|
+
if (!TSCONFIG_KEYS.includes(key))
|
|
1039
|
+
problems.push(`${file} has ${key}, which items may not have`);
|
|
1040
|
+
const options = "compilerOptions" in config && isRecord(config.compilerOptions) ? config.compilerOptions : {};
|
|
1041
|
+
for (const [option, value] of Object.entries(options)) {
|
|
1042
|
+
if (["paths", "baseUrl", "rootDirs"].includes(option))
|
|
1043
|
+
problems.push(`${file} sets compilerOptions.${option}, which moves imports`);
|
|
1044
|
+
else if (!TSCONFIG_OPTIONS.includes(option))
|
|
1045
|
+
problems.push(`${file} sets compilerOptions.${option}, which items may not set`);
|
|
1046
|
+
else if (option === "jsx" && value !== "react-jsx")
|
|
1047
|
+
problems.push(`${file} sets compilerOptions.jsx to ${JSON.stringify(value)}, not "react-jsx"`);
|
|
1048
|
+
}
|
|
1049
|
+
return problems;
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function canonical(value) {
|
|
1053
|
+
if (Array.isArray(value))
|
|
1054
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
1055
|
+
if (isRecord(value))
|
|
1056
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
1057
|
+
return JSON.stringify(value) ?? "undefined";
|
|
1058
|
+
}
|
|
1059
|
+
var PLAIN_VERSION = /^\^?\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$/;
|
|
1060
|
+
function versionsFit(value, inRepo, outside) {
|
|
1061
|
+
if (canonical(value) === canonical(inRepo))
|
|
1062
|
+
return true;
|
|
1063
|
+
if (!isRecord(value) || !isRecord(outside))
|
|
1064
|
+
return false;
|
|
1065
|
+
const keys = Object.keys(value).sort();
|
|
1066
|
+
if (canonical(keys) !== canonical(Object.keys(outside).sort()))
|
|
1067
|
+
return false;
|
|
1068
|
+
return keys.every((key) => {
|
|
1069
|
+
const version2 = value[key];
|
|
1070
|
+
const want = outside[key];
|
|
1071
|
+
return version2 === want || typeof version2 === "string" && typeof want === "string" && PLAIN_VERSION.test(version2) && PLAIN_VERSION.test(want);
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
function packageProblems(dir, item) {
|
|
1075
|
+
const path = join6(dir, "package.json");
|
|
1076
|
+
if (!existsSync6(path))
|
|
1077
|
+
return ["there is no package.json; vectoys create writes one"];
|
|
1078
|
+
let pkg;
|
|
1079
|
+
try {
|
|
1080
|
+
pkg = JSON.parse(readFileSync6(path, "utf8"));
|
|
1081
|
+
} catch {
|
|
1082
|
+
return ["package.json is not valid JSON"];
|
|
1083
|
+
}
|
|
1084
|
+
if (!isRecord(pkg))
|
|
1085
|
+
return ["package.json is not an object"];
|
|
1086
|
+
const description = typeof pkg.description === "string" ? pkg.description : "";
|
|
1087
|
+
const inRepo = { ...itemPackage(item.name, description, true) };
|
|
1088
|
+
const outside = { ...itemPackage(item.name, description, false) };
|
|
1089
|
+
return [...new Set([...Object.keys(inRepo), ...Object.keys(pkg)])].flatMap((key) => {
|
|
1090
|
+
if (!(key in inRepo))
|
|
1091
|
+
return [`package.json has ${key}, which items may not have`];
|
|
1092
|
+
const fits = key === "dependencies" || key === "devDependencies" ? versionsFit(pkg[key], inRepo[key], outside[key]) : canonical(pkg[key]) === canonical(inRepo[key]);
|
|
1093
|
+
return fits ? [] : [`package.json's ${key} is not what vectoys create writes`];
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
812
1096
|
function readItem(dir) {
|
|
813
1097
|
const path = join6(dir, "item.json");
|
|
814
1098
|
if (!existsSync6(path))
|
|
@@ -836,22 +1120,39 @@ function checks(dir) {
|
|
|
836
1120
|
if (typeof item === "string")
|
|
837
1121
|
return [{ pass: false, line: item }];
|
|
838
1122
|
const results = [{ pass: true, line: "item.json has what the registry needs" }];
|
|
1123
|
+
const config = [...configFiles(dir).map((file) => `the folder has ${file}, which items may not have`), ...tsconfigProblems(dir), ...packageProblems(dir, item)];
|
|
1124
|
+
if (config.length)
|
|
1125
|
+
return [...results, ...config.map((line) => ({ pass: false, line }))];
|
|
839
1126
|
const size = item.files.reduce((sum, file) => sum + lstatSync(resolve4(dir, file)).size, 0);
|
|
840
1127
|
const kb = (bytes) => `${(bytes / 1024).toFixed(1)} KB`;
|
|
841
1128
|
results.push({ pass: size < MAX_SOURCE, line: `the files are ${kb(size)}, ${size < MAX_SOURCE ? "under" : "over"} ${kb(MAX_SOURCE)}` });
|
|
842
1129
|
const named = brands([item.name, item.title, item.description].join(" "));
|
|
843
1130
|
results.push({ pass: named.length === 0, line: named.length ? `item.json names brands: ${named.join(", ")}` : "item.json names no brands" });
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1131
|
+
const read = sources(dir, [...new Set([...item.files, ...codeFiles(dir)])]);
|
|
1132
|
+
const unsafe = typeof read === "string" ? [`bun could not read the files: ${read}`] : safety(read, item);
|
|
1133
|
+
if (unsafe.length)
|
|
1134
|
+
return [...results, ...unsafe.map((line) => ({ pass: false, line }))];
|
|
1135
|
+
results.push({ pass: true, line: "the code uses no scripts, eval, network calls or outside imports that check can see" });
|
|
1136
|
+
const component = `src/${item.name}.tsx`;
|
|
1137
|
+
const wanted = componentName(item.name);
|
|
1138
|
+
const shape = { pass: false, line: `the component must be ${component} exporting ${wanted}` };
|
|
1139
|
+
if (!item.files.includes(component))
|
|
1140
|
+
return [...results, shape];
|
|
1141
|
+
const rendered = render(resolve4(dir, component), dir, wanted);
|
|
848
1142
|
if ("error" in rendered)
|
|
849
1143
|
return [...results, { pass: false, line: `${component} does not render on the server: ${rendered.error}` }];
|
|
1144
|
+
if (rendered.name !== wanted)
|
|
1145
|
+
return [...results, shape];
|
|
850
1146
|
const [html, again] = rendered.html;
|
|
851
1147
|
results.push({ pass: true, line: `${rendered.name} renders on the server` });
|
|
852
1148
|
const broken = ["NaN", "Infinity"].filter((word) => html.includes(word));
|
|
853
1149
|
results.push({ pass: broken.length === 0, line: broken.length ? `the markup has ${broken.join(" and ")} in it` : "the markup has no NaN or Infinity" });
|
|
854
1150
|
results.push({ pass: html === again, line: html === again ? "two renders give the same markup" : "two renders give different markup" });
|
|
1151
|
+
const outside = markupSafety(html);
|
|
1152
|
+
if (outside.length)
|
|
1153
|
+
results.push(...outside.map((line) => ({ pass: false, line })));
|
|
1154
|
+
else
|
|
1155
|
+
results.push({ pass: true, line: "the markup has no scripts or outside links" });
|
|
855
1156
|
const shown = brands(textOf(html));
|
|
856
1157
|
results.push({ pass: shown.length === 0, line: shown.length ? `the rendered text names brands: ${shown.join(", ")}` : "the rendered text names no brands" });
|
|
857
1158
|
const name = accessibleName(html);
|
|
@@ -1010,7 +1311,7 @@ function openPullRequest(change, runner, cwd) {
|
|
|
1010
1311
|
const clone = join8(work, "vectoys");
|
|
1011
1312
|
must(runner, "gh", ["repo", "clone", `${login}/vectoys`, clone, "--", "--depth", "1"], work);
|
|
1012
1313
|
must(runner, "git", ["checkout", "-b", change.branch], clone);
|
|
1013
|
-
change.write(clone);
|
|
1314
|
+
change.write(clone, runner);
|
|
1014
1315
|
must(runner, "git", ["add", "--all"], clone);
|
|
1015
1316
|
must(runner, "git", ["commit", "-m", change.title], clone);
|
|
1016
1317
|
const ref = `refs/heads/${change.branch}`;
|
|
@@ -1035,20 +1336,38 @@ function steps(change, own) {
|
|
|
1035
1336
|
`Open a pull request to ${UPSTREAM} main, or update the one already open from the branch`
|
|
1036
1337
|
];
|
|
1037
1338
|
}
|
|
1339
|
+
var CI_BUN = "1.3.14";
|
|
1340
|
+
var MIN_BUN = "1.3.2";
|
|
1341
|
+
var OLD_BUN = `Update bun to ${CI_BUN}, the version CI uses, and submit again.`;
|
|
1342
|
+
function atLeast(have, want) {
|
|
1343
|
+
const parts = (version2) => (/^(\d+)\.(\d+)\.(\d+)/.exec(version2) ?? []).slice(1).map(Number);
|
|
1344
|
+
const [a, b] = [parts(have), parts(want)];
|
|
1345
|
+
if (a.length !== 3)
|
|
1346
|
+
return false;
|
|
1347
|
+
for (let at = 0;at < 3; at++)
|
|
1348
|
+
if ((a[at] ?? 0) !== (b[at] ?? 0))
|
|
1349
|
+
return (a[at] ?? 0) > (b[at] ?? 0);
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
function needBun(runner, cwd) {
|
|
1353
|
+
const version2 = must(runner, "bun", ["--version"], cwd);
|
|
1354
|
+
if (!atLeast(version2, MIN_BUN))
|
|
1355
|
+
throw new Error(`bun ${version2} is older than ${MIN_BUN} and rewrites bun.lock beyond the item's lines. ${OLD_BUN}`);
|
|
1356
|
+
}
|
|
1038
1357
|
function itemFiles(dir, listed) {
|
|
1039
|
-
const extra = ["package.json", "tsconfig.json", "test"].filter((path) => existsSync8(join8(dir, path)));
|
|
1358
|
+
const extra = ["package.json", "tsconfig.json", "src/index.ts", "test"].filter((path) => existsSync8(join8(dir, path)));
|
|
1040
1359
|
return [...new Set(["item.json", ...listed, ...extra])];
|
|
1041
1360
|
}
|
|
1042
|
-
var
|
|
1361
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1043
1362
|
function inRepoForm(folder) {
|
|
1044
1363
|
const pkgPath = join8(folder, "package.json");
|
|
1045
1364
|
if (existsSync8(pkgPath)) {
|
|
1046
1365
|
const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
|
|
1047
|
-
if (!
|
|
1366
|
+
if (!isRecord2(pkg))
|
|
1048
1367
|
throw new Error("The item's package.json is not an object.");
|
|
1049
1368
|
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
1050
1369
|
const deps = pkg[field];
|
|
1051
|
-
if (!
|
|
1370
|
+
if (!isRecord2(deps))
|
|
1052
1371
|
continue;
|
|
1053
1372
|
for (const name of Object.keys(deps))
|
|
1054
1373
|
if (name.startsWith("@vectoys/") || name === "@vinena/vectoys" || name.startsWith("@vinena/vectoys-"))
|
|
@@ -1062,9 +1381,9 @@ function inRepoForm(folder) {
|
|
|
1062
1381
|
const tsPath = join8(folder, "tsconfig.json");
|
|
1063
1382
|
if (existsSync8(tsPath)) {
|
|
1064
1383
|
const tsconfig = JSON.parse(readFileSync8(tsPath, "utf8"));
|
|
1065
|
-
if (!
|
|
1384
|
+
if (!isRecord2(tsconfig))
|
|
1066
1385
|
throw new Error("The item's tsconfig.json is not an object.");
|
|
1067
|
-
const own =
|
|
1386
|
+
const own = isRecord2(tsconfig.compilerOptions) ? tsconfig.compilerOptions : {};
|
|
1068
1387
|
const kept = Object.fromEntries(Object.entries(own).filter(([key]) => !(key in compilerOptions2)));
|
|
1069
1388
|
writeFileSync6(tsPath, `${JSON.stringify({ extends: BASE_TSCONFIG, ...tsconfig, compilerOptions: kept }, null, 2)}
|
|
1070
1389
|
`);
|
|
@@ -1102,14 +1421,19 @@ function itemChange(context, dir) {
|
|
|
1102
1421
|
branch,
|
|
1103
1422
|
title,
|
|
1104
1423
|
body,
|
|
1105
|
-
steps: steps({ branch, title }, [`Copy ${files.join(", ")} into registry/${item.name}
|
|
1106
|
-
write: (clone) => {
|
|
1424
|
+
steps: steps({ branch, title }, [`Copy ${files.join(", ")} into registry/${item.name}/`, "Add the item's package to bun.lock with bun install --lockfile-only"]),
|
|
1425
|
+
write: (clone, runner) => {
|
|
1107
1426
|
for (const file of files) {
|
|
1108
1427
|
const to = join8(clone, "registry", item.name, file);
|
|
1109
1428
|
mkdirSync3(dirname5(to), { recursive: true });
|
|
1110
1429
|
cpSync(join8(dir, file), to, { recursive: true });
|
|
1111
1430
|
}
|
|
1112
1431
|
inRepoForm(join8(clone, "registry", item.name));
|
|
1432
|
+
must(runner, "bun", ["install", "--lockfile-only", "--ignore-scripts"], clone);
|
|
1433
|
+
const deleted = must(runner, "git", ["diff", "--numstat", "bun.lock"], clone).split(`
|
|
1434
|
+
`).some((line) => Number(line.split("\t")[1]) > 0);
|
|
1435
|
+
if (deleted)
|
|
1436
|
+
throw new Error(`bun ${must(runner, "bun", ["--version"], clone)} rewrote bun.lock beyond the item's lines. ${OLD_BUN}`);
|
|
1113
1437
|
}
|
|
1114
1438
|
};
|
|
1115
1439
|
}
|
|
@@ -1139,22 +1463,22 @@ function isListings(value) {
|
|
|
1139
1463
|
return Array.isArray(value) && value.every((entry) => typeof entry === "object" && entry !== null && ("name" in entry) && typeof entry.name === "string");
|
|
1140
1464
|
}
|
|
1141
1465
|
async function listingChange(context, given, runner) {
|
|
1142
|
-
const { index, base } = registryUrls(given);
|
|
1466
|
+
const { index, base: base2 } = registryUrls(given);
|
|
1143
1467
|
context.log(`Reading ${index}`);
|
|
1144
1468
|
const read = readIndex(await fetchJson(index));
|
|
1145
1469
|
if (typeof read === "string")
|
|
1146
1470
|
throw new Error(`${index} is not a vectoys registry index: ${read}.`);
|
|
1147
1471
|
const { items } = read;
|
|
1148
|
-
const name = read.name ?? new URL(
|
|
1472
|
+
const name = read.name ?? new URL(base2).hostname;
|
|
1149
1473
|
context.log(` pass it lists ${items} item${items === 1 ? "" : "s"}`);
|
|
1150
1474
|
const author = runner("git", ["config", "user.name"], context.cwd).stdout.trim();
|
|
1151
1475
|
if (!author)
|
|
1152
1476
|
throw new Error('Set your name for git first, as git config --global user.name "Your Name".');
|
|
1153
|
-
const listing = { name, url:
|
|
1477
|
+
const listing = { name, url: base2, author };
|
|
1154
1478
|
const branch = `registry/${name}`;
|
|
1155
1479
|
const title = `List the ${name} registry`;
|
|
1156
1480
|
const body = [
|
|
1157
|
-
`Lists the ${name} registry, at ${
|
|
1481
|
+
`Lists the ${name} registry, at ${base2}, by ${author}. Its index lists ${items} item${items === 1 ? "" : "s"}.`,
|
|
1158
1482
|
"",
|
|
1159
1483
|
"- [ ] I run this registry, and its items draw no maker's product."
|
|
1160
1484
|
].join(`
|
|
@@ -1191,6 +1515,8 @@ async function submit(context, given, options) {
|
|
|
1191
1515
|
context.log(CLOSED);
|
|
1192
1516
|
return;
|
|
1193
1517
|
}
|
|
1518
|
+
if (!options.registry)
|
|
1519
|
+
needBun(runner, context.cwd);
|
|
1194
1520
|
const { url, opened } = openPullRequest(change, runner, context.cwd);
|
|
1195
1521
|
context.log(`${opened ? "Opened" : "Updated"} ${url}`);
|
|
1196
1522
|
return url;
|