Add npm publish infrastructure with platform-specific packages#5
Conversation
Set up the esbuild-style pattern for distributing prebuilt binaries: - Add platform packages under npm/ for linux-x64, darwin-arm64, darwin-x64, and win32-x64 (each with os/cpu fields so npm only installs the matching one) - Update main package.json with optionalDependencies on all four - Rewrite nodejs-helper.js/cjs to resolve from platform packages first, falling back to local build/ then dist/ for development - Add npm-publish.yaml workflow: builds on all 4 platforms, runs integration tests, then publishes all packages on GitHub release To publish: create a GitHub release (tag matching the version), with NPM_TOKEN set as a repository secret. https://claude.ai/code/session_0156tM6LmjAC3xYz5xwETvym
There was a problem hiding this comment.
Pull request overview
This PR adds an npm publishing setup that mirrors the esbuild/swc pattern: a main @anthropic/crsqlite package that resolves a platform-specific optional dependency containing the prebuilt native extension, plus a GitHub Actions workflow to build/test/publish on release.
Changes:
- Added four platform-specific npm packages that ship prebuilt
crsqlitebinaries for linux-x64, darwin-arm64, darwin-x64, and win32-x64. - Updated the main Node helper (ESM + CJS) to resolve the extension path from a platform package first, then fall back to local
build/anddist/paths. - Added an
npm-publishGitHub Actions workflow to build artifacts per platform and publish packages to npm on GitHub release.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
npm/crsqlite-win32-x64/package.json |
Defines Windows x64 platform package metadata and published files. |
npm/crsqlite-win32-x64/index.js |
Exposes the extension basename path for the Windows package. |
npm/crsqlite-linux-x64/package.json |
Defines Linux x64 platform package metadata and published files. |
npm/crsqlite-linux-x64/index.js |
Exposes the extension basename path for the Linux package. |
npm/crsqlite-darwin-x64/package.json |
Defines macOS x64 platform package metadata and published files. |
npm/crsqlite-darwin-x64/index.js |
Exposes the extension basename path for the macOS x64 package. |
npm/crsqlite-darwin-arm64/package.json |
Defines macOS arm64 platform package metadata and published files. |
npm/crsqlite-darwin-arm64/index.js |
Exposes the extension basename path for the macOS arm64 package. |
core/package.json |
Switches distribution to platform optionalDependencies and removes dist/ from published files. |
core/nodejs-helper.js |
Adds platform package resolution + dev fallbacks for ESM consumers. |
core/nodejs-helper.cjs |
Adds platform package resolution + dev fallbacks for CJS consumers. |
.github/workflows/npm-publish.yaml |
Builds, tests, and publishes platform packages + the main package on release. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -26,6 +25,12 @@ | |||
| "engines": { | |||
| "node": ">=22.5.0" | |||
| }, | |||
| "optionalDependencies": { | |||
| "@anthropic/crsqlite-linux-x64": "0.1.0", | |||
| "@anthropic/crsqlite-darwin-arm64": "0.1.0", | |||
| "@anthropic/crsqlite-darwin-x64": "0.1.0", | |||
| "@anthropic/crsqlite-win32-x64": "0.1.0" | |||
| }, | |||
There was a problem hiding this comment.
@anthropic/crsqlite no longer publishes dist/ in the files list, but nodejs-helper still falls back to dist/crsqlite when the platform optionalDependency isn't available. On unsupported platforms (or if optional deps are skipped), extensionPath will point to a file that is not shipped, causing a runtime failure that's hard to diagnose. Consider either re-including a fallback binary in the published package, or make the helper throw a clear error when neither a platform package nor a local build artifact exists.
| function resolve() { | ||
| // 1. Try the platform-specific npm package. | ||
| const key = `${process.platform}-${process.arch}`; | ||
| const pkg = PLATFORM_PACKAGES[key]; | ||
| if (pkg) { | ||
| try { | ||
| return require(pkg).path; | ||
| } catch {} | ||
| } |
There was a problem hiding this comment.
The platform-package resolution try { return require(pkg).path; } catch {} swallows all errors, including cases where the platform package exists but is broken/mis-packaged (e.g., missing binary or exporting the wrong shape). This can silently fall back to build//dist/ and make failures non-deterministic. Consider only ignoring MODULE_NOT_FOUND for the optional dependency and rethrowing other errors (or include them in a helpful error message).
| // 2. Local build/ (dev). | ||
| const buildPath = join(__dirname, "build", "crsqlite"); | ||
| if ( | ||
| existsSync(buildPath + ".so") || | ||
| existsSync(buildPath + ".dylib") || | ||
| existsSync(buildPath + ".dll") | ||
| ) { | ||
| return buildPath; | ||
| } | ||
|
|
||
| // 3. Local dist/ (legacy). | ||
| return join(__dirname, "dist", "crsqlite"); | ||
| } |
There was a problem hiding this comment.
resolve() currently returns dist/crsqlite unconditionally as the final fallback, but dist/ is not included in the published npm package (and build/ won't exist for consumers). This means extensionPath can resolve to a non-existent path on unsupported platforms or when optional deps aren't installed. Consider checking dist for an actual file (e.g., .so/.dylib/.dll) and otherwise throwing a clear, actionable error (supported platforms + how to build from source).
| if (pkg) { | ||
| try { | ||
| return require(pkg).path; | ||
| } catch {} |
There was a problem hiding this comment.
Same as the ESM helper: catch {} here swallows any error from require(pkg), including real runtime errors in the platform package, and can hide packaging regressions. Consider only swallowing MODULE_NOT_FOUND and surfacing other failures.
| } catch {} | |
| } catch (error) { | |
| if (!error || error.code !== "MODULE_NOT_FOUND") { | |
| throw error; | |
| } | |
| } |
| const buildPath = join(__dirname, "build", "crsqlite"); | ||
| if ( | ||
| existsSync(buildPath + ".so") || | ||
| existsSync(buildPath + ".dylib") || | ||
| existsSync(buildPath + ".dll") | ||
| ) { | ||
| return buildPath; | ||
| } | ||
|
|
||
| return join(__dirname, "dist", "crsqlite"); | ||
| } |
There was a problem hiding this comment.
resolve() returns dist/crsqlite as a last resort, but dist/ is not shipped with the npm package anymore, so consumers can get an extensionPath that does not exist (unsupported platform / optional dep skipped). Consider checking for an actual binary in dist and throwing a helpful error if none is found.
| - name: Copy binary to platform package | ||
| shell: bash | ||
| run: | | ||
| cp core/build/crsqlite.${{ matrix.ext }} npm/crsqlite-${{ matrix.platform }}/crsqlite.${{ matrix.ext }} |
There was a problem hiding this comment.
On Windows runners, cmake --build build --config Release typically places outputs under core/build/Release/ (multi-config generators), so copying from core/build/crsqlite.dll is likely to fail. Consider copying from the actual target output (e.g., via a CMake -E copy using the built target path) or handling the Release/ subdir on Windows explicitly.
| cp core/build/crsqlite.${{ matrix.ext }} npm/crsqlite-${{ matrix.platform }}/crsqlite.${{ matrix.ext }} | |
| if [ "${{ matrix.os }}" = "windows-latest" ]; then | |
| cp core/build/Release/crsqlite.${{ matrix.ext }} npm/crsqlite-${{ matrix.platform }}/crsqlite.${{ matrix.ext }} | |
| else | |
| cp core/build/crsqlite.${{ matrix.ext }} npm/crsqlite-${{ matrix.platform }}/crsqlite.${{ matrix.ext }} | |
| fi |
.github/workflows/npm-publish.yaml
Outdated
| for platform in linux-x64 darwin-arm64 darwin-x64 win32-x64; do | ||
| cd npm/crsqlite-${platform} | ||
| npm publish --access public || echo "Skipped crsqlite-${platform} (may already exist)" |
There was a problem hiding this comment.
npm publish --access public || echo "Skipped..." will also mask real publish failures (auth issues, missing files, invalid package.json), letting the workflow continue and potentially publish the main package without the matching platform packages. Consider failing the job on publish errors and only skipping when the version already exists (e.g., pre-check with npm view / handle the specific error code).
| for platform in linux-x64 darwin-arm64 darwin-x64 win32-x64; do | |
| cd npm/crsqlite-${platform} | |
| npm publish --access public || echo "Skipped crsqlite-${platform} (may already exist)" | |
| set -euo pipefail | |
| for platform in linux-x64 darwin-arm64 darwin-x64 win32-x64; do | |
| cd npm/crsqlite-${platform} | |
| package_name=$(node -p "require('./package.json').name") | |
| package_version=$(node -p "require('./package.json').version") | |
| if npm view "${package_name}@${package_version}" version >/dev/null 2>&1; then | |
| echo "Skipped ${package_name}@${package_version} (already exists)" | |
| else | |
| npm publish --access public | |
| fi |
| test: | ||
| name: Test ${{ matrix.os }} | ||
| needs: build | ||
| runs-on: ${{ matrix.os }} | ||
| strategy: | ||
| matrix: | ||
| include: | ||
| - os: ubuntu-latest | ||
| platform: linux-x64 | ||
| - os: macos-14 | ||
| platform: darwin-arm64 | ||
| - os: macos-13 | ||
| platform: darwin-x64 | ||
| steps: |
There was a problem hiding this comment.
The workflow builds the Windows binary but does not run the Node integration tests on Windows. That means a broken Windows artifact could still be published. Consider adding a Windows entry to the test matrix (or an equivalent smoke test that loads the produced .dll).
| # The release tag must match the version in package.json (e.g. v0.1.0). | ||
| # Set NPM_TOKEN as a repository secret to authenticate with npm. | ||
|
|
There was a problem hiding this comment.
The workflow comment says the release tag must match package.json, but there is no step enforcing this. Without a guard, it's easy to accidentally publish with mismatched versions (which also breaks optionalDependencies resolution). Consider adding a step that validates GITHUB_REF_NAME (strip leading v) equals the version in core/package.json before publishing.
Replace NPM_TOKEN secret with GitHub's OIDC id-token. The publish job now uses --provenance, which proves the package was built by this exact workflow and commit. No stored secrets needed — just link each package to this repo on npmjs.com. https://claude.ai/code/session_0156tM6LmjAC3xYz5xwETvym
Summary
@anthropic/crsqlite-{linux-x64,darwin-arm64,darwin-x64,win32-x64}) following the esbuild/swc pattern — npm installs only the matching binary for the user's platformnodejs-helper.js/.cjs) to resolve from platform packages first, falling back to localbuild/thendist/for developmentnpm-publish.yamlworkflow that builds on all 4 platforms, runs integration tests, and publishes to npm on GitHub releaseHow to publish
NPM_TOKENas a repository secretv0.1.0)Test plan
build/)https://claude.ai/code/session_0156tM6LmjAC3xYz5xwETvym