@theholocron/holocron-plugin-neon 2.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/index.d.mts +105 -0
- package/dist/index.mjs +185 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Newton Koumantzelis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# `@theholocron/holocron-plugin-neon`
|
|
2
|
+
|
|
3
|
+
Neon plugin for [Holocron](../cli). Implements the `storage`
|
|
4
|
+
capability against [Neon's REST API](https://api-docs.neon.tech/reference/getting-started-with-neon-api).
|
|
5
|
+
|
|
6
|
+
## Auth
|
|
7
|
+
|
|
8
|
+
Token resolution order:
|
|
9
|
+
|
|
10
|
+
1. `--token <PAT>` flag on the holocron invocation
|
|
11
|
+
2. `HOLOCRON_NEON_API_KEY` env var
|
|
12
|
+
3. `NEON_API_KEY` env var (the default Neon's own CLI reads)
|
|
13
|
+
|
|
14
|
+
## Config
|
|
15
|
+
|
|
16
|
+
```jsonc
|
|
17
|
+
{
|
|
18
|
+
"providers": {
|
|
19
|
+
"storage": ["neon", { "projectId": "ancient-resonance-…" }]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `projectId` (required) — the Neon project id. The plugin binds to
|
|
25
|
+
this project; every method operates within it.
|
|
26
|
+
|
|
27
|
+
## What's implemented
|
|
28
|
+
|
|
29
|
+
| Method | What it does |
|
|
30
|
+
| ----------------------- | --------------------------------------------------------- |
|
|
31
|
+
| `getConnectionString` | Fetches the connection URI for a branch. `pooled: true` returns the PgBouncer URL. |
|
|
32
|
+
| `listBranches` | All branches on the bound project. |
|
|
33
|
+
| `createBranch` | Provisions a branch + a read_write compute endpoint inline (so the next connection-string call doesn't 404). |
|
|
34
|
+
| `destroyBranch` | DELETEs a branch. |
|
|
35
|
+
| `resetBranch` | Restores one branch to match another (Neon "restore branch" endpoint). |
|
|
36
|
+
| `enableExtension` | Runs `CREATE EXTENSION IF NOT EXISTS "..."` against the branch's default database via Neon's run_sql endpoint. Used for PostGIS, pgvector, etc. |
|
|
37
|
+
|
|
38
|
+
Vercel-managed Neon orgs reject project-create at the Neon API level
|
|
39
|
+
("organization is managed by Vercel"). For those setups, provision
|
|
40
|
+
the project via Vercel's marketplace integration (`vercel install
|
|
41
|
+
neon`) — the project becomes visible to Neon's API afterward and
|
|
42
|
+
holocron can take it from there. Marketplace provisioning is out of
|
|
43
|
+
scope for this plugin (covered by `holocron-plugin-vercel` if needed
|
|
44
|
+
later).
|
|
45
|
+
|
|
46
|
+
## Status
|
|
47
|
+
|
|
48
|
+
**v0.0.0 — first port.**
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ConnectionStringOptions, Storage, StorageBranch } from "@theholocron/cli";
|
|
2
|
+
|
|
3
|
+
//#region src/auth.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Token resolution for the Neon plugin.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order:
|
|
8
|
+
* 1. explicit `cliToken` argument (from `--token` flag)
|
|
9
|
+
* 2. HOLOCRON_NEON_API_KEY env var (preferred — explicit intent)
|
|
10
|
+
* 3. NEON_API_KEY env var (the default Neon CLI reads)
|
|
11
|
+
*/
|
|
12
|
+
declare class AuthError extends Error {
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
interface ResolveTokenInput {
|
|
16
|
+
/** From `--token` CLI flag. */
|
|
17
|
+
cliToken?: string;
|
|
18
|
+
/** Env vars; passed in for testability. Defaults to `process.env`. */
|
|
19
|
+
env?: NodeJS.ProcessEnv;
|
|
20
|
+
}
|
|
21
|
+
declare function resolveToken(input?: ResolveTokenInput): string;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/rest.d.ts
|
|
24
|
+
/**
|
|
25
|
+
* Thin REST wrapper around console.neon.tech/api/v2.
|
|
26
|
+
*
|
|
27
|
+
* Same pattern as the GitHub + Vercel REST clients — bearer auth,
|
|
28
|
+
* JSON-only bodies, transport-failure wrapping with status: 0 so the
|
|
29
|
+
* orchestrator's soft-skip path sees a clear "Neon GET /path failed"
|
|
30
|
+
* message instead of a generic `TypeError: fetch failed`.
|
|
31
|
+
*/
|
|
32
|
+
interface RestClientOptions {
|
|
33
|
+
token: string;
|
|
34
|
+
fetch?: typeof fetch;
|
|
35
|
+
baseUrl?: string;
|
|
36
|
+
}
|
|
37
|
+
interface RequestOptions {
|
|
38
|
+
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
39
|
+
body?: unknown;
|
|
40
|
+
query?: Record<string, string>;
|
|
41
|
+
}
|
|
42
|
+
declare class NeonRestClient {
|
|
43
|
+
private readonly token;
|
|
44
|
+
private readonly fetchImpl;
|
|
45
|
+
readonly baseUrl: string;
|
|
46
|
+
constructor(opts: RestClientOptions);
|
|
47
|
+
request<T>(path: string, opts?: RequestOptions): Promise<T>;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/capabilities/storage.d.ts
|
|
51
|
+
interface StorageOptions {
|
|
52
|
+
projectId: string;
|
|
53
|
+
}
|
|
54
|
+
declare class NeonStorage implements Storage {
|
|
55
|
+
private readonly rest;
|
|
56
|
+
readonly key: "storage";
|
|
57
|
+
readonly providerName = "neon";
|
|
58
|
+
private readonly base;
|
|
59
|
+
constructor(rest: NeonRestClient, opts: StorageOptions);
|
|
60
|
+
getConnectionString(scope: string, options?: ConnectionStringOptions): Promise<string>;
|
|
61
|
+
listBranches(): Promise<StorageBranch[]>;
|
|
62
|
+
createBranch(input: {
|
|
63
|
+
name: string;
|
|
64
|
+
from?: string;
|
|
65
|
+
}): Promise<StorageBranch>;
|
|
66
|
+
destroyBranch(branch: string): Promise<void>;
|
|
67
|
+
resetBranch(input: {
|
|
68
|
+
branch: string;
|
|
69
|
+
from: string;
|
|
70
|
+
}): Promise<void>;
|
|
71
|
+
enableExtension(input: {
|
|
72
|
+
branch: string;
|
|
73
|
+
extension: string;
|
|
74
|
+
}): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* The first database on a branch — the "default" for connection-string
|
|
77
|
+
* and extension purposes. Throws if the branch has no databases (which
|
|
78
|
+
* means it was created without an endpoint and needs initialization).
|
|
79
|
+
*/
|
|
80
|
+
private firstDatabase;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/index.d.ts
|
|
84
|
+
interface NeonPluginOptions extends ResolveTokenInput {
|
|
85
|
+
/** Neon project id this plugin is bound to. Required. */
|
|
86
|
+
projectId: string;
|
|
87
|
+
/** Override base URL for tests. */
|
|
88
|
+
baseUrl?: string;
|
|
89
|
+
/** Override `fetch` for tests. */
|
|
90
|
+
fetch?: typeof fetch;
|
|
91
|
+
}
|
|
92
|
+
interface PluginContext {
|
|
93
|
+
options: NeonPluginOptions;
|
|
94
|
+
rest: NeonRestClient;
|
|
95
|
+
}
|
|
96
|
+
declare function createContext(options: NeonPluginOptions): PluginContext;
|
|
97
|
+
declare function storage(ctx: PluginContext): Storage;
|
|
98
|
+
declare function createPlugin(options: NeonPluginOptions): {
|
|
99
|
+
name: string;
|
|
100
|
+
capabilities: {
|
|
101
|
+
storage: () => Storage;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
export { AuthError, NeonPluginOptions, NeonRestClient, NeonStorage, PluginContext, ResolveTokenInput, createContext, createPlugin, resolveToken, storage };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { ProviderApiError } from "@theholocron/cli";
|
|
2
|
+
//#region src/auth.ts
|
|
3
|
+
/**
|
|
4
|
+
* Token resolution for the Neon plugin.
|
|
5
|
+
*
|
|
6
|
+
* Resolution order:
|
|
7
|
+
* 1. explicit `cliToken` argument (from `--token` flag)
|
|
8
|
+
* 2. HOLOCRON_NEON_API_KEY env var (preferred — explicit intent)
|
|
9
|
+
* 3. NEON_API_KEY env var (the default Neon CLI reads)
|
|
10
|
+
*/
|
|
11
|
+
var AuthError = class extends Error {
|
|
12
|
+
name = "AuthError";
|
|
13
|
+
};
|
|
14
|
+
function resolveToken(input = {}) {
|
|
15
|
+
const env = input.env ?? process.env;
|
|
16
|
+
const token = input.cliToken || env.HOLOCRON_NEON_API_KEY || env.NEON_API_KEY;
|
|
17
|
+
if (!token) throw new AuthError("no Neon API key found. Pass --token <KEY>, or set HOLOCRON_NEON_API_KEY / NEON_API_KEY.");
|
|
18
|
+
return token;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/capabilities/storage.ts
|
|
22
|
+
/**
|
|
23
|
+
* `storage` capability for Neon.
|
|
24
|
+
*
|
|
25
|
+
* Ported from rando-id/rando.id `adapters/neon.ts`, bound to a single
|
|
26
|
+
* Neon project (the `projectId` plugin option). All methods operate
|
|
27
|
+
* within that project.
|
|
28
|
+
*
|
|
29
|
+
* Branch ops use Neon's REST API directly. `enableExtension` runs SQL
|
|
30
|
+
* against the branch's default database via the `run_sql` endpoint —
|
|
31
|
+
* Neon doesn't have a dedicated "create extension" endpoint, so this
|
|
32
|
+
* goes through SQL.
|
|
33
|
+
*
|
|
34
|
+
* Project-create is deliberately omitted: Vercel-managed Neon orgs
|
|
35
|
+
* reject create at the API level, and the recipe (`vercel install neon`
|
|
36
|
+
* → wait for project to appear → bind by id) is unavoidable for those
|
|
37
|
+
* setups. The plugin assumes the project already exists and is bound
|
|
38
|
+
* via `projectId` in options.
|
|
39
|
+
*/
|
|
40
|
+
var NeonStorage = class {
|
|
41
|
+
rest;
|
|
42
|
+
key = "storage";
|
|
43
|
+
providerName = "neon";
|
|
44
|
+
base;
|
|
45
|
+
constructor(rest, opts) {
|
|
46
|
+
this.rest = rest;
|
|
47
|
+
if (!opts.projectId) throw new Error("NeonStorage requires `projectId` in options");
|
|
48
|
+
this.base = `/projects/${opts.projectId}`;
|
|
49
|
+
}
|
|
50
|
+
async getConnectionString(scope, options = {}) {
|
|
51
|
+
const db = await this.firstDatabase(scope);
|
|
52
|
+
const params = {
|
|
53
|
+
branch_id: scope,
|
|
54
|
+
database_name: db.name,
|
|
55
|
+
role_name: db.owner_name,
|
|
56
|
+
pooled: options.pooled ? "true" : "false"
|
|
57
|
+
};
|
|
58
|
+
return (await this.rest.request(`${this.base}/connection_uri`, { query: params })).uri;
|
|
59
|
+
}
|
|
60
|
+
async listBranches() {
|
|
61
|
+
return (await this.rest.request(`${this.base}/branches`)).branches.map(mapBranch);
|
|
62
|
+
}
|
|
63
|
+
async createBranch(input) {
|
|
64
|
+
return mapBranch((await this.rest.request(`${this.base}/branches`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: {
|
|
67
|
+
branch: {
|
|
68
|
+
name: input.name,
|
|
69
|
+
...input.from ? { parent_id: input.from } : {}
|
|
70
|
+
},
|
|
71
|
+
endpoints: [{ type: "read_write" }]
|
|
72
|
+
}
|
|
73
|
+
})).branch);
|
|
74
|
+
}
|
|
75
|
+
async destroyBranch(branch) {
|
|
76
|
+
await this.rest.request(`${this.base}/branches/${encodeURIComponent(branch)}`, { method: "DELETE" });
|
|
77
|
+
}
|
|
78
|
+
async resetBranch(input) {
|
|
79
|
+
await this.rest.request(`${this.base}/branches/${encodeURIComponent(input.branch)}/restore`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: { source_branch_id: input.from }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async enableExtension(input) {
|
|
85
|
+
const db = await this.firstDatabase(input.branch);
|
|
86
|
+
await this.rest.request(`${this.base}/branches/${encodeURIComponent(input.branch)}/databases/${encodeURIComponent(db.name)}/run_sql`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: { query: `CREATE EXTENSION IF NOT EXISTS "${input.extension}"` }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The first database on a branch — the "default" for connection-string
|
|
93
|
+
* and extension purposes. Throws if the branch has no databases (which
|
|
94
|
+
* means it was created without an endpoint and needs initialization).
|
|
95
|
+
*/
|
|
96
|
+
async firstDatabase(branchId) {
|
|
97
|
+
const db = (await this.rest.request(`${this.base}/branches/${encodeURIComponent(branchId)}/databases`)).databases[0];
|
|
98
|
+
if (!db) throw new ProviderApiError(`Neon branch ${branchId} has no databases — initialize the branch first`, 404, void 0);
|
|
99
|
+
return db;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
function mapBranch(raw) {
|
|
103
|
+
return {
|
|
104
|
+
id: raw.id,
|
|
105
|
+
name: raw.name,
|
|
106
|
+
parentId: raw.parent_id ?? null,
|
|
107
|
+
createdAt: raw.created_at
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/rest.ts
|
|
112
|
+
/**
|
|
113
|
+
* Thin REST wrapper around console.neon.tech/api/v2.
|
|
114
|
+
*
|
|
115
|
+
* Same pattern as the GitHub + Vercel REST clients — bearer auth,
|
|
116
|
+
* JSON-only bodies, transport-failure wrapping with status: 0 so the
|
|
117
|
+
* orchestrator's soft-skip path sees a clear "Neon GET /path failed"
|
|
118
|
+
* message instead of a generic `TypeError: fetch failed`.
|
|
119
|
+
*/
|
|
120
|
+
var NeonRestClient = class {
|
|
121
|
+
token;
|
|
122
|
+
fetchImpl;
|
|
123
|
+
baseUrl;
|
|
124
|
+
constructor(opts) {
|
|
125
|
+
this.token = opts.token;
|
|
126
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
127
|
+
this.baseUrl = (opts.baseUrl ?? "https://console.neon.tech/api/v2").replace(/\/+$/, "");
|
|
128
|
+
}
|
|
129
|
+
async request(path, opts = {}) {
|
|
130
|
+
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : "/" + path}`);
|
|
131
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
|
|
132
|
+
const fullUrl = url.toString();
|
|
133
|
+
const headers = {
|
|
134
|
+
authorization: `Bearer ${this.token}`,
|
|
135
|
+
accept: "application/json"
|
|
136
|
+
};
|
|
137
|
+
const init = {
|
|
138
|
+
method: opts.method ?? "GET",
|
|
139
|
+
headers
|
|
140
|
+
};
|
|
141
|
+
if (opts.body !== void 0) {
|
|
142
|
+
headers["content-type"] = "application/json";
|
|
143
|
+
init.body = JSON.stringify(opts.body);
|
|
144
|
+
}
|
|
145
|
+
let res;
|
|
146
|
+
try {
|
|
147
|
+
res = await this.fetchImpl(fullUrl, init);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
150
|
+
throw new ProviderApiError(`Neon ${init.method} ${path} failed: ${detail}`, 0, void 0);
|
|
151
|
+
}
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
const body = await res.text().catch(() => "");
|
|
154
|
+
throw new ProviderApiError(`Neon ${init.method} ${path} → ${res.status}`, res.status, body);
|
|
155
|
+
}
|
|
156
|
+
if (res.status === 204) return void 0;
|
|
157
|
+
const text = await res.text();
|
|
158
|
+
if (!text) return void 0;
|
|
159
|
+
return JSON.parse(text);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/index.ts
|
|
164
|
+
function createContext(options) {
|
|
165
|
+
if (!options.projectId) throw new Error("@theholocron/holocron-plugin-neon requires `projectId` in options");
|
|
166
|
+
const restOpts = { token: resolveToken(options) };
|
|
167
|
+
if (options.baseUrl !== void 0) restOpts.baseUrl = options.baseUrl;
|
|
168
|
+
if (options.fetch !== void 0) restOpts.fetch = options.fetch;
|
|
169
|
+
return {
|
|
170
|
+
options,
|
|
171
|
+
rest: new NeonRestClient(restOpts)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function storage(ctx) {
|
|
175
|
+
return new NeonStorage(ctx.rest, { projectId: ctx.options.projectId });
|
|
176
|
+
}
|
|
177
|
+
function createPlugin(options) {
|
|
178
|
+
const ctx = createContext(options);
|
|
179
|
+
return {
|
|
180
|
+
name: "@theholocron/holocron-plugin-neon",
|
|
181
|
+
capabilities: { storage: () => storage(ctx) }
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
export { AuthError, NeonRestClient, NeonStorage, createContext, createPlugin, resolveToken, storage };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@theholocron/holocron-plugin-neon",
|
|
3
|
+
"version": "2.0.0-alpha.0",
|
|
4
|
+
"description": "Holocron plugin for Neon. Implements the storage capability against Neon's REST API — branch ops, connection strings, extensions.",
|
|
5
|
+
"homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-neon#readme",
|
|
6
|
+
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/theholocron/holocron.git",
|
|
10
|
+
"directory": "packages/holocron-plugin-neon"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Newton Koumantzelis",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.mjs",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.mts",
|
|
19
|
+
"import": "./dist/index.mjs",
|
|
20
|
+
"default": "./dist/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@theholocron/cli": "2.0.0-alpha.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@theholocron/tsconfig": "^4.1.0",
|
|
28
|
+
"@tsconfig/node-lts": "^24.0.0",
|
|
29
|
+
"@vitest/coverage-v8": "^3.2.6",
|
|
30
|
+
"eslint": "^9.36.0",
|
|
31
|
+
"globals": "^16.5.0",
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"vitest": "^3.2.6",
|
|
34
|
+
"tsdown": "^0.22.3",
|
|
35
|
+
"@theholocron/cli": "2.0.0-alpha.0"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsdown",
|
|
46
|
+
"lint": "eslint .",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"test:coverage": "vitest run --coverage"
|
|
51
|
+
},
|
|
52
|
+
"types": "./dist/index.d.mts"
|
|
53
|
+
}
|