Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .github/workflows/npm-publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
on:
release:
types: [published]
name: "npm-publish"

# The release tag must match the version in package.json (e.g. v0.1.0).
# Uses npm trusted publishing (OIDC provenance) — no NPM_TOKEN secret needed.
# Setup: on npmjs.com, link each package to this repo + workflow file.

Comment on lines +6 to +9

Copilot AI Apr 12, 2026

Copy link

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.
jobs:
build:
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
platform: linux-x64
ext: so
- os: macos-14
platform: darwin-arm64
ext: dylib
- os: macos-13
platform: darwin-x64
ext: dylib
- os: windows-latest
platform: win32-x64
ext: dll
steps:
- uses: actions/checkout@v4

- name: Build extension
run: |
cd core
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release

- name: Copy binary to platform package
shell: bash
run: |
cp core/build/crsqlite.${{ matrix.ext }} npm/crsqlite-${{ matrix.platform }}/crsqlite.${{ matrix.ext }}

Copilot AI Apr 12, 2026

Copy link

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.

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: crsqlite-${{ matrix.platform }}
path: npm/crsqlite-${{ matrix.platform }}/

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:
Comment on lines +49 to +62

Copilot AI Apr 12, 2026

Copy link

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.
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"

- name: Build extension
run: |
cd core
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

- name: Run integration tests
run: |
cd core
node --test test/integration.test.mjs

publish:
name: Publish to npm
needs: [build, test]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"

- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts/

