Skip to content
Open
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
28 changes: 0 additions & 28 deletions .eslintrc.js

This file was deleted.

145 changes: 135 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,153 @@
name: Release

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish"
required: true
dry_run:
description: "Perform a dry run without publishing"
type: boolean
required: false
default: true

concurrency:
group: npm-publish-${{ github.repository }}
cancel-in-progress: false

jobs:
release:
name: Release workflow

runs-on: ubuntu-latest

permissions:
contents: read
id-token: write # Required for OIDC trusted publishing

steps:
- uses: actions/checkout@v4
- name: Validate GitHub release and tag exists
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="v${{ inputs.version }}"
echo "Looking for release with tag $TAG..."

RELEASE=$(gh release view "$TAG" --repo ${{ github.repository }} --json tagName,name 2>/dev/null)
if [ $? -ne 0 ]; then
echo "❌ No GitHub release found with tag $TAG"
exit 1
fi

RELEASE_NAME=$(echo "$RELEASE" | jq -r '.name')
if [ "$RELEASE_NAME" != "$TAG" ]; then
echo "❌ Release name '$RELEASE_NAME' does not match expected '$TAG'"
exit 1
fi

echo "✅ GitHub release '$RELEASE_NAME' confirmed"

- name: Checkout tag
uses: actions/checkout@v4
with:
ref: "v${{ inputs.version }}"
fetch-depth: 0

- name: Ensure tag commit is on master
run: |
if ! git branch -r --contains "$(git rev-parse HEAD)" | grep -q "origin/master"; then
echo "❌ Tag is not based on master branch"
exit 1
fi
echo "✅ Tag commit is on master"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org/'
node-version: "lts/*"
registry-url: "https://registry.npmjs.org/"

- name: Install
run: yarn --frozen-lockfile --non-interactive
- name: Enable Corepack
run: corepack enable

- name: Detect Yarn version
id: yarn-version
run: |
# Resolve the Yarn major version from the packageManager field in
# package.json if present, otherwise fall back to the installed version.
YARN_VERSION=$(node -p "
try {
const pm = require('./package.json').packageManager ?? '';
const match = pm.match(/^yarn@(\d+)/);
match ? match[1] : '';
} catch { '' }
")
if [ -z "$YARN_VERSION" ]; then
YARN_VERSION=$(yarn --version | cut -d. -f1)
fi
echo "major=$YARN_VERSION" >> "$GITHUB_OUTPUT"
echo "Detected Yarn major version: $YARN_VERSION"

- name: Validate version matches package.json
run: |
PKG_VERSION=$(node -p "require('./package.json').version")
INPUT_VERSION="${{ inputs.version }}"
if [ "$PKG_VERSION" != "$INPUT_VERSION" ]; then
echo "❌ Version mismatch: package.json has $PKG_VERSION but input was $INPUT_VERSION"
exit 1
fi
echo "✅ Version $PKG_VERSION confirmed"

- name: Install dependencies
run: |
if [ "${{ steps.yarn-version.outputs.major }}" = "1" ]; then
yarn install --frozen-lockfile --non-interactive
else
yarn install --immutable
fi

- name: Build package (if build script exists)
run: |
if node -e "process.exit(require('./package.json').scripts?.build ? 0 : 1)" 2>/dev/null; then
yarn build
else
echo "No build script found — skipping build step"
fi

- name: Publish (dry run)
if: ${{ inputs.dry_run }}
env:
# The npm CLI automatically detects the OIDC environment via
# ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN and
# handles the token exchange itself. NODE_AUTH_TOKEN must still be set
# (even if empty) to satisfy the .npmrc written by actions/setup-node,
# otherwise npm errors before it reaches OIDC auth.
NODE_AUTH_TOKEN: ""
run: npm publish --provenance --access public --dry-run

- name: Publish
run: yarn publish
if: ${{ !inputs.dry_run }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
NODE_AUTH_TOKEN: ""
run: npm publish --provenance --access public

- name: Verify published version
if: ${{ !inputs.dry_run }}
run: |
PACKAGE_NAME=$(node -p "require('./package.json').name")
EXPECTED_VERSION="${{ inputs.version }}"

echo "Waiting for npm propagation..."

for i in {1..10}; do
PUBLISHED_VERSION=$(npm view "$PACKAGE_NAME" version 2>/dev/null)
if [ "$PUBLISHED_VERSION" = "$EXPECTED_VERSION" ]; then
echo "✅ Version $PUBLISHED_VERSION confirmed on npm"
exit 0
fi
echo "Not visible yet (attempt $i)..."
sleep 15
done

echo "❌ Published version not visible after waiting"
exit 1
109 changes: 102 additions & 7 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,72 @@ name: Tests
on: [push, pull_request]

jobs:
build:
build-test-matrix:
name: Build test matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
node <<'EOF' >> $GITHUB_OUTPUT
const https = require('https');

function compareSemver(a, b) {
const pa = a.split('.').map(Number);
const pb = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if (pa[i] !== pb[i]) return pb[i] - pa[i];
}
return 0;
}

https.get('https://nodejs.org/dist/index.json', res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const releases = JSON.parse(data);

// Filter LTS only
const lts = releases
.filter(r => r.lts)
.map(r => r.version.replace(/^v/, ''));

// Group by major
const byMajor = {};
for (const v of lts) {
const major = v.split('.')[0];
if (!byMajor[major]) byMajor[major] = [];
byMajor[major].push(v);
}

// For each major, pick highest semver
const latestPerMajor = Object.entries(byMajor)
.map(([major, versions]) => {
versions.sort(compareSemver);
return versions[0]; // highest patch/minor
});

// Sort majors descending
latestPerMajor.sort(compareSemver);

// Take latest 4 majors
const final = latestPerMajor.slice(0, 4);

const matrix = { "node-version": final };
console.log(`matrix=${JSON.stringify(matrix)}`);
});
});
EOF

