Skip to content

Commit

Permalink
feat!: Add support for Edge Runtime (#29)
Browse files Browse the repository at this point in the history
* Add support for Edge Runtime

* fix non-node detection

* changes made based on review

* remove lock file

* remove node 14, add node 20 to CI matrix

* rename test:esm to test:webcrypto

* fix variable mismatch

* add auto publish for push to main, switch to node:test, add esm test

* Changes based on review

* BREAKING CHANGE: Add ESM output to support Edge / Browser runtimes

* feat: Adding ESM to support Edge / Browser runtimes

BREAKING CHANGE: Internals have been changed, new output has been created.
  • Loading branch information
agadzik committed May 15, 2023
1 parent bbbcc57 commit e1ab3a0
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 46 deletions.
44 changes: 32 additions & 12 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
name: CI

on:
- push
- pull_request
push:
branches: ['main']
pull_request:
branches: ['main']

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
test:
name: Node.js ${{ matrix.node-version }}
runs-on: ubuntu-latest
name: Node.js ${{ matrix.node }} on ${{ matrix.os }}
timeout-minutes: 5
strategy:
fail-fast: false
matrix:
node-version:
- 16
- 14
os: [ubuntu-latest]
node: [18, 20]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Checkout
uses: actions/checkout@v3
- name: Use pnpm
run: corepack enable pnpm && pnpm --version
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: yarn install
- run: yarn test
node-version: ${{ matrix.node }}
cache: 'pnpm'
- run: pnpm install
- run: pnpm test
- name: Maybe Release
if: matrix.os == 'ubuntu-latest' && matrix.node == 18 && github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }}
run: pnpm dlx semantic-release@19.0.5
14 changes: 14 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: PR
on:
pull_request:
types: [opened, edited, synchronize]
pull_request_target:
types: [opened, edited, synchronize]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@0b14f54ac155d88e12522156e52cb6e397745cfd
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v18.16.0
23 changes: 16 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,29 @@
"license": "MIT",
"description": "generates a cryptographically strong random uid",
"repository": "vercel/uid-promise",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"main": "lib/cjs/index.js",
"module": "lib/esm/index.js",
"types": "lib/cjs/index.d.ts",
"packageManager": "pnpm@7.25.0",
"engines": {
"node": ">=18"
},
"files": [
"lib"
],
"scripts": {
"build": "tsc",
"test": "tsc && ava"
"build": "pnpm build:cjs && pnpm build:esm",
"build:cjs": "tsc",
"build:esm": "tsc --module esnext --outDir ./lib/esm && echo '{ \"type\": \"module\" }' > ./lib/esm/package.json",
"test": "pnpm test:cjs && pnpm test:esm && pnpm test:webcrypto",
"test:cjs": "pnpm build:cjs && node --test test/test.js",
"test:esm": "pnpm build:esm && node --test test/test-esm.mjs",
"test:webcrypto": "pnpm build:cjs && node --test --experimental-global-webcrypto test/test.js"
},
"devDependencies": {
"@types/node": "16.6.1",
"ava": "3.15.0",
"@types/node": "20.1.3",
"prettier": "2.3.2",
"typescript": "4.3.5"
"typescript": "4.9.5"
},
"prettier": {
"singleQuote": true,
Expand Down
29 changes: 29 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions src/chars.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// Charset from github.com/coreh/uid2
const UIDCHARS =
export const UIDCHARS =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

export default UIDCHARS;
module.exports = UIDCHARS;
22 changes: 12 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
// Packages
import { randomBytes } from 'crypto';
// the file extension is needed for ESM
import { UIDCHARS } from './chars.js';

// Utilities
import UIDCHARS from './chars';

const uuid = (len: number) =>
new Promise<string>((resolve, reject) => {
export const uid = (len: number) =>
new Promise<string>(async (resolve, reject) => {
if (!Number.isInteger(len)) {
reject(
new TypeError('You must supply a length integer to `uid-promise`.'),
Expand All @@ -18,6 +15,14 @@ const uuid = (len: number) =>
return;
}

const isWebCryptoFuncDefined =
typeof globalThis.crypto?.getRandomValues === 'function';

const randomBytes = isWebCryptoFuncDefined
? // the file extensions are needed for ESM
await import('./web-random-bytes.js').then((mod) => mod.randomBytes)
: await import('./node-random-bytes.js').then((mod) => mod.randomBytes);

randomBytes(len, (err, buf) => {
if (err) {
return reject(err);
Expand All @@ -38,6 +43,3 @@ const uuid = (len: number) =>
resolve(str.join(''));
});
});

export default uuid;
module.exports = uuid;
4 changes: 4 additions & 0 deletions src/node-random-bytes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { RandomBytes } from './types';
import { randomBytes as nodeRandomBytes } from 'crypto';

export const randomBytes: RandomBytes = nodeRandomBytes;
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type RandomBytes = (
size: number,
callback?: (err: Error | null, buf: Uint8Array) => void,
) => Uint8Array;
22 changes: 22 additions & 0 deletions src/web-random-bytes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { RandomBytes } from './types';

export const randomBytes: RandomBytes = (size, callback) => {
if (size < 0 || size > 65536) {
throw new RangeError(
'The value of "size" is out of range. It must be >= 0 && <= 65536. Received ' +
size,
);
}

var array = new Uint8Array(size);
globalThis.crypto.getRandomValues(array);

if (!callback) {
return array;
}

// Mimic async behavior with setTimeout
setTimeout(() => callback(null, array), 0);

return new Uint8Array();
};
34 changes: 34 additions & 0 deletions test/test-esm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Packages
import test from 'node:test';
import assert from 'node:assert';

// Utilities
import {UIDCHARS} from '../lib/esm/chars.js';
import {uid} from '../lib/esm/index.js';

test('should be correct length', async () => {
const len = 1 + Math.floor(Math.random() * 1000);
const u = await uid(len);
assert.strictEqual(u.length, len);
});

test('should contain only UIDCHARS', async () => {
const u = await uid(20);
u.split('').forEach((l) => assert.notStrictEqual(UIDCHARS.indexOf(l), -1));
});

test('should throw if no integer supplied', async () => {
uid()
.then(console.log)
.catch((err) => {
assert.strictEqual(err instanceof Error, true);
});
});

test('should throw if integer is not positive value', async () => {
uid(0)
.then(console.log)
.catch((err) => {
assert.strictEqual(err instanceof Error, true);
});
});
23 changes: 12 additions & 11 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
// Packages
const test = require('ava');
const test = require('node:test');
const assert = require('node:assert')

// Utilities
const UIDCHARS = require('../lib/chars');
const uid = require('../lib');
const {UIDCHARS} = require('../lib/cjs/chars');
const { uid } = require('../lib/cjs');

test('should be correct length', async (t) => {
test('should be correct length', async () => {
const len = 1 + Math.floor(Math.random() * 1000);
const u = await uid(len);
t.is(u.length, len);
assert.strictEqual(u.length, len);
});

test('should contain only UIDCHARS', async (t) => {
test('should contain only UIDCHARS', async () => {
const u = await uid(20);
u.split('').forEach((l) => t.not(UIDCHARS.indexOf(l), -1));
u.split('').forEach((l) => assert.notStrictEqual(UIDCHARS.indexOf(l), -1));
});

test('should throw if no integer supplied', async (t) => {
test('should throw if no integer supplied', async () => {
uid()
.then(console.log)
.catch((err) => {
t.is(err instanceof Error, true);
assert.strictEqual(err instanceof Error, true);
});
});

test('should throw if integer is not positive value', async (t) => {
test('should throw if integer is not positive value', async () => {
uid(0)
.then(console.log)
.catch((err) => {
t.is(err instanceof Error, true);
assert.strictEqual(err instanceof Error, true);
});
});
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"compilerOptions": {
"declaration": true,
"esModuleInterop": true,
"lib": ["esnext"],
"lib": ["esnext", "DOM"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "./lib",
"outDir": "./lib/cjs",
"types": ["node"],
"strict": true,
"target": "esnext",
Expand Down

0 comments on commit e1ab3a0

Please sign in to comment.