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
5 changes: 5 additions & 0 deletions .changeset/bright-rivers-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"uniku": major
---

Finalize the v1 public API: use only `uniku/cuid/v2` with `cuidv2`, replace second-based `secs` options with millisecond-based `msecs`, replace UUID v7 and TypeID `seq` with `counter`, and use `length` for Nanoid's object-form option. Existing identifier data remains valid; see the v1 migration guide for mechanical before-and-after examples.
12 changes: 12 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mode": "pre",
"tag": "rc",
"initialVersions": {
"@uniku/docs": "0.0.0",
"@uniku/examples": "0.0.1",
"@uniku/cli": "0.7.0",
"uniku": "0.6.0",
"@uniku/e2e-cloudflare": "0.0.5"
},
"changesets": ["bright-rivers-release", "quiet-aliases-retire"]
}
5 changes: 5 additions & 0 deletions .changeset/quiet-aliases-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"uniku": patch
---

Remove the deprecated `secs`, `seq`, and Nanoid object-form `size` aliases after their final migration release in `0.6.0`. Use `msecs`, `counter`, and `length` instead. `CONFLICTING_OPTIONS` is also removed from the v1 error-code catalog because no supported option pair conflicts after these aliases are gone.
114 changes: 114 additions & 0 deletions .github/workflows/publish-uniku-v1-codemod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: Publish uniku v1 Codemod

on:
push:
tags:
- "uniku-v1-codemod@*"

concurrency:
group: publish-uniku-v1-codemod
cancel-in-progress: false

permissions:
contents: read

jobs:
publish:
name: Publish Registry package
if: github.repository == 'jkomyno/uniku'
runs-on: ubuntu-latest
timeout-minutes: 15
environment: codemod-registry
permissions:
contents: read
id-token: write
steps:
- name: Checkout approved tag source
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.ref }}

- name: Verify immutable source and version
id: source
shell: bash
run: |
set -euo pipefail

expected_workflow_ref="$GITHUB_REPOSITORY/.github/workflows/publish-uniku-v1-codemod.yml@"
if [[ "$GITHUB_WORKFLOW_REF" != "$expected_workflow_ref"* ]]; then
echo "::error::Unexpected workflow identity: $GITHUB_WORKFLOW_REF"
exit 1
fi
if [[ "$GITHUB_REF" != refs/tags/uniku-v1-codemod@* ]]; then
echo "::error::Unexpected publication ref: $GITHUB_REF"
exit 1
fi

