Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4bd936d
feat(drivers): enhance drivers with typed options for get, set, remov…
schplitt Aug 24, 2025
54244e3
fix: lint
schplitt Aug 24, 2025
5ace236
fix: correct get and set options
schplitt Aug 24, 2025
561aee7
feat(types): implement WithSafeName utility type
schplitt Aug 24, 2025
c6df26a
fix: lint
schplitt Aug 24, 2025
b094800
feat(types): implement SafeName utility type and refactor getSafeName…
schplitt Aug 24, 2025
c9e65ed
chore: apply automated updates
autofix-ci[bot] Aug 24, 2025
ed3bff9
refactor(utils): move getSafeName function to gen-drivers
schplitt Aug 24, 2025
79f7c7b
chore: apply automated updates
autofix-ci[bot] Aug 24, 2025
5a5b77a
refactor(types): use only driver name casing for driver options
schplitt Aug 27, 2025
0778cfb
refactor: use generated `GetOptions` inside deno driver
schplitt Aug 27, 2025
60ba7d7
docs: add guide for creating drivers with typed options
schplitt Aug 27, 2025
ef7636a
chore: apply automated updates
autofix-ci[bot] Aug 27, 2025
995a569
chore: apply automated updates (attempt 2/3)
autofix-ci[bot] Aug 27, 2025
a8ba05f
docs: better structure
schplitt Aug 27, 2025
39455f3
refactor(types): rename driver options to use safe names and add clea…
schplitt Aug 28, 2025
9558f33
docs: add contributing guide for developing drivers in Unstorage
schplitt Aug 28, 2025
05a9600
docs: add clear options
schplitt Aug 28, 2025
4cf481f
docs: add general contribution guide
schplitt Aug 28, 2025
46293c6
chore: apply automated updates
autofix-ci[bot] Aug 28, 2025
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
182 changes: 182 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Contribution Guide

<!-- https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors -->

> All contributors lead the growth of Unstorage - including you!

## Discussions

You can involve in discussions using:

