From 9ea2a4499e0212ccd783f923a51bcd607ab71280 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 14 Sep 2026 16:00:19 -0400 Subject: [PATCH 1/9] packages: prepare the public manifests for npm Give the seven public packages the fields npm and its consumers need: `files` limited to `dist` (minus the incremental build info), `engines`, a full `repository` entry with `directory`, a LICENSE copy, and a README that states the supported TypeScript resolution modes (node16, nodenext, bundler). Declare the runtime dependencies that were only devDependencies or an optional peer: pointers on format, evm on pointers, bugc on ethereum-cryptography. Each of those packages failed to import when installed alone. Run bugc's CLI from `dist/bin/bugc.js` with a node shebang; the source shebang needed tsx, which consumers do not have. Mark conformance private: it is a test harness, not a library. --- packages/bugc-react/LICENSE | 25 +++++++++++++++++++++++++ packages/bugc-react/README.md | 12 ++++++++++++ packages/bugc-react/package.json | 12 ++++++++++++ packages/bugc/LICENSE | 25 +++++++++++++++++++++++++ packages/bugc/README.md | 13 +++++++++++++ packages/bugc/bin/bugc.ts | 2 +- packages/bugc/package.json | 16 ++++++++++++---- packages/conformance/package.json | 4 +--- packages/evm/LICENSE | 25 +++++++++++++++++++++++++ packages/evm/README.md | 12 ++++++++++++ packages/evm/package.json | 22 +++++++++++++--------- packages/format/LICENSE | 25 +++++++++++++++++++++++++ packages/format/README.md | 12 ++++++++++++ packages/format/package.json | 15 +++++++++++---- packages/pointers-react/LICENSE | 25 +++++++++++++++++++++++++ packages/pointers-react/README.md | 12 ++++++++++++ packages/pointers-react/package.json | 12 ++++++++++++ packages/pointers/LICENSE | 25 +++++++++++++++++++++++++ packages/pointers/README.md | 6 ++++++ packages/pointers/package.json | 14 +++++++++++++- packages/programs-react/LICENSE | 25 +++++++++++++++++++++++++ packages/programs-react/README.md | 13 +++++++++++++ packages/programs-react/package.json | 12 ++++++++++++ 23 files changed, 342 insertions(+), 22 deletions(-) create mode 100644 packages/bugc-react/LICENSE create mode 100644 packages/bugc-react/README.md create mode 100644 packages/bugc/LICENSE create mode 100644 packages/bugc/README.md create mode 100644 packages/evm/LICENSE create mode 100644 packages/evm/README.md create mode 100644 packages/format/LICENSE create mode 100644 packages/format/README.md create mode 100644 packages/pointers-react/LICENSE create mode 100644 packages/pointers-react/README.md create mode 100644 packages/pointers/LICENSE create mode 100644 packages/programs-react/LICENSE create mode 100644 packages/programs-react/README.md 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..6ce62ec24d 100644 --- a/packages/bugc-react/package.json +++ b/packages/bugc-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/bugc-react" + }, "license": "MIT", + "files": [ + "dist", + "!dist/**/*.tsbuildinfo" + ], + "engines": { + "node": ">=20" + }, "exports": { ".": { "types": "./dist/src/index.d.ts", 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..9d127a5bf3 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,7 +64,7 @@ "#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", @@ -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/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..d83b1b7889 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", @@ -27,6 +39,7 @@ "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/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..b0a1580b1e 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", @@ -71,6 +79,5 @@ }, "publishConfig": { "access": "public" - }, - "gitHead": "a5f00cb643dd589c6d6fc7d4471adbfefbb99e86" + } } 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..fe54f9c19e 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", 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/package.json b/packages/pointers/package.json index 7de1fa7a25..fc6638ad24 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", @@ -43,7 +55,6 @@ "test": "vitest" }, "devDependencies": { - "@ethdebug/format": "^0.1.0-0", "chalk": "^5.6.2", "cli-highlight": "^2.1.11", "ganache": "7.9.x", @@ -53,6 +64,7 @@ "vitest": "^3.2.4" }, "dependencies": { + "@ethdebug/format": "^0.1.0-0", "ethereum-cryptography": "^2.2.1" }, "publishConfig": { 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..f210e191f2 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", From 9649ff1c3b3a4ef2c3d7844d8a2283336a703e47 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 14 Sep 2026 16:08:49 -0400 Subject: [PATCH 2/9] pointers: move the test cases out of src `src/test-cases.ts` imported the `test/` helpers, so it could not stay in the shipped tree once `test/` is excluded from the build. It lives in `test/` now; the integration test, the example runner, and the four docs pages that embed it follow. --- packages/pointers/bin/run-example.ts | 2 +- packages/pointers/src/integration.test.ts | 2 +- packages/pointers/{src => test}/test-cases.ts | 4 ++-- .../pointers/testing/test-cases/string-storage.mdx | 2 +- .../pointers/testing/test-cases/struct-storage.mdx | 2 +- .../pointers/testing/test-cases/test-cases.mdx | 2 +- .../pointers/testing/test-cases/uint256-array-memory.mdx | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) rename packages/pointers/{src => test}/test-cases.ts (98%) 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/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/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") } From 8ab43bb7824cdec5e3a44ef072f05b3a6d26e673 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 14 Sep 2026 16:24:20 -0400 Subject: [PATCH 3/9] build: compile publishable output from tsconfig.build.json Each public package gets a `tsconfig.build.json` that includes only `src/` (plus `bin/` for bugc), excludes tests, turns off declaration maps, and restates its project references against the sibling build configs: references are not inherited through `extends`, and `tsc --build` needs them to build dependencies first. One `build` script per package does the generation step, wipes `dist/`, and compiles; `prepare` calls it, so a fresh install and a publish produce the same clean output. Root `build` runs the package builds in dependency order through Lerna, skipping the private packages; root `test` runs vitest once. A root `nx.json` makes Lerna resolve the workspace root inside a git worktree instead of the shallowest ancestor that contains nx. format's `watch` no longer starts with the clean build, which raced the other watchers. --- .gitignore | 1 + nx.json | 1 + package.json | 4 ++-- packages/bugc/package.json | 4 ++-- packages/bugc/tsconfig.build.json | 17 +++++++++++++++++ packages/evm/package.json | 4 ++-- packages/evm/tsconfig.build.json | 13 +++++++++++++ packages/format/package.json | 5 +++-- packages/format/tsconfig.build.json | 12 ++++++++++++ packages/pointers/package.json | 5 +++-- packages/pointers/tsconfig.build.json | 14 ++++++++++++++ 11 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 nx.json create mode 100644 packages/bugc/tsconfig.build.json create mode 100644 packages/evm/tsconfig.build.json create mode 100644 packages/format/tsconfig.build.json create mode 100644 packages/pointers/tsconfig.build.json 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/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..c494fb332a 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", diff --git a/packages/bugc/package.json b/packages/bugc/package.json index 9d127a5bf3..d5ecf05619 100644 --- a/packages/bugc/package.json +++ b/packages/bugc/package.json @@ -68,7 +68,7 @@ }, "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", @@ -78,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", 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/evm/package.json b/packages/evm/package.json index d83b1b7889..2227154566 100644 --- a/packages/evm/package.json +++ b/packages/evm/package.json @@ -33,8 +33,8 @@ } }, "scripts": { - "prepare": "tsc", - "build": "tsc", + "build": "rm -rf dist && tsc --build tsconfig.build.json", + "prepare": "yarn build", "watch": "tsc --watch --preserveWatchOutput", "test": "vitest run" }, 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/package.json b/packages/format/package.json index b0a1580b1e..d2eac06f00 100644 --- a/packages/format/package.json +++ b/packages/format/package.json @@ -54,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", 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/pointers/package.json b/packages/pointers/package.json index fc6638ad24..e556cac67b 100644 --- a/packages/pointers/package.json +++ b/packages/pointers/package.json @@ -49,8 +49,9 @@ "#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" }, 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" }] +} From 87aae448461a41a0abc0c6a9dffbda9d942c4eef Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 14 Sep 2026 16:39:36 -0400 Subject: [PATCH 4/9] react packages: ship stylesheets in dist tsc does not copy CSS, so the React packages' stylesheets were absent from anything built from `dist/`. The build now copies `src/**/*.css` into `dist/src/`, bugc-react exports the new path, and the playground imports it. --- package.json | 1 + packages/bugc-react/package.json | 6 +- packages/bugc-react/tsconfig.build.json | 13 ++++ .../src/compiler/CompilerOutput.tsx | 12 ++-- packages/pointers-react/package.json | 4 +- packages/pointers-react/tsconfig.build.json | 16 +++++ packages/programs-react/package.json | 4 +- packages/programs-react/tsconfig.build.json | 13 ++++ yarn.lock | 69 +++++++++++++++++-- 9 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 packages/bugc-react/tsconfig.build.json create mode 100644 packages/pointers-react/tsconfig.build.json create mode 100644 packages/programs-react/tsconfig.build.json diff --git a/package.json b/package.json index c494fb332a..1be0987453 100644 --- a/package.json +++ b/package.json @@ -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/package.json b/packages/bugc-react/package.json index 6ce62ec24d..7fccd580a1 100644 --- a/packages/bugc-react/package.json +++ b/packages/bugc-react/package.json @@ -23,7 +23,7 @@ "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/*": { @@ -44,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/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/package.json b/packages/pointers-react/package.json index fe54f9c19e..c93dadd65a 100644 --- a/packages/pointers-react/package.json +++ b/packages/pointers-react/package.json @@ -33,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/programs-react/package.json b/packages/programs-react/package.json index f210e191f2..c1a97ee176 100644 --- a/packages/programs-react/package.json +++ b/packages/programs-react/package.json @@ -33,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/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" From 908e6710c2f628394afba37d57bac227f0d3f3f0 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 15 Sep 2026 14:24:13 -0400 Subject: [PATCH 5/9] bin: add a tarball allow-list check `checkPackList` accepts only `package.json`, README, LICENSE, `dist/src/**` and `dist/bin/**`, and rejects anything that looks like a test or build info. `packList` runs `npm pack --dry-run --json` and reads the last JSON array, because a package's `prepare` script prints before it. Tests live in a root `bin` vitest project. --- bin/packlist.test.ts | 49 ++++++++++++++++++++++++++++++++++++++++++++ bin/packlist.ts | 40 ++++++++++++++++++++++++++++++++++++ vitest.config.ts | 5 ++++- 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 bin/packlist.test.ts create mode 100644 bin/packlist.ts 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/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"], From f200d3e49c66cbaa190d90b46b778a8ffe891187 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 14 Sep 2026 16:52:11 -0400 Subject: [PATCH 6/9] bin: publish packages tagged at HEAD with the npm CLI `publish-tagged.ts` reads the Lerna tags that point at HEAD, maps them to public workspaces, checks each tag against the manifest version, and publishes in dependency order with `npm publish --access public --tag latest`. The explicit tag is required: npm 11 refuses to publish a prerelease version without one. Provenance is requested only under GitHub Actions. Before each publish the script asks the registry for the package's versions and skips one that is already there, so a re-run after a partial failure is safe; any other registry error aborts the run. It also checks the pack list, prints published, skipped and failed summaries, and warns about a tag that names no workspace. Lerna's own publish command is not used: it cannot exchange the GitHub OIDC token for npm credentials. --- bin/publish-tagged.test.ts | 234 +++++++++++++++++++++++++++++++++++++ bin/publish-tagged.ts | 223 +++++++++++++++++++++++++++++++++++ 2 files changed, 457 insertions(+) create mode 100644 bin/publish-tagged.test.ts create mode 100644 bin/publish-tagged.ts 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); + } +} From 3a5bbb1f131f6cb30d8eec5d3e44eeb7083f0b6c Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 15 Sep 2026 14:24:16 -0400 Subject: [PATCH 7/9] bin: check and smoke-test the publishable tarballs `check-tarballs.ts` runs the allow-list check on every public package. `smoke-tarballs.ts` goes further: it packs each package, installs it alone in a temporary directory with only its sibling tarballs, imports it, and runs bugc's CLI. The allow-list sees file names only; the smoke test is what catches a runtime dependency declared as a devDependency, which the workspace's hoisted node_modules and the registry's stale 0.1.0-0 builds would otherwise hide. --- bin/check-tarballs.ts | 23 +++++ bin/smoke-tarballs.test.ts | 39 +++++++++ bin/smoke-tarballs.ts | 175 +++++++++++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 bin/check-tarballs.ts create mode 100644 bin/smoke-tarballs.test.ts create mode 100644 bin/smoke-tarballs.ts 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/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()); +} From f268ce783fc8d06ae71d613961feeac39a979a15 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 14 Sep 2026 17:02:59 -0400 Subject: [PATCH 8/9] ci: test on Node 20 and 22 and check the tarballs The publish job needs Node 22 for npm 11, so the test suite runs on both versions. The tarball checks run after the tests. --- .github/workflows/ci.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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: From 3a86ef16eb13cde85bbe814d7e8e029e76619271 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 14 Sep 2026 17:03:16 -0400 Subject: [PATCH 9/9] ci: publish packages tagged at HEAD on push to main Lerna pushes the Publish commit and its tags in one atomic push, and GitHub creates no tag events when more than three tags arrive at once, so the workflow triggers on pushes to main. A short `check` job looks for package tags at HEAD and gates the publish job, which installs npm 11 for trusted publishing, runs the tests, and calls `publish-tagged.ts`. A manual dispatch defaults to a dry run. --- .github/workflows/publish.yml | 70 +++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/publish.yml 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' || '' }}