tag_version="${GITHUB_REF_NAME#uniku-v1-codemod@}"
package_version="$(jq -r '.version' codemods/v1/package.json)"
manifest_version="$(awk -F '"' '/^version:/ { print $2; exit }' codemods/v1/codemod.yaml)"
if [[ ! "$tag_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Tag version is not an exact stable semantic version: $tag_version"
exit 1
fi
if [[ "$tag_version" != "$package_version" ]] || [[ "$tag_version" != "$manifest_version" ]]; then
echo "::error::Tag, package, and manifest versions must match"
exit 1
fi

git fetch --no-tags origin main:refs/remotes/origin/main
tag_source_sha="$(git rev-parse "$GITHUB_REF^{commit}")"
approved_source_sha="$(git rev-parse "$GITHUB_SHA^{commit}")"
checkout_source_sha="$(git rev-parse HEAD)"
main_source_sha="$(git rev-parse origin/main)"
if [[ "$tag_source_sha" != "$approved_source_sha" || "$checkout_source_sha" != "$approved_source_sha" || "$main_source_sha" != "$approved_source_sha" ]]; then
echo "::error::Tag, checkout, approved source, and current origin/main must identify the same commit"
exit 1
fi

echo "sha=$approved_source_sha" >> "$GITHUB_OUTPUT"
echo "version=$tag_version" >> "$GITHUB_OUTPUT"

- name: Setup Node.js
uses: ./.github/actions/setup-node

- name: Verify locked Codemod CLI
run: test "$(pnpm --filter @jkomyno/uniku-v1 exec codemod --version)" = "codemod 1.14.0"

- name: Validate package
run: |
pnpm --filter @jkomyno/uniku-v1 typecheck
pnpm --filter @jkomyno/uniku-v1 test
pnpm --filter @jkomyno/uniku-v1 workflow:validate

- name: Request Codemod OIDC token
id: oidc
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const token = await core.getIDToken('https://codemod.com')
core.setSecret(token)
core.setOutput('token', token)

- name: Publish exact Registry version
working-directory: codemods/v1
env:
CODEMOD_AUTH_TOKEN: ${{ steps.oidc.outputs.token }}
run: pnpm exec codemod publish . --disable-analytics

- name: Record publication evidence
env:
PACKAGE_VERSION: ${{ steps.source.outputs.version }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
SOURCE_SHA: ${{ steps.source.outputs.sha }}
run: |
{
echo "### Codemod Registry publication"
echo
echo "- Package: \`@jkomyno/uniku-v1@$PACKAGE_VERSION\`"
echo "- Tag: \`$GITHUB_REF_NAME\`"
echo "- Source SHA: \`$SOURCE_SHA\`"
echo "- Workflow run: $RUN_URL"
} >> "$GITHUB_STEP_SUMMARY"
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pnpm changeset # Create changeset for versioning
- **Monorepo**: pnpm workspaces + Turborepo
- **Toolchain**: `mise.toml` is the canonical runtime version source; shared CI setup reads its Node.js, Bun, Deno, and compatibility pins, while compatibility jobs may switch runtimes after dependency installation
- **Published packages**: `packages/uniku` (ID generators) and `packages/cli` (the `uniku` command)
- **Registry codemods**: `codemods/v1` is a private workspace published only through the Codemod Registry; verify it with `pnpm --filter @jkomyno/uniku-v1 test` and `pnpm --filter @jkomyno/uniku-v1 workflow:validate`
- **Release scope**: Changesets versions and tags only the published packages; keep private workspaces such as `examples` and the Cloudflare E2E workspace out of generated versions, tags, and changelogs
- **Runnable docs examples**: `examples` contains Bun tests for the typechecked Hono, Drizzle v1, and Effect v4 examples rendered by the docs site
- **E2E workspace**: Cloudflare Workers tests live under `packages/uniku/__tests__/e2e/runtimes/cloudflare`
Expand Down
5 changes: 2 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ packages/
src/
common/
bytes.ts # Byte manipulation (increment, timestamp writing)
random-pool.ts # Thread-safe random byte pooling (for CUID2)
random.ts # Simple random pool for UUID/ULID/KSUID
uuid/
v4.ts # UUID v4 implementation
Expand All @@ -133,8 +132,8 @@ packages/
ulid/
ulid.ts # ULID implementation
crockford.ts # Crockford Base32 encoding
cuid2/
cuid2.ts # CUID2 implementation (SHA3-512 based)
cuid/
v2.ts # CUID v2 implementation (SHA3-512 based)
nanoid/
nanoid.ts # Nanoid implementation
ksuid/
Expand Down
5 changes: 2 additions & 3 deletions apps/docs/content/docs/guides/performance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,8 @@ Every entry point is independently importable and tree-shakeable: importing `uni
| `uniku/xid` | ~1.9 KB |
| `uniku/nanoid` | ~1.2 KB |
| `uniku/uuid/v4` | ~1.2 KB |
| `uniku/cuid/v2` | ~1015 B |
| `uniku/cuid2` | ~1.0 KB |
| `uniku/errors` | ~184 B |
| `uniku/cuid/v2` | ~1.0 KB |
| `uniku/errors` | ~445 B |
| `uniku/generators` | ~101 B |

The CUID v2 entry point imports SHA3-512 from `@noble/hashes`, uniku's one runtime dependency; the size above excludes that external dependency's own weight.
Expand Down
47 changes: 46 additions & 1 deletion apps/docs/content/docs/migration/v1.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ uniku v1 keeps its canonical identifier formats stable while finalizing its pre-

## Move CUID v2 to the versioned entry point

`uniku/cuid2` is a pre-v1 alias. Use the canonical versioned import before upgrading:
V1 removes the pre-v1 `uniku/cuid2` alias. Use the canonical versioned import:

```diff
- import { cuid2 } from 'uniku/cuid2'
Expand All @@ -17,6 +17,51 @@ uniku v1 keeps its canonical identifier formats stable while finalizing its pre-

The generator behavior is the same. This is an import migration, not a data migration: existing CUID v2 values remain valid.

## Convert second-based timestamp options to milliseconds

KSUID, ObjectID, and XID now expose only the shared `msecs` timestamp option. Convert an existing Unix-seconds value before passing it:

```diff
- const ksuidValue = ksuid({ secs })
- const objectIdValue = objectid({ secs })
- const xidValue = xid({ secs })
+ const ksuidValue = ksuid({ msecs: secs * 1000 })
+ const objectIdValue = objectid({ msecs: secs * 1000 })
+ const xidValue = xid({ msecs: secs * 1000 })
```

The conversion is `msecs = secs * 1000`; these formats still store whole seconds, so equivalent deterministic inputs produce the same identifier data.

## Rename UUID v7 and TypeID counters

The pre-v1 `seq` spelling is now `counter` for UUID v7 and for TypeID, which inherits the UUID v7 options:

```diff
- const uuid = uuidv7({ seq: 42 })
- const userId = typeid('user', { seq: 42 })
+ const uuid = uuidv7({ counter: 42 })
+ const userId = typeid('user', { counter: 42 })
```

Only the option name changes. The counter value is packed identically.

## Rename Nanoid's object-form length

Use `length` instead of `size` in Nanoid's object form:

```diff
- const id = nanoid({ alphabet: '0123456789abcdef', size: 12 })
+ const id = nanoid({ alphabet: '0123456789abcdef', length: 12 })
```

The positional `nanoid(number)` overload is unchanged:

```ts
const id = nanoid(12)
```

These migrations change imports and option names, not identifier formats. Existing persisted UUID v7, TypeID, KSUID, ObjectID, XID, Nanoid, and CUID v2 values require no data migration.

## Update error-code matches

Pre-v1 error codes included the generator name. V1 uses strategy-agnostic codes and reports the generator separately through `error.strategy`:
Expand Down
1 change: 0 additions & 1 deletion apps/docs/content/docs/reference/errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ The following codes form the v1 public contract:
| Code | Error class(es) | Strategies | Meaning |
| --- | --- | --- | --- |
| `TIMESTAMP_OUT_OF_RANGE` | `InvalidInputError`, `ParseError` | `uuid`, `ulid`, `typeid`, `ksuid`, `objectid`, `tsid`, `xid` | A timestamp option is not an integer in the format's range, or a decoded timestamp overflows. |
| `CONFLICTING_OPTIONS` | `InvalidInputError` | `uuid`, `typeid`, `nanoid`, `ksuid`, `objectid`, `xid` | Mutually exclusive compatibility and canonical options were supplied together. |
| `COUNTER_OUT_OF_RANGE` | `InvalidInputError` | `uuid`, `typeid`, `objectid`, `tsid`, `xid` | A counter option is outside the format's supported bit width. |
| `NODE_OUT_OF_RANGE` | `InvalidInputError` | `tsid` | A TSID node value is outside the configured node-bit range. |
| `NODE_BITS_OUT_OF_RANGE` | `InvalidInputError` | `tsid` | A TSID node-bit allocation is invalid. |
Expand Down
8 changes: 3 additions & 5 deletions apps/docs/scripts/generate-api-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ type GeneratorDefinition = {
examples: Record<string, ApiExample>
id: string
exportName: string
sourceExportName?: string
sourcePath: string
typeName: string
}
Expand Down Expand Up @@ -168,9 +167,8 @@ const generators: GeneratorDefinition[] = [
},
id: 'cuid-v2',
exportName: 'cuidv2',
sourceExportName: 'cuid2',
sourcePath: 'packages/uniku/src/cuid2/cuid2.ts',
typeName: 'Cuid2',
sourcePath: 'packages/uniku/src/cuid/v2.ts',
typeName: 'CuidV2',
},
{
examples: {
Expand Down Expand Up @@ -451,7 +449,7 @@ function generate(): Record<string, GeneratorApi> {

const typeAlias = findTypeAlias(sourceFile, generator.typeName)
apis[generator.id] = {
description: findExportDescription(sourceFile, generator.sourceExportName ?? generator.exportName),
description: findExportDescription(sourceFile, generator.exportName),
members: membersFor(program, typeAlias, generator.exportName, generator.examples, sourceFile),
sourcePath: relative(repositoryRoot, absoluteSourcePath).replaceAll('\\', '/'),
}
Expand Down
42 changes: 3 additions & 39 deletions apps/docs/src/generated/api-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,6 @@ export const generatorApis: Record<GeneratorId, GeneratorApi> = {
"name": "counter",
"optional": true,
"type": "number"
},
{
"description": "Deprecated alias for `counter`; unsigned 32-bit sequence value.",
"name": "seq",
"optional": true,
"type": "number"
}
],
"typeName": "UuidV7Options"
Expand Down Expand Up @@ -425,12 +419,6 @@ export const generatorApis: Record<GeneratorId, GeneratorApi> = {
"name": "counter",
"optional": true,
"type": "number"
},
{
"description": "Deprecated alias for `counter`; unsigned 32-bit sequence value.",
"name": "seq",
"optional": true,
"type": "number"
}
],
"typeName": "TypeidOptions"
Expand Down Expand Up @@ -581,12 +569,12 @@ export const generatorApis: Record<GeneratorId, GeneratorApi> = {
"type": "Uint8Array"
}
],
"typeName": "Cuid2Options"
"typeName": "CuidV2Options"
},
"signatures": [
{
"description": "Generate a CUID v2 string.",
"text": "cuidv2(options?: Cuid2Options): string"
"text": "cuidv2(options?: CuidV2Options): string"
}
]
},
Expand All @@ -604,7 +592,7 @@ export const generatorApis: Record<GeneratorId, GeneratorApi> = {
]
}
],
"sourcePath": "packages/uniku/src/cuid2/cuid2.ts"
"sourcePath": "packages/uniku/src/cuid/v2.ts"
},
"nanoid": {
"description": "Generate a URL-friendly unique string ID. Nanoid is a tiny, secure, URL-friendly unique string ID generator. It uses a URL-safe alphabet (A-Za-z0-9_-) and generates 21-character IDs by default with 126 bits of entropy. Unlike UUID v7 or ULID, nanoid is NOT time-ordered. Use it for: - URL shorteners - Session tokens - Invite codes - Any case where you need short, random IDs",
Expand Down Expand Up @@ -634,12 +622,6 @@ export const generatorApis: Record<GeneratorId, GeneratorApi> = {
"name": "length",
"optional": true,
"type": "number"
},
{
"description": "Deprecated alias for `length`.",
"name": "size",
"optional": true,
"type": "number"
}
],
"typeName": "NanoidOptions"
Expand Down Expand Up @@ -697,12 +679,6 @@ export const generatorApis: Record<GeneratorId, GeneratorApi> = {
"name": "msecs",
"optional": true,
"type": "number"
},
{
"description": "Deprecated alias for `msecs`; timestamp in seconds since the Unix epoch.",
"name": "secs",
"optional": true,
"type": "number"
}
],
"typeName": "KsuidOptions"
Expand Down Expand Up @@ -826,12 +802,6 @@ export const generatorApis: Record<GeneratorId, GeneratorApi> = {
"optional": true,
"type": "number"
},
{
"description": "Deprecated alias for `msecs`; timestamp in seconds since the Unix epoch.",
"name": "secs",
"optional": true,
"type": "number"
},
{
"description": "24-bit counter value (0 to 0xFFFFFF).",
"name": "counter",
Expand Down Expand Up @@ -1132,12 +1102,6 @@ export const generatorApis: Record<GeneratorId, GeneratorApi> = {
"optional": true,
"type": "number"
},
{
"description": "Deprecated alias for `msecs`; Unix timestamp in seconds.",
"name": "secs",
"optional": true,
"type": "number"
},
{
"description": "24-bit counter. Explicit values do not consume shared state.",
"name": "counter",
Expand Down
6 changes: 6 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
"!apps/docs/.source",
"!apps/docs/src/generated",
"!apps/docs/src/routeTree.gen.ts",
// Codemod fixtures intentionally contain pre-migration and unsupported syntax.
"!codemods/v1/tests/**/input.*",
"!codemods/v1/tests/**/expected.*",
"!codemods/v1/tests/**/metrics.json",
"!codemods/v1/tests/integration/consumer",
"!codemods/v1/tests/integration/expected",
"!**/.turbo",
"!public",
// Read-only source clone of Effect (git submodule) — never lint.
Expand Down
Loading
Loading