- name: Prepare platform packages
run: |
for platform in linux-x64 darwin-arm64 darwin-x64 win32-x64; do
cp -r artifacts/crsqlite-${platform}/* npm/crsqlite-${platform}/
echo "Contents of npm/crsqlite-${platform}:"
ls -la npm/crsqlite-${platform}/
done

- name: Publish platform packages
run: |
for platform in linux-x64 darwin-arm64 darwin-x64 win32-x64; do
cd npm/crsqlite-${platform}
npm publish --access public --provenance || echo "Skipped crsqlite-${platform} (may already exist)"
cd ../..
done

- name: Publish main package
run: |
cd core
npm publish --access public --provenance
35 changes: 32 additions & 3 deletions core/nodejs-helper.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,33 @@
// CJS entry point — see nodejs-helper.js for usage examples.
// CJS entry point — see nodejs-helper.js for usage and resolution order.
const { join } = require("node:path");
const extensionPath = join(__dirname, "dist", "crsqlite");
module.exports = { extensionPath };
const { existsSync } = require("node:fs");

const PLATFORM_PACKAGES = {
"linux-x64": "@shards-lang/crsqlite-linux-x64",
"darwin-arm64": "@shards-lang/crsqlite-darwin-arm64",
"darwin-x64": "@shards-lang/crsqlite-darwin-x64",
"win32-x64": "@shards-lang/crsqlite-win32-x64",
};

function resolve() {
const key = `${process.platform}-${process.arch}`;
const pkg = PLATFORM_PACKAGES[key];
if (pkg) {
try {
return require(pkg).path;
} catch {}

Copilot AI Apr 12, 2026

Copy link

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

const buildPath = join(__dirname, "build", "crsqlite");
if (
existsSync(buildPath + ".so") ||
existsSync(buildPath + ".dylib") ||
existsSync(buildPath + ".dll")
) {
return buildPath;
}

return join(__dirname, "dist", "crsqlite");
}
Comment on lines +21 to +31

Copilot AI Apr 12, 2026

Copy link

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.

module.exports.extensionPath = resolve();
49 changes: 45 additions & 4 deletions core/nodejs-helper.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,61 @@
// Exports the path to the cr-sqlite loadable extension.
//
// Resolution order:
// 1. Platform-specific npm package (@shards-lang/crsqlite-{os}-{cpu})
// 2. Local build/ directory (development)
// 3. Local dist/ directory (legacy / manual builds)
//
// Usage with node:sqlite (Node >= 22.5):
//
// import { extensionPath } from '@anthropic/crsqlite';
// import { extensionPath } from '@shards-lang/crsqlite';
// import { DatabaseSync } from 'node:sqlite';
// const db = new DatabaseSync(':memory:', { allowExtension: true });
// db.loadExtension(extensionPath);
//
// Usage with better-sqlite3:
//
// import { extensionPath } from '@anthropic/crsqlite';
// import { extensionPath } from '@shards-lang/crsqlite';
// import Database from 'better-sqlite3';
// const db = new Database(':memory:');
// db.loadExtension(extensionPath);

import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";

const __dirname = dirname(fileURLToPath(import.meta.url));
export const extensionPath = join(__dirname, "dist", "crsqlite");
const require = createRequire(import.meta.url);

const PLATFORM_PACKAGES = {
"linux-x64": "@shards-lang/crsqlite-linux-x64",
"darwin-arm64": "@shards-lang/crsqlite-darwin-arm64",
"darwin-x64": "@shards-lang/crsqlite-darwin-x64",
"win32-x64": "@shards-lang/crsqlite-win32-x64",
};

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 {}
}
Comment on lines +37 to +45

Copilot AI Apr 12, 2026

Copy link

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.

// 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");
}
Comment on lines +47 to +59

Copilot AI Apr 12, 2026

Copy link

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.

export const extensionPath = resolve();
11 changes: 8 additions & 3 deletions core/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@anthropic/crsqlite",
"name": "@shards-lang/crsqlite",
"version": "0.1.0",
"description": "cr-sqlite loadable extension for Node.js — CRDT-based multi-master replication for SQLite",
"type": "module",
Expand All @@ -15,8 +15,7 @@
"files": [
"nodejs-helper.js",
"nodejs-helper.cjs",
"nodejs-helper.d.ts",
"dist/"
"nodejs-helper.d.ts"
],
"scripts": {
"build": "cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build && mkdir -p dist && cp build/crsqlite.* dist/",
Expand All @@ -26,6 +25,12 @@
"engines": {
"node": ">=22.5.0"
},
"optionalDependencies": {
"@shards-lang/crsqlite-linux-x64": "0.1.0",
"@shards-lang/crsqlite-darwin-arm64": "0.1.0",
"@shards-lang/crsqlite-darwin-x64": "0.1.0",
"@shards-lang/crsqlite-win32-x64": "0.1.0"
},
Comment on lines 15 to +33

Copilot AI Apr 12, 2026

Copy link

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.
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 2 additions & 0 deletions npm/crsqlite-darwin-arm64/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const { join } = require("node:path");
module.exports.path = join(__dirname, "crsqlite");
15 changes: 15 additions & 0 deletions npm/crsqlite-darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@shards-lang/crsqlite-darwin-arm64",
"version": "0.1.0",
"description": "cr-sqlite prebuilt binary for macOS arm64 (Apple Silicon)",
"os": ["darwin"],
"cpu": ["arm64"],
"main": "index.js",
"files": ["index.js", "crsqlite.dylib"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/shards-lang/cr-sqlite.git",
"directory": "npm/crsqlite-darwin-arm64"
}
}
2 changes: 2 additions & 0 deletions npm/crsqlite-darwin-x64/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const { join } = require("node:path");
module.exports.path = join(__dirname, "crsqlite");
15 changes: 15 additions & 0 deletions npm/crsqlite-darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@shards-lang/crsqlite-darwin-x64",
"version": "0.1.0",
"description": "cr-sqlite prebuilt binary for macOS x64 (Intel)",
"os": ["darwin"],
"cpu": ["x64"],
"main": "index.js",
"files": ["index.js", "crsqlite.dylib"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/shards-lang/cr-sqlite.git",
"directory": "npm/crsqlite-darwin-x64"
}
}
2 changes: 2 additions & 0 deletions npm/crsqlite-linux-x64/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const { join } = require("node:path");
module.exports.path = join(__dirname, "crsqlite");
15 changes: 15 additions & 0 deletions npm/crsqlite-linux-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@shards-lang/crsqlite-linux-x64",
"version": "0.1.0",
"description": "cr-sqlite prebuilt binary for Linux x64",
"os": ["linux"],
"cpu": ["x64"],
"main": "index.js",
"files": ["index.js", "crsqlite.so"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/shards-lang/cr-sqlite.git",
"directory": "npm/crsqlite-linux-x64"
}
}
2 changes: 2 additions & 0 deletions npm/crsqlite-win32-x64/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const { join } = require("node:path");
module.exports.path = join(__dirname, "crsqlite");
15 changes: 15 additions & 0 deletions npm/crsqlite-win32-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@shards-lang/crsqlite-win32-x64",
"version": "0.1.0",
"description": "cr-sqlite prebuilt binary for Windows x64",
"os": ["win32"],
"cpu": ["x64"],
"main": "index.js",
"files": ["index.js", "crsqlite.dll"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/shards-lang/cr-sqlite.git",
"directory": "npm/crsqlite-win32-x64"
}
}
Loading