diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca49cc56d7..7feb6a33ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,12 +25,15 @@ jobs: run-tests: runs-on: ubuntu-latest + strategy: + matrix: + node: [20, 22] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: ${{ matrix.node }} cache: yarn cache-dependency-path: yarn.lock @@ -40,6 +43,12 @@ jobs: - name: Run tests run: yarn test + - name: Check publishable tarballs + run: yarn tsx bin/check-tarballs.ts + + - name: Smoke-test installed tarballs + run: yarn tsx bin/smoke-tarballs.ts + build-web: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..f9e0fa8d73 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,70 @@ +name: Publish tagged packages +on: + push: + branches: [main] + workflow_dispatch: + inputs: + dry-run: + description: "Run npm publish with --dry-run" + type: boolean + default: true + +permissions: + contents: read + id-token: write + +# lerna.json sets no commit message; adding "[skip ci]" there would +# silently stop this workflow from publishing. + +jobs: + check: + runs-on: ubuntu-latest + outputs: + tagged: ${{ steps.tags.outputs.tagged }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: tags + run: | + if git tag --points-at HEAD | grep -q '^@ethdebug/'; then + echo tagged=true >> "$GITHUB_OUTPUT" + else + echo tagged=false >> "$GITHUB_OUTPUT" + fi + + publish: + needs: check + if: >- + needs.check.outputs.tagged == 'true' || + github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 20 + concurrency: + group: publish + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: yarn + cache-dependency-path: yarn.lock + + - name: Use npm 11 (trusted publishing) + run: npm install -g npm@11 + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run tests + run: yarn test + + - name: Publish + run: >- + yarn tsx bin/publish-tagged.ts + ${{ (github.event_name == 'workflow_dispatch' && inputs.dry-run) && '--dry-run' || '' }} diff --git a/.gitignore b/.gitignore index 8c45a19663..04a08a5cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ *.tsbuildinfo coverage/ .worktrees/ +.nx/ diff --git a/bin/check-tarballs.ts b/bin/check-tarballs.ts new file mode 100644 index 0000000000..053ad1f1d8 --- /dev/null +++ b/bin/check-tarballs.ts @@ -0,0 +1,23 @@ +import { fileURLToPath } from "node:url"; +import { checkPackList, packList } from "./packlist.js"; +import { readWorkspaces } from "./publish-tagged.js"; + +const root = fileURLToPath(new URL("..", import.meta.url)); + +let failed = false; +for (const workspace of readWorkspaces(root)) { + if (workspace.private) { + continue; + } + const bad = checkPackList(packList(workspace.dir)); + if (bad.length > 0) { + failed = true; + console.error(`${workspace.name}: disallowed files in tarball:`); + for (const path of bad) { + console.error(` ${path}`); + } + } else { + console.log(`${workspace.name}: ok`); + } +} +process.exit(failed ? 1 : 0); diff --git a/bin/packlist.test.ts b/bin/packlist.test.ts new file mode 100644 index 0000000000..beeeab1452 --- /dev/null +++ b/bin/packlist.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { checkPackList, parsePackOutput } from "./packlist.js"; + +describe("checkPackList", () => { + it("accepts the allowed shape", () => { + expect( + checkPackList([ + "package.json", + "README.md", + "LICENSE", + "dist/src/index.js", + "dist/src/a/b.d.ts", + "dist/bin/bugc.js", + ]), + ).toEqual([]); + }); + + it("rejects src, tests, buildinfo, and dist/test", () => { + expect( + checkPackList([ + "src/index.ts", + "dist/src/x.test.js", + "dist/tsconfig.build.tsbuildinfo", + "dist/test/helper.js", + "dist/vitest.config.js", + ]), + ).toEqual([ + "src/index.ts", + "dist/src/x.test.js", + "dist/tsconfig.build.tsbuildinfo", + "dist/test/helper.js", + "dist/vitest.config.js", + ]); + }); +}); + +describe("parsePackOutput", () => { + it("takes the last JSON array after script noise", () => { + const out = [ + "yarn run v1.22.22", + "$ node ./bin/generate-schema-yamls.js", + "Done in 0.46s.", + "[", + ' { "files": [ { "path": "dist/src/index.js" } ] }', + "]", + ].join("\n"); + expect(parsePackOutput(out)).toEqual(["dist/src/index.js"]); + }); +}); diff --git a/bin/packlist.ts b/bin/packlist.ts new file mode 100644 index 0000000000..74fdc31ba5 --- /dev/null +++ b/bin/packlist.ts @@ -0,0 +1,40 @@ +import { execFileSync } from "node:child_process"; + +const allowed = [ + /^package\.json$/, + /^README[^/]*$/, + /^LICENSE[^/]*$/, + /^dist\/src\//, + /^dist\/bin\//, +]; + +const rejected = [/\.test\./, /\.tsbuildinfo$/]; + +export function checkPackList(files: string[]): string[] { + return files.filter( + (path) => + !allowed.some((re) => re.test(path)) || + rejected.some((re) => re.test(path)), + ); +} + +export function parsePackOutput(stdout: string): string[] { + const lines = stdout.split("\n"); + const start = lines.lastIndexOf("["); + if (start < 0) { + throw new Error("npm pack --json: no JSON array in output"); + } + const parsed = JSON.parse(lines.slice(start).join("\n")) as { + files: { path: string }[]; + }[]; + return parsed.flatMap((entry) => entry.files.map((file) => file.path)); +} + +export function packList(packageDir: string): string[] { + const stdout = execFileSync("npm", ["pack", "--dry-run", "--json"], { + cwd: packageDir, + encoding: "utf8", + stdio: ["ignore", "pipe", "inherit"], + }); + return parsePackOutput(stdout); +} diff --git a/bin/publish-tagged.test.ts b/bin/publish-tagged.test.ts new file mode 100644 index 0000000000..80d1b887b7 --- /dev/null +++ b/bin/publish-tagged.test.ts @@ -0,0 +1,234 @@ +import type { SpawnSyncReturns } from "node:child_process"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + classifyView, + parseTags, + publishArgs, + readWorkspaces, + selectPackages, + topoSort, + viewVersions, + type Workspace, +} from "./publish-tagged.js"; + +vi.mock("node:child_process", () => ({ + execFileSync: vi.fn(), + spawnSync: vi.fn(), +})); + +const ws = ( + name: string, + version: string, + deps: string[] = [], + isPrivate = false, +): Workspace => ({ + name, + version, + dir: `/repo/packages/${name.replace("@ethdebug/", "")}`, + private: isPrivate, + dependencies: deps, +}); + +describe("parseTags", () => { + it("keeps only @ethdebug package tags", () => { + expect( + parseTags(["@ethdebug/format@0.1.0-1", "v1", "@other/x@1.0.0", ""]), + ).toEqual([{ name: "@ethdebug/format", version: "0.1.0-1" }]); + }); +}); + +describe("selectPackages", () => { + const workspaces = [ + ws("@ethdebug/format", "0.1.0-1"), + ws("@ethdebug/format-web", "0.1.0-1", [], true), + ]; + + it("skips private packages", () => { + expect( + selectPackages( + [ + { name: "@ethdebug/format", version: "0.1.0-1" }, + { name: "@ethdebug/format-web", version: "0.1.0-1" }, + ], + workspaces, + ).map((w) => w.name), + ).toEqual(["@ethdebug/format"]); + }); + + it("errors on a tag/manifest version mismatch", () => { + expect(() => + selectPackages( + [{ name: "@ethdebug/format", version: "0.1.0-2" }], + workspaces, + ), + ).toThrow(/0\.1\.0-2.*0\.1\.0-1/); + }); + + it("warns and ignores a tag with no matching workspace", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const result = selectPackages( + [{ name: "@ethdebug/nope", version: "1.0.0" }], + workspaces, + ); + expect(result).toEqual([]); + expect(warn).toHaveBeenCalledWith( + "@ethdebug/nope: no such workspace, ignoring", + ); + warn.mockRestore(); + }); +}); + +describe("topoSort", () => { + it("orders dependencies before dependents, including peers", () => { + const sorted = topoSort([ + ws("@ethdebug/evm", "1", ["@ethdebug/pointers"]), + ws("@ethdebug/pointers", "1", ["@ethdebug/format"]), + ws("@ethdebug/format", "1"), + ]).map((w) => w.name); + expect(sorted).toEqual([ + "@ethdebug/format", + "@ethdebug/pointers", + "@ethdebug/evm", + ]); + }); +}); + +describe("classifyView", () => { + it("treats E404 as unpublished", () => { + expect( + classifyView(1, '{"error":{"code":"E404","summary":"x"}}', "0.1.0-1"), + ).toBe("unpublished"); + }); + it("treats a version present in the array as published", () => { + expect(classifyView(0, '["0.1.0-0","0.1.0-1"]', "0.1.0-1")).toBe( + "published", + ); + }); + it("treats a version absent from the array as unpublished", () => { + expect(classifyView(0, '["0.1.0-0"]', "0.1.0-1")).toBe("unpublished"); + }); + it("aborts on any other error", () => { + expect(() => + classifyView(1, '{"error":{"code":"ETIMEDOUT"}}', "0.1.0-1"), + ).toThrow(/ETIMEDOUT/); + }); + it("aborts on a non-array success", () => { + expect(() => classifyView(0, '"0.1.0-1"', "0.1.0-1")).toThrow(/unexpected/); + }); +}); + +describe("viewVersions", () => { + it("prefixes the package name on a probe failure", () => { + const result: SpawnSyncReturns = { + pid: 1, + output: [null, "", ""], + stdout: '{"error":{"code":"ETIMEDOUT"}}', + stderr: "", + status: 1, + signal: null, + }; + vi.mocked(spawnSync).mockReturnValue(result); + expect(() => viewVersions("@ethdebug/format", "0.1.0-1")).toThrow( + /@ethdebug\/format/, + ); + }); +}); + +describe("readWorkspaces", () => { + let root: string | undefined; + + afterEach(() => { + if (root) { + rmSync(root, { recursive: true, force: true }); + root = undefined; + } + }); + + it("merges dependencies and peerDependencies, filtered", () => { + root = mkdtempSync(join(tmpdir(), "ws-")); + const packagesDir = join(root, "packages"); + mkdirSync(join(packagesDir, "a"), { recursive: true }); + mkdirSync(join(packagesDir, "b"), { recursive: true }); + mkdirSync(join(packagesDir, "c"), { recursive: true }); + writeFileSync( + join(packagesDir, "a", "package.json"), + JSON.stringify({ name: "@ethdebug/a", version: "1.0.0" }), + ); + writeFileSync( + join(packagesDir, "b", "package.json"), + JSON.stringify({ + name: "@ethdebug/b", + version: "1.0.0", + dependencies: { "@ethdebug/a": "^1.0.0", lodash: "^4" }, + peerDependencies: { "@ethdebug/c": "^1.0.0" }, + }), + ); + writeFileSync( + join(packagesDir, "c", "package.json"), + JSON.stringify({ + name: "@ethdebug/c", + version: "1.0.0", + private: true, + }), + ); + writeFileSync(join(packagesDir, ".DS_Store"), ""); + + const workspaces = readWorkspaces(root); + expect(workspaces).toHaveLength(3); + expect(workspaces.some((w) => w.dir.endsWith(".DS_Store"))).toBe(false); + + const byName = new Map(workspaces.map((w) => [w.name, w])); + expect(byName.get("@ethdebug/a")?.dependencies).toEqual([]); + expect(byName.get("@ethdebug/b")?.dependencies).toEqual([ + "@ethdebug/a", + "@ethdebug/c", + ]); + expect(byName.get("@ethdebug/c")?.private).toBe(true); + for (const name of ["a", "b", "c"]) { + const dir = byName.get(`@ethdebug/${name}`)?.dir ?? ""; + expect(dir.endsWith(join("packages", name))).toBe(true); + } + }); +}); + +describe("publishArgs", () => { + it("tags a publish as latest", () => { + expect(publishArgs(false, {})).toEqual([ + "publish", + "--access", + "public", + "--tag", + "latest", + ]); + }); + + it("appends --dry-run when requested", () => { + expect(publishArgs(true, {})).toEqual([ + "publish", + "--access", + "public", + "--tag", + "latest", + "--dry-run", + ]); + }); + + it("appends --provenance under GitHub Actions", () => { + expect(publishArgs(false, { GITHUB_ACTIONS: "true" })).toEqual([ + "publish", + "--access", + "public", + "--tag", + "latest", + "--provenance", + ]); + }); + + it("omits --provenance outside GitHub Actions", () => { + expect(publishArgs(false, {})).not.toContain("--provenance"); + }); +}); diff --git a/bin/publish-tagged.ts b/bin/publish-tagged.ts new file mode 100644 index 0000000000..eb927a818f --- /dev/null +++ b/bin/publish-tagged.ts @@ -0,0 +1,223 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { checkPackList, packList } from "./packlist.js"; + +export interface Workspace { + name: string; + version: string; + dir: string; + private: boolean; + dependencies: string[]; +} + +export interface Tag { + name: string; + version: string; +} + +export function parseTags(lines: string[]): Tag[] { + const tags: Tag[] = []; + for (const line of lines) { + const match = /^(@ethdebug\/[^@]+)@(.+)$/.exec(line.trim()); + if (match) { + tags.push({ name: match[1], version: match[2] }); + } + } + return tags; +} + +export function readWorkspaces(root: string): Workspace[] { + const packagesDir = join(root, "packages"); + return readdirSync(packagesDir) + .filter((entry) => existsSync(join(packagesDir, entry, "package.json"))) + .map((entry) => { + const dir = join(packagesDir, entry); + const manifest = JSON.parse( + readFileSync(join(dir, "package.json"), "utf8"), + ) as { + name: string; + version: string; + private?: boolean; + dependencies?: Record; + peerDependencies?: Record; + }; + const dependencies = Object.keys({ + ...manifest.dependencies, + ...manifest.peerDependencies, + }).filter((dep) => dep.startsWith("@ethdebug/")); + return { + name: manifest.name, + version: manifest.version, + dir, + private: manifest.private === true, + dependencies, + }; + }); +} + +export function selectPackages( + tags: Tag[], + workspaces: Workspace[], +): Workspace[] { + const selected: Workspace[] = []; + for (const tag of tags) { + const workspace = workspaces.find((w) => w.name === tag.name); + if (!workspace) { + console.warn(`${tag.name}: no such workspace, ignoring`); + continue; + } + if (workspace.private) { + continue; + } + if (workspace.version !== tag.version) { + throw new Error( + `${tag.name}: tag version ${tag.version} does not match ` + + `manifest version ${workspace.version}`, + ); + } + selected.push(workspace); + } + return selected; +} + +export function topoSort(workspaces: Workspace[]): Workspace[] { + const byName = new Map(workspaces.map((w) => [w.name, w])); + const done = new Set(); + const sorted: Workspace[] = []; + const visit = (workspace: Workspace, trail: string[]) => { + if (done.has(workspace.name)) { + return; + } + if (trail.includes(workspace.name)) { + throw new Error(`dependency cycle: ${trail.join(" -> ")}`); + } + for (const dep of workspace.dependencies) { + const target = byName.get(dep); + if (target) { + visit(target, [...trail, workspace.name]); + } + } + done.add(workspace.name); + sorted.push(workspace); + }; + for (const workspace of workspaces) { + visit(workspace, []); + } + return sorted; +} + +export type ViewResult = "published" | "unpublished"; + +export function classifyView( + status: number, + stdout: string, + version: string, +): ViewResult { + let parsed: unknown; + try { + parsed = JSON.parse(stdout); + } catch { + throw new Error(`npm view: unexpected output: ${stdout}`); + } + if (status !== 0) { + const code = (parsed as { error?: { code?: string } }).error?.code; + if (code === "E404") { + return "unpublished"; + } + throw new Error(`npm view failed: ${code ?? stdout}`); + } + if (!Array.isArray(parsed)) { + throw new Error(`npm view: unexpected non-array result: ${stdout}`); + } + return parsed.includes(version) ? "published" : "unpublished"; +} + +export function viewVersions(name: string, version: string): ViewResult { + const result = spawnSync("npm", ["view", name, "versions", "--json"], { + encoding: "utf8", + }); + try { + return classifyView(result.status ?? 1, result.stdout, version); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`${name}: ${message}`); + } +} + +export function publishArgs(dryRun: boolean, env: NodeJS.ProcessEnv): string[] { + const args = ["publish", "--access", "public", "--tag", "latest"]; + if (env.GITHUB_ACTIONS) { + args.push("--provenance"); + } + if (dryRun) { + args.push("--dry-run"); + } + return args; +} + +function publish(workspace: Workspace, dryRun: boolean): void { + const args = publishArgs(dryRun, process.env); + const result = spawnSync("npm", args, { + cwd: workspace.dir, + stdio: "inherit", + }); + if (result.status !== 0) { + throw new Error(`npm publish failed for ${workspace.name}`); + } +} + +export function main(argv: string[]): number { + const dryRun = argv.includes("--dry-run"); + const root = fileURLToPath(new URL("..", import.meta.url)); + const tagLines = execFileSync("git", ["tag", "--points-at", "HEAD"], { + cwd: root, + encoding: "utf8", + }).split("\n"); + const tags = parseTags(tagLines); + if (tags.length === 0) { + console.log("no @ethdebug package tags at HEAD; nothing to publish"); + return 0; + } + const selected = topoSort(selectPackages(tags, readWorkspaces(root))); + const published: string[] = []; + const skipped: string[] = []; + let failed: string | undefined; + try { + for (const workspace of selected) { + const label = `${workspace.name}@${workspace.version}`; + failed = label; + if (viewVersions(workspace.name, workspace.version) === "published") { + console.log(`${label}: already published, skipping`); + skipped.push(label); + failed = undefined; + continue; + } + const bad = checkPackList(packList(workspace.dir)); + if (bad.length > 0) { + throw new Error( + `${label}: disallowed files in tarball:\n ${bad.join("\n ")}`, + ); + } + console.log(`${label}: publishing${dryRun ? " (dry run)" : ""}`); + publish(workspace, dryRun); + published.push(label); + failed = undefined; + } + } finally { + console.log(`published: ${published.join(", ") || "none"}`); + console.log(`skipped: ${skipped.join(", ") || "none"}`); + console.log(`failed: ${failed ?? "none"}`); + } + return 0; +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + try { + process.exit(main(process.argv.slice(2))); + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exit(1); + } +} diff --git a/bin/smoke-tarballs.test.ts b/bin/smoke-tarballs.test.ts new file mode 100644 index 0000000000..41ae3b68b9 --- /dev/null +++ b/bin/smoke-tarballs.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { siblingTarballs } from "./smoke-tarballs.js"; +import type { Workspace } from "./publish-tagged.js"; + +const ws = (dependencies: string[]): Workspace => ({ + name: "@ethdebug/x", + version: "1.0.0", + dir: "/repo/packages/x", + private: false, + dependencies, +}); + +describe("siblingTarballs", () => { + it("returns the tarball for each built @ethdebug dependency", () => { + const built = new Map([ + ["@ethdebug/format", "/tmp/ethdebug-format-1.0.0.tgz"], + ["@ethdebug/pointers", "/tmp/ethdebug-pointers-1.0.0.tgz"], + ]); + expect( + siblingTarballs(ws(["@ethdebug/format", "@ethdebug/pointers"]), built), + ).toEqual([ + "/tmp/ethdebug-format-1.0.0.tgz", + "/tmp/ethdebug-pointers-1.0.0.tgz", + ]); + }); + + it("skips a dependency with no built tarball yet", () => { + const built = new Map([ + ["@ethdebug/format", "/tmp/ethdebug-format-1.0.0.tgz"], + ]); + expect( + siblingTarballs(ws(["@ethdebug/format", "@ethdebug/missing"]), built), + ).toEqual(["/tmp/ethdebug-format-1.0.0.tgz"]); + }); + + it("returns an empty list for a workspace with no dependencies", () => { + expect(siblingTarballs(ws([]), new Map())).toEqual([]); + }); +}); diff --git a/bin/smoke-tarballs.ts b/bin/smoke-tarballs.ts new file mode 100644 index 0000000000..3a8f36f512 --- /dev/null +++ b/bin/smoke-tarballs.ts @@ -0,0 +1,175 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { readWorkspaces, topoSort, type Workspace } from "./publish-tagged.js"; + +export function siblingTarballs( + workspace: Workspace, + built: Map, +): string[] { + return workspace.dependencies + .map((dep) => built.get(dep)) + .filter((path): path is string => path !== undefined); +} + +function lastNonEmptyLine(text: string): string { + const lines = text + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); + const last = lines[lines.length - 1]; + if (!last) { + throw new Error("npm pack: produced no output"); + } + return last; +} + +function pack(workspace: Workspace, destDir: string): string { + const stdout = execFileSync("npm", ["pack", "--pack-destination", destDir], { + cwd: workspace.dir, + encoding: "utf8", + stdio: ["ignore", "pipe", "inherit"], + }); + return join(destDir, lastNonEmptyLine(stdout)); +} + +interface Attempt { + status: number; + stderr: string; +} + +function npmInstall(consumerDir: string, tarballs: string[]): Attempt { + const result = spawnSync( + "npm", + ["install", "--no-audit", "--no-fund", "--loglevel=error", ...tarballs], + { cwd: consumerDir, encoding: "utf8" }, + ); + return { status: result.status ?? 1, stderr: result.stderr ?? "" }; +} + +function importCheck(consumerDir: string, name: string): Attempt { + const result = spawnSync( + "node", + ["--input-type=module", "-e", `await import(${JSON.stringify(name)})`], + { cwd: consumerDir, encoding: "utf8" }, + ); + return { status: result.status ?? 1, stderr: result.stderr ?? "" }; +} + +function bugcHelp(consumerDir: string): Attempt { + const bin = join(consumerDir, "node_modules", ".bin", "bugc"); + const result = spawnSync(bin, ["--help"], { + cwd: consumerDir, + encoding: "utf8", + }); + return { status: result.status ?? 1, stderr: result.stderr ?? "" }; +} + +function consumerName(workspace: Workspace): string { + return workspace.name.replace("@ethdebug/", ""); +} + +// A dependency two or more hops away (e.g. evm -> pointers -> format) +// is not passed as an install target, so npm resolves it against the +// real registry unless told otherwise. @ethdebug/format is already +// published there under an older, incompatible build, so every +// @ethdebug/* package built so far in this run is pinned via +// "overrides", except the workspace itself and its direct +// dependencies: those are already installed as explicit root +// dependencies via the tarball arguments, and npm rejects an +// override that conflicts with a root package's own direct +// dependency. +function overridesFor( + workspace: Workspace, + built: Map, +): Record { + const skip = new Set([workspace.name, ...workspace.dependencies]); + const overrides: Record = {}; + for (const [name, tarball] of built) { + if (!skip.has(name)) { + overrides[name] = `file:${tarball}`; + } + } + return overrides; +} + +function makeConsumerDir( + tmp: string, + workspace: Workspace, + built: Map, +): string { + const dir = join(tmp, `consumer-${consumerName(workspace)}`); + mkdirSync(dir, { recursive: true }); + const overrides = overridesFor(workspace, built); + writeFileSync( + join(dir, "package.json"), + JSON.stringify({ + name: "consumer", + private: true, + type: "module", + ...(Object.keys(overrides).length > 0 ? { overrides } : {}), + }), + ); + return dir; +} + +function report(name: string, failure: Attempt): void { + console.error(`${name}: FAIL`); + console.error(failure.stderr); +} + +export function main(): number { + const root = fileURLToPath(new URL("..", import.meta.url)); + const workspaces = topoSort( + readWorkspaces(root).filter((workspace) => !workspace.private), + ); + + const tmp = mkdtempSync(join(tmpdir(), "ethdebug-smoke-")); + const built = new Map(); + let failed = false; + + try { + for (const workspace of workspaces) { + const tarball = pack(workspace, tmp); + const siblings = siblingTarballs(workspace, built); + built.set(workspace.name, tarball); + + const consumerDir = makeConsumerDir(tmp, workspace, built); + + const install = npmInstall(consumerDir, [tarball, ...siblings]); + if (install.status !== 0) { + failed = true; + report(workspace.name, install); + continue; + } + + const imported = importCheck(consumerDir, workspace.name); + if (imported.status !== 0) { + failed = true; + report(workspace.name, imported); + continue; + } + + if (workspace.name === "@ethdebug/bugc") { + const help = bugcHelp(consumerDir); + if (help.status !== 0) { + failed = true; + report(workspace.name, help); + continue; + } + } + + console.log(`${workspace.name}: ok`); + } + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + + return failed ? 1 : 0; +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exit(main()); +} diff --git a/nx.json b/nx.json new file mode 100644 index 0000000000..3d3c416a1a --- /dev/null +++ b/nx.json @@ -0,0 +1 @@ +{ "$schema": "./node_modules/nx/schemas/nx-schema.json" } diff --git a/package.json b/package.json index 31e535ccd6..1be0987453 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "packages/*" ], "scripts": { - "build": "yarn --cwd packages/format prepare:yamls && yarn --cwd packages/bugc prepare:examples && tsc --build packages/format packages/pointers packages/evm packages/bugc packages/conformance packages/programs-react packages/pointers-react", + "build": "yarn lerna run build --no-private", "bundle": "tsx ./bin/bundle-schema.ts", - "test": "vitest", + "test": "vitest run", "test:coverage": "vitest run --coverage", "start": "./bin/start", "lerna": "lerna", @@ -25,6 +25,7 @@ "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "concurrently": "^8.2.2", + "copyfiles": "^2.4.1", "eslint": "^9.0.0", "eslint-plugin-react-hooks": "^7.0.1", "globals": "^17.0.0", diff --git a/packages/bugc-react/LICENSE b/packages/bugc-react/LICENSE new file mode 100644 index 0000000000..5d84fb65eb --- /dev/null +++ b/packages/bugc-react/LICENSE @@ -0,0 +1,25 @@ +Note: This license applies to all files in this repository EXCEPT those in the +schemas/ directory, which are licensed under CC0 1.0 Universal. See +schemas/LICENSE for details. + +MIT License + +Copyright (c) 2024 ethdebug contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/bugc-react/README.md b/packages/bugc-react/README.md new file mode 100644 index 0000000000..bb8b2f4c15 --- /dev/null +++ b/packages/bugc-react/README.md @@ -0,0 +1,12 @@ +# @ethdebug/bugc-react + +React components for visualizing BUG compiler output. + +Part of [ethdebug/format](https://github.com/ethdebug/format). See +the [documentation](https://ethdebug.github.io/format/). + +## TypeScript + +This package uses `package.json` `imports` for internal modules. +Consumers must use `moduleResolution` `node16`, `nodenext`, or +`bundler`. The legacy `node10` resolution is not supported. diff --git a/packages/bugc-react/package.json b/packages/bugc-react/package.json index 3eb23f4f6f..7fccd580a1 100644 --- a/packages/bugc-react/package.json +++ b/packages/bugc-react/package.json @@ -5,13 +5,25 @@ "type": "module", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/ethdebug/format.git", + "directory": "packages/bugc-react" + }, "license": "MIT", + "files": [ + "dist", + "!dist/**/*.tsbuildinfo" + ], + "engines": { + "node": ">=20" + }, "exports": { ".": { "types": "./dist/src/index.d.ts", "default": "./dist/src/index.js" }, - "./src/components/*.css": "./src/components/*.css" + "./dist/src/components/*.css": "./dist/src/components/*.css" }, "imports": { "#components/*": { @@ -32,8 +44,8 @@ } }, "scripts": { - "prepare": "tsc", - "build": "tsc", + "build": "rm -rf dist && tsc --build tsconfig.build.json && copyfiles -u 1 \"src/**/*.css\" dist/src", + "prepare": "yarn build", "watch": "tsc --watch --preserveWatchOutput", "test": "vitest run" }, diff --git a/packages/bugc-react/tsconfig.build.json b/packages/bugc-react/tsconfig.build.json new file mode 100644 index 0000000000..4e50fa63ee --- /dev/null +++ b/packages/bugc-react/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "declarationMap": false }, + "include": ["src/**/*"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.test.tsx", + "test/**" + ], + "references": [{ "path": "../bugc/tsconfig.build.json" }] +} diff --git a/packages/bugc/LICENSE b/packages/bugc/LICENSE new file mode 100644 index 0000000000..5d84fb65eb --- /dev/null +++ b/packages/bugc/LICENSE @@ -0,0 +1,25 @@ +Note: This license applies to all files in this repository EXCEPT those in the +schemas/ directory, which are licensed under CC0 1.0 Universal. See +schemas/LICENSE for details. + +MIT License + +Copyright (c) 2024 ethdebug contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/bugc/README.md b/packages/bugc/README.md new file mode 100644 index 0000000000..2b7f78443a --- /dev/null +++ b/packages/bugc/README.md @@ -0,0 +1,13 @@ +# @ethdebug/bugc + +The BUG language compiler with ethdebug/format debug information +support. + +Part of [ethdebug/format](https://github.com/ethdebug/format). See +the [documentation](https://ethdebug.github.io/format/). + +## TypeScript + +This package uses `package.json` `imports` for internal modules. +Consumers must use `moduleResolution` `node16`, `nodenext`, or +`bundler`. The legacy `node10` resolution is not supported. diff --git a/packages/bugc/bin/bugc.ts b/packages/bugc/bin/bugc.ts index 4049fde1a9..6806846413 100755 --- a/packages/bugc/bin/bugc.ts +++ b/packages/bugc/bin/bugc.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env tsx +#!/usr/bin/env node /* eslint-disable no-console */ /** diff --git a/packages/bugc/package.json b/packages/bugc/package.json index 932b73d829..d5ecf05619 100644 --- a/packages/bugc/package.json +++ b/packages/bugc/package.json @@ -17,10 +17,13 @@ }, "files": [ "dist", - "bin" + "!dist/**/*.tsbuildinfo" ], + "engines": { + "node": ">=20" + }, "bin": { - "bugc": "./bin/bugc.ts" + "bugc": "./dist/bin/bugc.js" }, "imports": { "#ast": "./dist/src/ast/index.js", @@ -61,11 +64,11 @@ "#types": "./dist/src/types/index.js", "#types/analysis": "./dist/src/types/analysis/index.js", "#types/spec": "./dist/src/types/spec.js", - "#test/*": "./dist/test/*.js" + "#test/*": "./test/*.ts" }, "scripts": { "prepare:examples": "node ./bin/generate-examples.js", - "build": "yarn prepare:examples && tsc", + "build": "yarn prepare:examples && rm -rf dist && tsc --build tsconfig.build.json", "build:watch": "tsc --watch --preserveWatchOutput", "watch": "tsc --watch --preserveWatchOutput", "test": "vitest run", @@ -75,7 +78,7 @@ "typecheck": "tsc --noEmit", "format": "prettier --write \"src/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\"", - "prepare": "yarn prepare:examples && tsc" + "prepare": "yarn build" }, "keywords": [ "ethereum", @@ -84,6 +87,11 @@ "ethdebug" ], "author": "", + "repository": { + "type": "git", + "url": "git+https://github.com/ethdebug/format.git", + "directory": "packages/bugc" + }, "license": "MIT", "devDependencies": { "@ethdebug/evm": "^0.1.0-0", @@ -95,7 +103,6 @@ "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "eslint": "^8.0.0", - "ethereum-cryptography": "^3.2.0", "fast-check": "^4.2.0", "prettier": "^3.5.3", "tsx": "^4.19.4", @@ -105,6 +112,7 @@ }, "dependencies": { "@ethdebug/format": "^0.1.0-0", + "ethereum-cryptography": "^3.2.0", "fp-ts": "^2.16.11", "parsimmon": "^1.18.1" }, diff --git a/packages/bugc/tsconfig.build.json b/packages/bugc/tsconfig.build.json new file mode 100644 index 0000000000..f394fe4c90 --- /dev/null +++ b/packages/bugc/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "declarationMap": false }, + "include": ["src/**/*", "bin/**/*"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.test.tsx", + "test/**" + ], + "references": [ + { "path": "../format/tsconfig.build.json" }, + { "path": "../pointers/tsconfig.build.json" }, + { "path": "../evm/tsconfig.build.json" } + ] +} diff --git a/packages/conformance/package.json b/packages/conformance/package.json index 586ee8e3f5..0f0b662977 100644 --- a/packages/conformance/package.json +++ b/packages/conformance/package.json @@ -1,6 +1,7 @@ { "name": "@ethdebug/conformance", "version": "0.1.0-0", + "private": true, "description": "Reusable ETHDebug conformance runner and adapters", "type": "module", "main": "dist/src/index.js", @@ -51,8 +52,5 @@ "@types/node": "^20.0.0", "typescript": "^5.0.0", "vitest": "^3.2.4" - }, - "publishConfig": { - "access": "public" } } diff --git a/packages/evm/LICENSE b/packages/evm/LICENSE new file mode 100644 index 0000000000..5d84fb65eb --- /dev/null +++ b/packages/evm/LICENSE @@ -0,0 +1,25 @@ +Note: This license applies to all files in this repository EXCEPT those in the +schemas/ directory, which are licensed under CC0 1.0 Universal. See +schemas/LICENSE for details. + +MIT License + +Copyright (c) 2024 ethdebug contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/evm/README.md b/packages/evm/README.md new file mode 100644 index 0000000000..dbbd94e245 --- /dev/null +++ b/packages/evm/README.md @@ -0,0 +1,12 @@ +# @ethdebug/evm + +EVM execution and state access for ethdebug/format. + +Part of [ethdebug/format](https://github.com/ethdebug/format). See +the [documentation](https://ethdebug.github.io/format/). + +## TypeScript + +This package uses `package.json` `imports` for internal modules. +Consumers must use `moduleResolution` `node16`, `nodenext`, or +`bundler`. The legacy `node10` resolution is not supported. diff --git a/packages/evm/package.json b/packages/evm/package.json index 02c5c8336f..2227154566 100644 --- a/packages/evm/package.json +++ b/packages/evm/package.json @@ -5,7 +5,19 @@ "type": "module", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/ethdebug/format.git", + "directory": "packages/evm" + }, "license": "MIT", + "files": [ + "dist", + "!dist/**/*.tsbuildinfo" + ], + "engines": { + "node": ">=20" + }, "imports": { "#executor": { "types": "./src/executor.ts", @@ -21,12 +33,13 @@ } }, "scripts": { - "prepare": "tsc", - "build": "tsc", + "build": "rm -rf dist && tsc --build tsconfig.build.json", + "prepare": "yarn build", "watch": "tsc --watch --preserveWatchOutput", "test": "vitest run" }, "dependencies": { + "@ethdebug/pointers": "^0.1.0-0", "@ethereumjs/common": "^10.0.0", "@ethereumjs/evm": "^10.0.0", "@ethereumjs/statemanager": "^10.0.0", @@ -34,19 +47,10 @@ "ethereum-cryptography": "^3.2.0" }, "devDependencies": { - "@ethdebug/pointers": "^0.1.0-0", "@types/node": "^20.0.0", "typescript": "^5.0.0", "vitest": "^3.2.4" }, - "peerDependencies": { - "@ethdebug/pointers": "^0.1.0-0" - }, - "peerDependenciesMeta": { - "@ethdebug/pointers": { - "optional": true - } - }, "publishConfig": { "access": "public" } diff --git a/packages/evm/tsconfig.build.json b/packages/evm/tsconfig.build.json new file mode 100644 index 0000000000..171e4e488a --- /dev/null +++ b/packages/evm/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "declarationMap": false }, + "include": ["src/**/*"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.test.tsx", + "test/**" + ], + "references": [{ "path": "../pointers/tsconfig.build.json" }] +} diff --git a/packages/format/LICENSE b/packages/format/LICENSE new file mode 100644 index 0000000000..5d84fb65eb --- /dev/null +++ b/packages/format/LICENSE @@ -0,0 +1,25 @@ +Note: This license applies to all files in this repository EXCEPT those in the +schemas/ directory, which are licensed under CC0 1.0 Universal. See +schemas/LICENSE for details. + +MIT License + +Copyright (c) 2024 ethdebug contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/format/README.md b/packages/format/README.md new file mode 100644 index 0000000000..b8668508e7 --- /dev/null +++ b/packages/format/README.md @@ -0,0 +1,12 @@ +# @ethdebug/format + +ethdebug/format schemas distributed as NPM package. + +Part of [ethdebug/format](https://github.com/ethdebug/format). See +the [documentation](https://ethdebug.github.io/format/). + +## TypeScript + +This package uses `package.json` `imports` for internal modules. +Consumers must use `moduleResolution` `node16`, `nodenext`, or +`bundler`. The legacy `node10` resolution is not supported. diff --git a/packages/format/package.json b/packages/format/package.json index 2738fe9a82..d2eac06f00 100644 --- a/packages/format/package.json +++ b/packages/format/package.json @@ -4,11 +4,19 @@ "description": "ethdebug/format schemas distributed as NPM package", "type": "module", "main": "dist/src/index.js", - "repository": "https://github.com/ethdebug/format", + "repository": { + "type": "git", + "url": "git+https://github.com/ethdebug/format.git", + "directory": "packages/format" + }, "license": "MIT", "files": [ - "dist" + "dist", + "!dist/**/*.tsbuildinfo" ], + "engines": { + "node": ">=20" + }, "imports": { "#describe": { "types": "./src/describe.ts", @@ -46,12 +54,13 @@ }, "scripts": { "prepare:yamls": "node ./bin/generate-schema-yamls.js", - "prepare": "yarn prepare:yamls && tsc", + "build": "yarn prepare:yamls && rm -rf dist && tsc --build tsconfig.build.json", + "prepare": "yarn build", "clean": "rm -rf dist && rm src/schemas/yamls.ts", "test": "vitest", "watch:typescript": "tsc --watch", "watch:schemas": "nodemon --watch ../../schemas -e 'yaml' --exec 'yarn prepare:yamls'", - "watch": "yarn prepare && concurrently --names=tsc,schemas \"yarn watch:typescript\" \"yarn watch:schemas\"" + "watch": "yarn prepare:yamls && concurrently --names=tsc,schemas \"yarn watch:typescript\" \"yarn watch:schemas\"" }, "dependencies": { "json-schema-typed": "8.0.1", @@ -71,6 +80,5 @@ }, "publishConfig": { "access": "public" - }, - "gitHead": "a5f00cb643dd589c6d6fc7d4471adbfefbb99e86" + } } diff --git a/packages/format/tsconfig.build.json b/packages/format/tsconfig.build.json new file mode 100644 index 0000000000..16af548a96 --- /dev/null +++ b/packages/format/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "declarationMap": false }, + "include": ["src/**/*"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.test.tsx", + "test/**" + ] +} diff --git a/packages/playground/src/compiler/CompilerOutput.tsx b/packages/playground/src/compiler/CompilerOutput.tsx index e911ac24c8..b73087b3c2 100644 --- a/packages/playground/src/compiler/CompilerOutput.tsx +++ b/packages/playground/src/compiler/CompilerOutput.tsx @@ -11,12 +11,12 @@ import { ErrorView } from "./ErrorView"; import "./CompilerOutput.css"; // CSS for bugc-react components -import "@ethdebug/bugc-react/src/components/variables.css"; -import "@ethdebug/bugc-react/src/components/AstView.css"; -import "@ethdebug/bugc-react/src/components/BytecodeView.css"; -import "@ethdebug/bugc-react/src/components/CfgView.css"; -import "@ethdebug/bugc-react/src/components/EthdebugTooltip.css"; -import "@ethdebug/bugc-react/src/components/IrView.css"; +import "@ethdebug/bugc-react/dist/src/components/variables.css"; +import "@ethdebug/bugc-react/dist/src/components/AstView.css"; +import "@ethdebug/bugc-react/dist/src/components/BytecodeView.css"; +import "@ethdebug/bugc-react/dist/src/components/CfgView.css"; +import "@ethdebug/bugc-react/dist/src/components/EthdebugTooltip.css"; +import "@ethdebug/bugc-react/dist/src/components/IrView.css"; interface CompilerOutputProps { result: CompileResult; diff --git a/packages/pointers-react/LICENSE b/packages/pointers-react/LICENSE new file mode 100644 index 0000000000..5d84fb65eb --- /dev/null +++ b/packages/pointers-react/LICENSE @@ -0,0 +1,25 @@ +Note: This license applies to all files in this repository EXCEPT those in the +schemas/ directory, which are licensed under CC0 1.0 Universal. See +schemas/LICENSE for details. + +MIT License + +Copyright (c) 2024 ethdebug contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/pointers-react/README.md b/packages/pointers-react/README.md new file mode 100644 index 0000000000..d45624bb06 --- /dev/null +++ b/packages/pointers-react/README.md @@ -0,0 +1,12 @@ +# @ethdebug/pointers-react + +React components for visualizing ethdebug/format pointer resolution. + +Part of [ethdebug/format](https://github.com/ethdebug/format). See +the [documentation](https://ethdebug.github.io/format/). + +## TypeScript + +This package uses `package.json` `imports` for internal modules. +Consumers must use `moduleResolution` `node16`, `nodenext`, or +`bundler`. The legacy `node10` resolution is not supported. diff --git a/packages/pointers-react/package.json b/packages/pointers-react/package.json index 86d5ba8969..c93dadd65a 100644 --- a/packages/pointers-react/package.json +++ b/packages/pointers-react/package.json @@ -5,7 +5,19 @@ "type": "module", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/ethdebug/format.git", + "directory": "packages/pointers-react" + }, "license": "MIT", + "files": [ + "dist", + "!dist/**/*.tsbuildinfo" + ], + "engines": { + "node": ">=20" + }, "imports": { "#components/*": { "types": "./src/components/*.tsx", @@ -21,8 +33,8 @@ } }, "scripts": { - "prepare": "tsc", - "build": "tsc", + "build": "rm -rf dist && tsc --build tsconfig.build.json && copyfiles -u 1 \"src/**/*.css\" dist/src", + "prepare": "yarn build", "watch": "tsc --watch --preserveWatchOutput", "test": "vitest run" }, diff --git a/packages/pointers-react/tsconfig.build.json b/packages/pointers-react/tsconfig.build.json new file mode 100644 index 0000000000..1ee184372e --- /dev/null +++ b/packages/pointers-react/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "declarationMap": false }, + "include": ["src/**/*"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.test.tsx", + "test/**" + ], + "references": [ + { "path": "../format/tsconfig.build.json" }, + { "path": "../pointers/tsconfig.build.json" } + ] +} diff --git a/packages/pointers/LICENSE b/packages/pointers/LICENSE new file mode 100644 index 0000000000..5d84fb65eb --- /dev/null +++ b/packages/pointers/LICENSE @@ -0,0 +1,25 @@ +Note: This license applies to all files in this repository EXCEPT those in the +schemas/ directory, which are licensed under CC0 1.0 Universal. See +schemas/LICENSE for details. + +MIT License + +Copyright (c) 2024 ethdebug contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/pointers/README.md b/packages/pointers/README.md index d6f715e284..98ce619827 100644 --- a/packages/pointers/README.md +++ b/packages/pointers/README.md @@ -3,3 +3,9 @@ _This NPM package contains a reference implementation for dereferencing **ethdebug/format** [pointers](https://ethdebug.github.io/format/spec/pointer/overview)._ + +## TypeScript + +This package uses `package.json` `imports` for internal modules. +Consumers must use `moduleResolution` `node16`, `nodenext`, or +`bundler`. The legacy `node10` resolution is not supported. diff --git a/packages/pointers/bin/run-example.ts b/packages/pointers/bin/run-example.ts index f3e6a34681..676c62cc80 100644 --- a/packages/pointers/bin/run-example.ts +++ b/packages/pointers/bin/run-example.ts @@ -6,7 +6,7 @@ import "../src/index.js"; import { observeTrace } from "../test/index.js"; -import { observeTraceTests } from "../src/test-cases.js"; +import { observeTraceTests } from "../test/test-cases.js"; export async function run() { const { pointer, compileOptions, observe } = diff --git a/packages/pointers/package.json b/packages/pointers/package.json index 7de1fa7a25..e556cac67b 100644 --- a/packages/pointers/package.json +++ b/packages/pointers/package.json @@ -4,7 +4,19 @@ "description": "Reference implementation for ethdebug/format pointers", "main": "dist/src/index.js", "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/ethdebug/format.git", + "directory": "packages/pointers" + }, "license": "MIT", + "files": [ + "dist", + "!dist/**/*.tsbuildinfo" + ], + "engines": { + "node": ">=20" + }, "imports": { "#cursor": { "types": "./src/cursor.ts", @@ -37,13 +49,13 @@ "#test/*": "./test/*.ts" }, "scripts": { - "run-example": "node ./dist/bin/run-example.js", - "prepare": "tsc --build", + "build": "rm -rf dist && tsc --build tsconfig.build.json", + "prepare": "yarn build", + "run-example": "tsx bin/run-example.ts", "watch": "tsc --build --watch", "test": "vitest" }, "devDependencies": { - "@ethdebug/format": "^0.1.0-0", "chalk": "^5.6.2", "cli-highlight": "^2.1.11", "ganache": "7.9.x", @@ -53,6 +65,7 @@ "vitest": "^3.2.4" }, "dependencies": { + "@ethdebug/format": "^0.1.0-0", "ethereum-cryptography": "^2.2.1" }, "publishConfig": { diff --git a/packages/pointers/src/integration.test.ts b/packages/pointers/src/integration.test.ts index 4db2f8d219..2c346b24ce 100644 --- a/packages/pointers/src/integration.test.ts +++ b/packages/pointers/src/integration.test.ts @@ -1,7 +1,7 @@ import { expect, describe, it } from "vitest"; import { observeTrace } from "../test/index.js"; -import { observeTraceTests } from "./test-cases.js"; +import { observeTraceTests } from "../test/test-cases.js"; describe("dereference (integration)", () => { describe("changing pointer values over the course of a trace", () => { diff --git a/packages/pointers/src/test-cases.ts b/packages/pointers/test/test-cases.ts similarity index 98% rename from packages/pointers/src/test-cases.ts rename to packages/pointers/test/test-cases.ts index ad726d4ec2..9faf55a05d 100644 --- a/packages/pointers/src/test-cases.ts +++ b/packages/pointers/test/test-cases.ts @@ -2,8 +2,8 @@ import { singleSourceCompilation, findExamplePointer, type ObserveTraceOptions, -} from "../test/index.js"; -import { type Cursor, Data } from "./index.js"; +} from "./index.js"; +import { type Cursor, Data } from "../src/index.js"; export interface ObserveTraceTest extends ObserveTraceOptions { expectedValues: V[]; diff --git a/packages/pointers/tsconfig.build.json b/packages/pointers/tsconfig.build.json new file mode 100644 index 0000000000..c40749e2a8 --- /dev/null +++ b/packages/pointers/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "declarationMap": false }, + "include": ["src/**/*", "typings.d.ts"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.test.tsx", + "test/**", + "bin/**" + ], + "references": [{ "path": "../format/tsconfig.build.json" }] +} diff --git a/packages/programs-react/LICENSE b/packages/programs-react/LICENSE new file mode 100644 index 0000000000..5d84fb65eb --- /dev/null +++ b/packages/programs-react/LICENSE @@ -0,0 +1,25 @@ +Note: This license applies to all files in this repository EXCEPT those in the +schemas/ directory, which are licensed under CC0 1.0 Universal. See +schemas/LICENSE for details. + +MIT License + +Copyright (c) 2024 ethdebug contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/programs-react/README.md b/packages/programs-react/README.md new file mode 100644 index 0000000000..07cdd846b9 --- /dev/null +++ b/packages/programs-react/README.md @@ -0,0 +1,13 @@ +# @ethdebug/programs-react + +React components for visualizing ethdebug/format program +annotations. + +Part of [ethdebug/format](https://github.com/ethdebug/format). See +the [documentation](https://ethdebug.github.io/format/). + +## TypeScript + +This package uses `package.json` `imports` for internal modules. +Consumers must use `moduleResolution` `node16`, `nodenext`, or +`bundler`. The legacy `node10` resolution is not supported. diff --git a/packages/programs-react/package.json b/packages/programs-react/package.json index 0baa592be9..c1a97ee176 100644 --- a/packages/programs-react/package.json +++ b/packages/programs-react/package.json @@ -5,7 +5,19 @@ "type": "module", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/ethdebug/format.git", + "directory": "packages/programs-react" + }, "license": "MIT", + "files": [ + "dist", + "!dist/**/*.tsbuildinfo" + ], + "engines": { + "node": ">=20" + }, "imports": { "#components/*": { "types": "./src/components/*.tsx", @@ -21,8 +33,8 @@ } }, "scripts": { - "prepare": "tsc", - "build": "tsc", + "build": "rm -rf dist && tsc --build tsconfig.build.json && copyfiles -u 1 \"src/**/*.css\" dist/src", + "prepare": "yarn build", "watch": "tsc --watch --preserveWatchOutput", "test": "vitest run" }, diff --git a/packages/programs-react/tsconfig.build.json b/packages/programs-react/tsconfig.build.json new file mode 100644 index 0000000000..dd64717535 --- /dev/null +++ b/packages/programs-react/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "declarationMap": false }, + "include": ["src/**/*"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.test.tsx", + "test/**" + ], + "references": [{ "path": "../format/tsconfig.build.json" }] +} diff --git a/packages/web/docs/implementation-guides/pointers/testing/test-cases/string-storage.mdx b/packages/web/docs/implementation-guides/pointers/testing/test-cases/string-storage.mdx index 0faa61b46d..4e4fa38c54 100644 --- a/packages/web/docs/implementation-guides/pointers/testing/test-cases/string-storage.mdx +++ b/packages/web/docs/implementation-guides/pointers/testing/test-cases/string-storage.mdx @@ -18,7 +18,7 @@ value. sourceFile.getVariableStatement("stringStorageTest")} /> diff --git a/packages/web/docs/implementation-guides/pointers/testing/test-cases/struct-storage.mdx b/packages/web/docs/implementation-guides/pointers/testing/test-cases/struct-storage.mdx index 26a2b6dcf7..82ef104eab 100644 --- a/packages/web/docs/implementation-guides/pointers/testing/test-cases/struct-storage.mdx +++ b/packages/web/docs/implementation-guides/pointers/testing/test-cases/struct-storage.mdx @@ -16,7 +16,7 @@ with a few small fields (`struct Record { uint8 x; uint8 y; bytes4 salt; }`). sourceFile.getVariableStatement("structStorageTest")} /> diff --git a/packages/web/docs/implementation-guides/pointers/testing/test-cases/test-cases.mdx b/packages/web/docs/implementation-guides/pointers/testing/test-cases/test-cases.mdx index 9f4ee16dce..71729817dc 100644 --- a/packages/web/docs/implementation-guides/pointers/testing/test-cases/test-cases.mdx +++ b/packages/web/docs/implementation-guides/pointers/testing/test-cases/test-cases.mdx @@ -14,7 +14,7 @@ Test cases are aggregated into the `observeTraceTests` variable: sourceFile.getVariableStatement("observeTraceTests")} links={{ structStorageTest: diff --git a/packages/web/docs/implementation-guides/pointers/testing/test-cases/uint256-array-memory.mdx b/packages/web/docs/implementation-guides/pointers/testing/test-cases/uint256-array-memory.mdx index fda08009a7..099bb5009f 100644 --- a/packages/web/docs/implementation-guides/pointers/testing/test-cases/uint256-array-memory.mdx +++ b/packages/web/docs/implementation-guides/pointers/testing/test-cases/uint256-array-memory.mdx @@ -16,7 +16,7 @@ the course of the transaction. sourceFile.getVariableStatement("uint256ArrayMemoryTest") } diff --git a/vitest.config.ts b/vitest.config.ts index 10e9204017..05ca38447c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,10 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - projects: ["packages/*"], + projects: [ + "packages/*", + { test: { name: "bin", include: ["bin/**/*.test.ts"] } }, + ], coverage: { provider: "v8", reporter: ["text", "json", "html"], diff --git a/yarn.lock b/yarn.lock index 5a707921de..fff686d1c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7962,6 +7962,19 @@ copy-webpack-plugin@^11.0.0: schema-utils "^4.0.0" serialize-javascript "^6.0.0" +copyfiles@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.4.1.tgz#d2dcff60aaad1015f09d0b66e7f0f1c5cd3c5da5" + integrity sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg== + dependencies: + glob "^7.0.5" + minimatch "^3.0.3" + mkdirp "^1.0.4" + noms "0.0.0" + through2 "^2.0.1" + untildify "^4.0.0" + yargs "^16.1.0" + core-js-compat@^3.31.0: version "3.35.0" resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz" @@ -9968,7 +9981,7 @@ glob@^10.4.1: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.1.3: +glob@^7.0.5, glob@^7.1.3: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -10735,7 +10748,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -12779,6 +12792,13 @@ minimatch@9.0.3, minimatch@^9.0.0, minimatch@^9.0.1, minimatch@^9.0.3: dependencies: brace-expansion "^2.0.1" +minimatch@^3.0.3: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + minimatch@^5.0.1: version "5.1.6" resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" @@ -12888,7 +12908,7 @@ minizlib@^2.1.1, minizlib@^2.1.2: minipass "^3.0.0" yallist "^4.0.0" -mkdirp@^1.0.3: +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -13100,6 +13120,14 @@ nodemon@^3.1.11: touch "^3.1.0" undefsafe "^2.0.5" +noms@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859" + integrity sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow== + dependencies: + inherits "^2.0.1" + readable-stream "~1.0.31" + nopt@^7.0.0: version "7.2.0" resolved "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz" @@ -15074,6 +15102,16 @@ readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@~1.0.31: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -16198,6 +16236,11 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" @@ -16513,7 +16556,7 @@ throttleit@2.1.0: resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-2.1.0.tgz#a7e4aa0bf4845a5bd10daa39ea0c783f631a07b4" integrity sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw== -through2@^2.0.0: +through2@^2.0.0, through2@^2.0.1: version "2.0.5" resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -17015,6 +17058,11 @@ unpipe@~1.0.0: resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + upath@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz" @@ -17791,6 +17839,19 @@ yargs@^16.0.0, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^16.1.0: + version "16.2.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.2.tgz#c56731dca0d2788ae0866dd3c83907d6bab85f7d" + integrity sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"