- [GitHub Discussions](https://github.com/unjs/unstorage/discussions)

## Contribute to the Code

> [!IMPORTANT]
> Please discuss your ideas with the maintainers before opening a pull request.

### Local Development

- Clone the [`unjs/unstorage`](https://github.com/unjs/unstorage) git repository.
- Install the latest LTS version of [Node.js](https://nodejs.org/en/) (v22+).
- Enable [corepack](https://github.com/nodejs/corepack) using `corepack enable` (run `npm i -g corepack` if it's not available).
- Install dependencies using `pnpm install`.
- Run the generation script after creating a new driver using `pnpm generate`.
- Build the project using `pnpm build`.
- Add, modify, and run tests using `pnpm test`.

## Reporting Issues

You might encounter a bug while using Unstorage.

Although we aim to resolve all known issues, new bugs can emerge over time. Your bug report helps us find and fix them faster — even if you're unable to fix the underlying code yourself.

Here’s how to report a bug effectively:

### Ensure It's a Bug

Sometimes what seems like a bug may actually be expected behavior or a missing feature. Make sure you’re reporting an actual bug by creating a minimal unstorage project and reducing scope.

### Create a Minimal Reproduction

Please create a minimal reproduction using the starter templates or a simple repository.

Sometimes, bugs originate from another layer — not Unstorage itself. A minimal reproduction helps identify the source and speeds up debugging.

If your bug involves a higher-level framework, please report it there. Maintainers will help narrow it down to an Unstorage-level issue if needed.

### Search Existing Issues and Discussions

Before creating a new issue, search existing [issues](https://github.com/unjs/unstorage/issues) and [discussions](https://github.com/unjs/unstorage/discussions) to see if your bug has already been reported.

If it has already been reported:

- Add a 👍 reaction to the original post (instead of commenting "me too" or "when will it be fixed").
- If you can provide additional context or a better/smaller reproduction, please share it.

> [!NOTE]
> If the issue seems related but different or old or already closed, it's **better to open a new issue**. Maintainers will merge similar issues if needed.

## Contributing: Developing Drivers for Unstorage

This guide explains how to develop a custom driver for `unstorage`, including naming conventions, typing options, and best practices. For a practical example, see the [`deno-kv` driver](https://github.com/unjs/unstorage/blob/main/src/drivers/deno-kv.ts).

## 1. Naming Your Driver and Options

- **Driver file:** Use a clear, kebab-case name (e.g., `my-custom-driver.ts`).
- **Options interface:** Name it as `MyCustomOptions` (PascalCase, ends with `Options`).
- **Driver interface:** Name it as `MyCustomDriver` (PascalCase, ends with `Driver`).
- **Driver name constant:** It is recommended to use a `DRIVER_NAME` constant as the driver name in the implementation and should match the pattern in [`src/drivers`](https://github.com/unjs/unstorage/tree/main/src/drivers) (see e.g. `deno-kv.ts`, `fs.ts`, `redis.ts`). The file name should match the `DRIVER_NAME` constant.
- **File name transformation:** The file name is used to determine how to access the driver's options in the global options types. Internally, a transformation (is applied to the file name for type access. For example, `deno-kv` becomes `denoKV`, so you would access options as `opts.denoKV`.

## 2. Typing Driver Options

Define an options interface for your driver. This describes the configuration users can pass when initializing the driver.

```ts
// my-custom-driver.ts
export interface MyCustomOptions {
path: string;
ttl?: number;
}
```

## 3. Typing Driver Methods

Define a driver interface with the suffix `Driver`. This interface should have the following properties:

- `getOptions`: Type for options passed to `getItem`.
- `setOptions`: Type for options passed to `setItem`.
- `removeOptions`: Type for options passed to `removeItem`.
- `listOptions`: Type for options passed to `getKeys`.
- `clearOptions`: Type for options passed to `clear` (if your driver supports it).

Example:

```ts
export interface MyCustomDriver {
getOptions: { raw?: boolean };
setOptions: { foo?: string };
removeOptions: {};
listOptions: { prefix?: string };
clearOptions: { deep?: boolean };
}
```

## 4. Implementing the Driver

Use the `defineDriver` helper, passing your options and driver types:

```ts
import { defineDriver } from "unstorage";
import type { MyCustomOptions, MyCustomDriver } from "./my-custom-driver";
import type {
GetOptions,
SetOptions,
RemoveOptions,
ListOptions,
ClearOptions,
} from "../types";

const DRIVER_NAME = "my-custom-driver";

export default defineDriver<MyCustomOptions, unknown, MyCustomDriver>(
(options) => {
return {
name: DRIVER_NAME,
async getItem(key, opts: GetOptions) {
// Get the raw option
const raw = opts?.["my-custom-driver"]?.raw;
/** Implementation */
},
async setItem(key, value, opts: SetOptions) {
/** Implementation */
},
async removeItem(key, opts: RemoveOptions) {
/** Implementation */
},
async getKeys(base, opts: ListOptions) {
/** Implementation */
},
async clear(base, opts: ClearOptions) {
/** Implementation */
},
// ...other methods...
};
}
);
```

Take a look at the [deno driver](https://github.com/unjs/unstorage/blob/main/src/drivers/deno-kv.ts) for a complete implementation example with typed options.

## 5. Generating and Using Types Globally

After defining your driver and its types:

1. **Run the code generation script:**

```sh
pnpm gen-drivers
```

This will update the generated types so your driver's options are available inside the `GetOptions`, `SetOptions`, `RemoveOptions`, and `ListOptions` types.

2. **Use the generated types** in your code:

```ts
import type { SetOptions } from "unstorage";

defineDriver((options) => {
return {
async setItem(key, value, opts: SetOptions) {
// opts is typed for your driver
},
};
});
```

Some important notes:

- Always use the `Driver` and `Options` suffixes for your types.
- Document your options and method behaviors.
- Document if your driver supports the [common options](https://github.com/unjs/unstorage/blob/src/types.ts#L27)
79 changes: 78 additions & 1 deletion scripts/gen-drivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const drivers: {
subpath: string;
optionsTExport?: string;
optionsTName?: string;
driverOptionsExport?: string;
driverName?: string;
}[] = [];

for (const entry of driverEntries) {
Expand All @@ -31,25 +33,35 @@ for (const entry of driverEntries) {
const fullPath = join(driversDir, `${name}.ts`);

const contents = await readFile(fullPath, "utf8");
const optionsTExport = findTypeExports(contents).find((type) =>
const typeExports = findTypeExports(contents);
const optionsTExport = typeExports.find((type) =>
type.name?.endsWith("Options")
)?.name;

const driverOptionsExport = typeExports.find((type) =>
type.name?.endsWith("Driver")
)?.name;

const safeName = camelCase(name)
.replace(/kv/i, "KV")
.replace("localStorage", "localstorage");

const names = [...new Set([name, safeName])];

// TODO: due to name + safe name, options are duplicated for same driver which is confusing a bit tedious to pass options (e.g. deno-kv or denoKV?) -> currently only (deno-kv works but denoKV is typed)
const optionsTName = upperFirst(safeName) + "Options";

const driverName = upperFirst(safeName) + "Driver";

drivers.push({
name,
safeName,
names,
subpath,
optionsTExport,
optionsTName,
driverOptionsExport,
driverName,
});
}

Expand All @@ -64,6 +76,14 @@ ${drivers
)
.join("\n")}

${drivers
.filter((d) => d.driverOptionsExport)
.map(
(d) =>
/* ts */ `import type { ${d.driverOptionsExport} as ${d.driverName} } from "${d.subpath}";`
)
.join("\n")}

export type BuiltinDriverName = ${drivers.flatMap((d) => d.names.map((name) => `"${name}"`)).join(" | ")};

export type BuiltinDriverOptions = {
Expand All @@ -76,6 +96,63 @@ export type BuiltinDriverOptions = {
export const builtinDrivers = {
${drivers.flatMap((d) => d.names.map((name) => `"${name}": "${d.subpath}"`)).join(",\n ")},
} as const;

export type BuiltinDrivers = {
${drivers
.filter((d) => d.driverOptionsExport)
.flatMap((d) => d.names.map((name) => `"${name}": ${d.driverName};`))
.join("\n ")}
}

export type DriverGetOptions = {
${drivers
.filter((d) => d.driverOptionsExport)
.map(
(d) =>
`"${d.safeName}"?: ${d.driverName} extends { getOptions: infer TGet } ? unknown extends TGet ? {} : TGet : {}`
)
.join("\n ")}
}

export type DriverSetOptions = {
${drivers
.filter((d) => d.driverOptionsExport)
.map(
(d) =>
`"${d.safeName}"?: ${d.driverName} extends { setOptions: infer TSet } ? unknown extends TSet ? {} : TSet : {}`
)
.join("\n ")}
}

export type DriverRemoveOptions = {
${drivers
.filter((d) => d.driverOptionsExport)
.map(
(d) =>
`"${d.safeName}"?: ${d.driverName} extends { removeOptions: infer TRemove } ? unknown extends TRemove ? {} : TRemove : {}`
)
.join("\n ")}
}

export type DriverListOptions = {
${drivers
.filter((d) => d.driverOptionsExport)
.map(
(d) =>
`"${d.safeName}"?: ${d.driverName} extends { listOptions: infer TList } ? unknown extends TList ? {} : TList : {}`
)
.join("\n ")}
}

export type DriverClearOptions = {
${drivers
.filter((d) => d.driverOptionsExport)
.map(
(d) =>
`"${d.safeName}"?: ${d.driverName} extends { clearOptions: infer TClear } ? unknown extends TClear ? {} : TClear : {}`
)
.join("\n ")}
}
`;

await writeFile(driversMetaFile, genCode, "utf8");
Expand Down
27 changes: 27 additions & 0 deletions src/_drivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import type { VercelBlobOptions as VercelBlobOptions } from "unstorage/drivers/v
import type { VercelKVOptions as VercelKVOptions } from "unstorage/drivers/vercel-kv";
import type { VercelCacheOptions as VercelRuntimeCacheOptions } from "unstorage/drivers/vercel-runtime-cache";

import type { DenoKvDriver as DenoKVDriver } from "unstorage/drivers/deno-kv";

export type BuiltinDriverName = "azure-app-configuration" | "azureAppConfiguration" | "azure-cosmos" | "azureCosmos" | "azure-key-vault" | "azureKeyVault" | "azure-storage-blob" | "azureStorageBlob" | "azure-storage-table" | "azureStorageTable" | "capacitor-preferences" | "capacitorPreferences" | "cloudflare-kv-binding" | "cloudflareKVBinding" | "cloudflare-kv-http" | "cloudflareKVHttp" | "cloudflare-r2-binding" | "cloudflareR2Binding" | "db0" | "deno-kv-node" | "denoKVNode" | "deno-kv" | "denoKV" | "fs-lite" | "fsLite" | "fs" | "github" | "http" | "indexedb" | "localstorage" | "lru-cache" | "lruCache" | "memory" | "mongodb" | "netlify-blobs" | "netlifyBlobs" | "null" | "overlay" | "planetscale" | "redis" | "s3" | "session-storage" | "sessionStorage" | "uploadthing" | "upstash" | "vercel-blob" | "vercelBlob" | "vercel-kv" | "vercelKV" | "vercel-runtime-cache" | "vercelRuntimeCache";

export type BuiltinDriverOptions = {
Expand Down Expand Up @@ -140,3 +142,28 @@ export const builtinDrivers = {
"vercel-runtime-cache": "unstorage/drivers/vercel-runtime-cache",
"vercelRuntimeCache": "unstorage/drivers/vercel-runtime-cache",
} as const;

export type BuiltinDrivers = {
"deno-kv": DenoKVDriver;
"denoKV": DenoKVDriver;
}

export type DriverGetOptions = {
"denoKV"?: DenoKVDriver extends { getOptions: infer TGet } ? unknown extends TGet ? {} : TGet : {}
}

export type DriverSetOptions = {
"denoKV"?: DenoKVDriver extends { setOptions: infer TSet } ? unknown extends TSet ? {} : TSet : {}
}

export type DriverRemoveOptions = {
"denoKV"?: DenoKVDriver extends { removeOptions: infer TRemove } ? unknown extends TRemove ? {} : TRemove : {}
}

export type DriverListOptions = {
"denoKV"?: DenoKVDriver extends { listOptions: infer TList } ? unknown extends TList ? {} : TList : {}
}

export type DriverClearOptions = {
"denoKV"?: DenoKVDriver extends { clearOptions: infer TClear } ? unknown extends TClear ? {} : TClear : {}
}
15 changes: 8 additions & 7 deletions src/drivers/deno-kv.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineDriver, createError, normalizeKey } from "./utils/index";
import type { Kv, KvKey } from "@deno/kv";
import type { SetOptions } from "../types";

// https://docs.deno.com/deploy/kv/manual/

Expand All @@ -12,11 +13,11 @@ export interface DenoKvOptions {
*/
ttl?: number;
}
interface DenoKVSetOptions {
/**
* TTL in seconds.
*/
ttl?: number;

export interface DenoKvDriver {
setOptions: {};
getOptions: {};
removeOptions: {};
}

const DRIVER_NAME = "deno-kv";
Expand Down Expand Up @@ -75,12 +76,12 @@ export default defineDriver<DenoKvOptions, Promise<Deno.Kv | Kv>>(
const value = await kv.get(r(key));
return value.value;
},
async setItem(key, value, tOptions: DenoKVSetOptions) {
async setItem(key, value, tOptions: SetOptions) {
const ttl = normalizeTTL(tOptions?.ttl ?? opts?.ttl);
const kv = await getKv();
await kv.set(r(key), value, { expireIn: ttl });
},
async setItemRaw(key, value, tOptions: DenoKVSetOptions) {
async setItemRaw(key, value, tOptions: SetOptions) {
const ttl = normalizeTTL(tOptions?.ttl ?? opts?.ttl);
const kv = await getKv();
await kv.set(r(key), value, { expireIn: ttl });
Expand Down
Loading