test:
needs: build-test-matrix
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [16.x, 18.x, 20.x, 22.x]
fail-fast: false
matrix: ${{ fromJson(needs.build-test-matrix.outputs.matrix) }}
name: Node ${{ matrix.node-version }}

steps:
- uses: actions/checkout@v4
Expand All @@ -18,13 +77,49 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'

- name: Enable Corepack
run: corepack enable

- name: Detect Yarn version
id: yarn-version
run: |
YARN_VERSION=$(node -p "
try {
const pm = require('./package.json').packageManager ?? '';
const match = pm.match(/^yarn@(\d+)/);
match ? match[1] : '';
} catch { '' }
")
if [ -z "$YARN_VERSION" ]; then
YARN_VERSION=$(yarn --version | cut -d. -f1)
fi
echo "major=$YARN_VERSION" >> "$GITHUB_OUTPUT"
echo "Detected Yarn major version: $YARN_VERSION"

- name: Install dependencies
run: yarn --frozen-lockfile --non-interactive --prefer-offline
run: |
if [ "${{ steps.yarn-version.outputs.major }}" = "1" ]; then
yarn install --frozen-lockfile --non-interactive --prefer-offline
else
yarn install --immutable
fi

- name: Lint
run: yarn lint

- name: Test
run: yarn test

- name: Lint
run: yarn lint
test-results:
name: Tests passed
needs: test
runs-on: ubuntu-latest
if: always()
steps:
- name: Check all matrix jobs passed
run: |
if [ "${{ needs.test.result }}" != "success" ]; then
echo "One or more test jobs failed"
exit 1
fi
64 changes: 64 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict';

const globals = require('globals');
const eslintJS = require('@eslint/js');

module.exports = [
eslintJS.configs.recommended,
{
languageOptions: {
sourceType: 'commonjs',
globals: {
...globals.browser,
...globals.node,
}
},
rules: {
'indent': ['error', 4],
'no-shadow': ['error'],
'no-var': ['error'],
'no-unused-vars': ['error', {
vars: 'all',
args: 'none',
caughtErrors: 'none',
ignoreRestSiblings: false,
ignoreUsingDeclarations: false,
reportUsedIgnorePattern: false,
}],
'operator-linebreak': ['error', 'after'],
'quote-props': ['error', 'consistent-as-needed'],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'space-before-function-paren': ['error', 'never'],
'strict': ['error', 'safe']
},
},
{
files: ['test/**/*.mjs'],
languageOptions: {
sourceType: 'module',
globals: {
afterEach: 'readonly',
beforeEach: 'readonly',
describe: 'readonly',
it: 'readonly',
},
},
rules: {
'no-unused-expressions': 0,
'comma-dangle': ['error', {
arrays: 'always-multiline',
objects: 'always-multiline',
imports: 'never',
exports: 'never',
functions: 'ignore',
}],
},
},
{
files: ['examples/**/*.js'],
rules: {
strict: 0,
},
},
];
Loading
Loading