diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..988baa3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 24.x + - uses: dtolnay/rust-toolchain@stable + - run: npm ci + - run: npm run build + - run: npm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..33e2e75 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,107 @@ +name: Publish to npm + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + branches: [main] + +jobs: + check: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + should_publish: ${{ steps.v.outputs.should_publish }} + version: ${{ steps.v.outputs.version }} + tag: ${{ steps.v.outputs.tag }} + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 24.x + - name: Check version against npm registry + id: v + run: | + NAME=$(node -p "require('./package.json').name") + VER=$(node -p "require('./package.json').version") + echo "version=$VER" >> "$GITHUB_OUTPUT" + if [[ "$VER" == *-* ]]; then + echo "tag=beta" >> "$GITHUB_OUTPUT" + else + echo "tag=latest" >> "$GITHUB_OUTPUT" + fi + if npm view "${NAME}@${VER}" version 2>/dev/null; then + echo "should_publish=false" >> "$GITHUB_OUTPUT" + echo "Version $VER already published, skipping." + else + echo "should_publish=true" >> "$GITHUB_OUTPUT" + echo "New version detected: $VER" + fi + + build: + needs: check + if: needs.check.outputs.should_publish == 'true' + strategy: + fail-fast: true + matrix: + include: + - { triple: linux-x64-gnu, os: ubuntu-latest, target: x86_64-unknown-linux-gnu } + - { triple: linux-arm64-gnu, os: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu } + - { triple: darwin-x64, os: macos-13, target: x86_64-apple-darwin } + - { triple: darwin-arm64, os: macos-14, target: aarch64-apple-darwin } + - { triple: win32-x64-msvc, os: windows-latest, target: x86_64-pc-windows-msvc } + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 24.x + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - run: npm ci + - name: Build native addon + run: npm run build + - name: Name artifact + shell: bash + run: mv index.node "index-${{ matrix.triple }}.node" + - uses: actions/upload-artifact@v4 + with: + name: bin-${{ matrix.triple }} + path: index-${{ matrix.triple }}.node + if-no-files-found: error + + publish: + needs: [check, build] + runs-on: ubuntu-latest + permissions: + contents: write + # id-token: write enables npm trusted publishing (OIDC). The registry + # exchanges this short-lived token for a scoped upload credential, so no + # long-lived NPM_TOKEN secret is needed. Provenance is generated too. + id-token: write + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 24.x + registry-url: https://registry.npmjs.org + - uses: actions/download-artifact@v4 + with: + path: artifacts + - name: Collect binaries into package root + run: find artifacts -name 'index-*.node' -exec cp {} . \; + - name: Publish to npm + run: npm publish --provenance --access public --tag ${{ needs.check.outputs.tag }} + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${{ needs.check.outputs.version }}" + PRERELEASE="" + if [[ "$VERSION" == *-* ]]; then + PRERELEASE="--prerelease" + fi + gh release create "v${VERSION}" --generate-notes $PRERELEASE diff --git a/README.md b/README.md index c2a5d37..b7fc697 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,30 @@ $ npm run build This command uses the [@neon-rs/cli](https://www.npmjs.com/package/@neon-rs/cli) utility to assemble the binary Node addon from the output of `cargo`. +## Using yn + +`yn` ships prebuilt binaries for the common desktop platforms +(`linux-x64-gnu`, `linux-arm64-gnu`, `darwin-x64`, `darwin-arm64`, +`win32-x64-msvc`) bundled in a single package. A small loader picks the binary +matching the host at runtime, so consumers just import the package: + +```js +import { applyUpdates } from '@y-crdt/yn' +// or, CommonJS: +const { applyUpdates } = require('@y-crdt/yn') +``` + ## Exploring yn -After building yn, you can explore its exports at the Node console: +After building yn, you can explore its exports at the Node console. When run from +the project directory the loader falls back to the locally built `index.node`: ```sh $ npm i $ npm run build $ node -> require('.').hello('node') -'hello node' +> require('.').applyUpdates(true, []) +Uint8Array(2) [ 0, 0 ] ``` ## Available Scripts @@ -79,7 +93,9 @@ yn/ | `README.md` | This file. | | `src/` | The directory tree containing the Rust source code for the project. | | `lib.rs` | Entry point for the Rust source code. | -| `index.node` | The main module, a [Node addon](https://nodejs.org/api/addons.html) generated by the build and pointed to by `"main"` in `package.json`. | +| `index.node` | A locally built [Node addon](https://nodejs.org/api/addons.html) for the host platform. In the published package this is replaced by per-platform `index-.node` binaries. | +| `load.cjs` | Runtime loader (the `"main"` entry) that selects the correct prebuilt binary for the host platform. | +| `index.mjs` | ESM entry point that re-exports the bindings from `load.cjs`. | | `package.json` | The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. | | `target/` | Binary artifacts generated by the Rust build. | diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..a34eb12 --- /dev/null +++ b/index.mjs @@ -0,0 +1,8 @@ +// ESM entry point. Native `.node` addons must be loaded through `require`, so we +// bridge to the CommonJS loader and re-export its bindings. +import { createRequire } from 'node:module' + +const binding = createRequire(import.meta.url)('./load.cjs') + +export const applyUpdates = binding.applyUpdates +export default binding diff --git a/load.cjs b/load.cjs new file mode 100644 index 0000000..d9a4160 --- /dev/null +++ b/load.cjs @@ -0,0 +1,40 @@ +'use strict' + +// Runtime loader for the @y-crdt/yn native addon. +// +// The published package is a single "fat" tarball that bundles one prebuilt +// `index-.node` binary per supported platform. This loader selects the +// binary matching the host platform/arch. When developing locally it falls back +// to a freshly built `index.node` (the output of `npm run build`). + +const { platform, arch } = process + +function targetTriple () { + if (platform === 'darwin' && arch === 'arm64') return 'darwin-arm64' + if (platform === 'darwin' && arch === 'x64') return 'darwin-x64' + if (platform === 'linux' && arch === 'x64') return 'linux-x64-gnu' + if (platform === 'linux' && arch === 'arm64') return 'linux-arm64-gnu' + if (platform === 'win32' && arch === 'x64') return 'win32-x64-msvc' + return null +} + +function load () { + const triple = targetTriple() + if (triple) { + try { + return require(`./index-${triple}.node`) + } catch (_) { + /* fall through to local dev build */ + } + } + try { + return require('./index.node') + } catch (_) { + /* fall through to error */ + } + throw new Error( + `@y-crdt/yn: no prebuilt binary available for ${platform}-${arch}` + ) +} + +module.exports = load() diff --git a/package-lock.json b/package-lock.json index 424d68c..acabe8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "yn", + "name": "@y-crdt/yn", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "yn", + "name": "@y-crdt/yn", "version": "0.1.0", "license": "ISC", "devDependencies": { diff --git a/package.json b/package.json index 412d858..0d33988 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,20 @@ "name": "@y-crdt/yn", "version": "0.1.0", "description": "nodejs bindings to yrs", - "main": "index.node", + "main": "./load.cjs", + "exports": { + ".": { + "import": "./index.mjs", + "require": "./load.cjs" + }, + "./package.json": "./package.json" + }, + "files": [ + "load.cjs", + "index.mjs", + "index-*.node", + "README.md" + ], "scripts": { "test": "cargo test", "cargo-build": "cargo build --message-format=json-render-diagnostics > cargo.log", @@ -16,6 +29,10 @@ }, "author": "Bartosz Sypytkowski", "license": "ISC", + "repository": { + "type": "git", + "url": "git+https://github.com/y-crdt/yn.git" + }, "keywords": [ "y-crdt", "yjs",