Skip to content

Add npm publish infrastructure with platform-specific packages#5

Merged
sinkingsugar merged 3 commits intopure-c-portfrom
claude/node-integration-options-Is824
Apr 12, 2026
Merged

Add npm publish infrastructure with platform-specific packages#5
sinkingsugar merged 3 commits intopure-c-portfrom
claude/node-integration-options-Is824

Conversation

@sinkingsugar
Copy link
Copy Markdown
Member

Summary

  • Add platform-specific npm packages (@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 platform
  • Update the main package's helper (nodejs-helper.js/.cjs) to resolve from platform packages first, falling back to local build/ then dist/ for development
  • Add npm-publish.yaml workflow that builds on all 4 platforms, runs integration tests, and publishes to npm on GitHub release

How to publish

  1. Set NPM_TOKEN as a repository secret
  2. Create a GitHub release with a tag matching the version (e.g. v0.1.0)
  3. The workflow handles build, test, and publish automatically

Test plan

  • All 12 Node.js integration tests pass locally (extension loading, CRR ops, merge/sync, conflict resolution)
  • CJS and ESM resolution both resolve correctly in dev (fallback to build/)
  • CI passes on this PR (c-tests, node-tests, asan, valgrind, python)
  • Verify npm-publish workflow runs on a test release (requires NPM_TOKEN secret)

https://claude.ai/code/session_0156tM6LmjAC3xYz5xwETvym

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
Copilot AI review requested due to automatic review settings April 12, 2026 15:28
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 crsqlite binaries 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/ and dist/ paths.
  • Added an npm-publish GitHub 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.

Comment on lines 15 to +33
@@ -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"
},
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +45
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 {}
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +59
// 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");
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
if (pkg) {
try {
return require(pkg).path;
} catch {}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
} catch {}
} catch (error) {
if (!error || error.code !== "MODULE_NOT_FOUND") {
throw error;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +31
const buildPath = join(__dirname, "build", "crsqlite");
if (
existsSync(buildPath + ".so") ||
existsSync(buildPath + ".dylib") ||
existsSync(buildPath + ".dll")
) {
return buildPath;
}

return join(__dirname, "dist", "crsqlite");
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
- name: Copy binary to platform package
shell: bash
run: |
cp core/build/crsqlite.${{ matrix.ext }} npm/crsqlite-${{ matrix.platform }}/crsqlite.${{ matrix.ext }}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +112
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)"
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +61
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:
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +8
# 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.

Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
claude added 2 commits April 12, 2026 15:36
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
@sinkingsugar sinkingsugar merged commit 4e2c502 into pure-c-port Apr 12, 2026
8 checks passed
@sinkingsugar sinkingsugar deleted the claude/node-integration-options-Is824 branch April 12, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants