@pnpm/pnpr.client 1.1.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 +22 -0
- package/README.md +43 -0
- package/lib/fetchFromPnpmRegistry.d.ts +89 -0
- package/lib/fetchFromPnpmRegistry.js +179 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -0
- package/lib/protocol.d.ts +13 -0
- package/lib/protocol.js +2 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-2016 Rico Sta. Cruz and other contributors
|
|
4
|
+
Copyright (c) 2016-2026 Zoltan Kochan and other contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @pnpm/pnpr.client
|
|
2
|
+
|
|
3
|
+
Client library for the pnpr server. Reads the local store state, sends it to the server, and writes the received files into the content-addressable store.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
1. Reads integrity hashes from the local store index (`index.db`).
|
|
8
|
+
2. Sends `POST /v1/install` to the pnpr server with the project's dependencies and the store integrities.
|
|
9
|
+
3. Parses the NDJSON streaming response — `D`-lines (missing file digests) are dispatched to worker downloads against `/v1/files`, `I`-lines are buffered as raw store-index entries, and the final `L`-line yields the resolved lockfile and stats.
|
|
10
|
+
4. File download workers write each received file directly to the local CAFS (`files/{hash[:2]}/{hash[2:]}`).
|
|
11
|
+
5. Writes store index entries for all new packages in a single SQLite transaction.
|
|
12
|
+
6. Returns the resolved lockfile for use with pnpm's headless install (linking phase).
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
This package is used internally by pnpm when the `pnprServer` config option is set. It is not intended to be called directly, but can be used programmatically:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { fetchFromPnpmRegistry } from '@pnpm/pnpr.client'
|
|
20
|
+
import { StoreIndex } from '@pnpm/store.index'
|
|
21
|
+
|
|
22
|
+
const storeIndex = new StoreIndex('/path/to/store')
|
|
23
|
+
|
|
24
|
+
const { lockfile, stats } = await fetchFromPnpmRegistry({
|
|
25
|
+
registryUrl: 'http://localhost:4000',
|
|
26
|
+
storeDir: '/path/to/store',
|
|
27
|
+
storeIndex,
|
|
28
|
+
dependencies: { react: '^19.0.0' },
|
|
29
|
+
devDependencies: { typescript: '^5.0.0' },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
console.log(`Resolved ${stats.totalPackages} packages`)
|
|
33
|
+
console.log(`${stats.alreadyInStore} cached, ${stats.filesToDownload} files downloaded`)
|
|
34
|
+
// lockfile is ready for headless install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
Add to `pnpm-workspace.yaml` to enable automatically during `pnpm install`:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
pnprServer: http://localhost:4000
|
|
43
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { LockfileFile, LockfileObject } from '@pnpm/lockfile.types';
|
|
2
|
+
import { StoreIndex } from '@pnpm/store.index';
|
|
3
|
+
import type { ResponseMetadata } from './protocol.js';
|
|
4
|
+
export interface PnprProject {
|
|
5
|
+
/** Relative dir within the workspace (e.g. "." or "packages/foo") */
|
|
6
|
+
dir: string;
|
|
7
|
+
dependencies?: Record<string, string>;
|
|
8
|
+
devDependencies?: Record<string, string>;
|
|
9
|
+
optionalDependencies?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export interface FetchFromPnpmRegistryOptions {
|
|
12
|
+
/** URL of the pnpr server */
|
|
13
|
+
registryUrl: string;
|
|
14
|
+
/** Client's store directory */
|
|
15
|
+
storeDir: string;
|
|
16
|
+
/** Client's store index */
|
|
17
|
+
storeIndex: StoreIndex;
|
|
18
|
+
/** Dependencies to resolve (single project) */
|
|
19
|
+
dependencies?: Record<string, string>;
|
|
20
|
+
/** Dev dependencies to resolve (single project) */
|
|
21
|
+
devDependencies?: Record<string, string>;
|
|
22
|
+
/** Optional dependencies to resolve (single project) */
|
|
23
|
+
optionalDependencies?: Record<string, string>;
|
|
24
|
+
/** Multiple projects in a workspace */
|
|
25
|
+
projects?: PnprProject[];
|
|
26
|
+
/**
|
|
27
|
+
* The client's default registry. The server resolves against this
|
|
28
|
+
* (and `namedRegistries`) rather than its own configuration.
|
|
29
|
+
*/
|
|
30
|
+
registry?: string;
|
|
31
|
+
/** The client's named-registry aliases (`namedRegistries`). */
|
|
32
|
+
namedRegistries?: Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* The caller's forwarded upstream credentials, keyed by nerf-darted
|
|
35
|
+
* registry URI, so the server resolves/fetches private content as the
|
|
36
|
+
* caller. Distinct from `authorization` (pnpr identity).
|
|
37
|
+
*/
|
|
38
|
+
authHeaders?: Record<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
* `Authorization` for the pnpr server's own URL (`undefined` if none):
|
|
41
|
+
* identifies the caller to pnpr's gate and keys the grant table.
|
|
42
|
+
*/
|
|
43
|
+
authorization?: string;
|
|
44
|
+
/** Overrides */
|
|
45
|
+
overrides?: Record<string, string>;
|
|
46
|
+
/** Node.js version for resolution */
|
|
47
|
+
nodeVersion?: string;
|
|
48
|
+
/** Minimum release age in minutes */
|
|
49
|
+
minimumReleaseAge?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Existing lockfile for incremental resolution, in the on-disk format
|
|
52
|
+
* the wire protocol carries. The caller reads it with
|
|
53
|
+
* `readWantedLockfileFile` so no in-memory→on-disk round-trip is needed.
|
|
54
|
+
*/
|
|
55
|
+
lockfile?: LockfileFile;
|
|
56
|
+
/**
|
|
57
|
+
* `--lockfile-only`: resolve and return only the lockfile — fetch no
|
|
58
|
+
* files into the local store. Forwarded to the server (which skips the
|
|
59
|
+
* file diff); the client ignores the (empty) file payload so the store
|
|
60
|
+
* stays untouched. Mirrors pnpm's resolve + write, fetch nothing, link
|
|
61
|
+
* nothing. See https://github.com/pnpm/pnpm/issues/12146.
|
|
62
|
+
*/
|
|
63
|
+
lockfileOnly?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface FetchFromPnpmRegistryResult {
|
|
66
|
+
lockfile: LockfileObject;
|
|
67
|
+
stats: ResponseMetadata['stats'];
|
|
68
|
+
/** Promise that resolves when all file downloads are written to CAFS */
|
|
69
|
+
fileDownloads: Promise<void>;
|
|
70
|
+
/** Pre-packed store index entries to write to SQLite */
|
|
71
|
+
indexEntries: Array<{
|
|
72
|
+
key: string;
|
|
73
|
+
buffer: Uint8Array;
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fetch resolved dependencies from a pnpr server in a single round trip.
|
|
78
|
+
*
|
|
79
|
+
* `POST /v1/install` (with `inlineFiles`) answers with one gzipped binary
|
|
80
|
+
* body: a length-prefixed JSON header (lockfile, stats, store-index
|
|
81
|
+
* entries, or verification violations) followed by the missing files'
|
|
82
|
+
* contents as binary frames. We parse the header here and hand the file
|
|
83
|
+
* frames to a worker that writes them straight into the CAFS.
|
|
84
|
+
*/
|
|
85
|
+
export declare function fetchFromPnpmRegistry(opts: FetchFromPnpmRegistryOptions): Promise<FetchFromPnpmRegistryResult>;
|
|
86
|
+
export declare function writeRawIndexEntries(indexEntries: Array<{
|
|
87
|
+
key: string;
|
|
88
|
+
buffer: Uint8Array;
|
|
89
|
+
}>, storeIndex: StoreIndex): void;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import { URL } from 'node:url';
|
|
4
|
+
import { gunzip } from 'node:zlib';
|
|
5
|
+
import { convertToLockfileObject } from '@pnpm/lockfile.fs';
|
|
6
|
+
import { StoreIndex } from '@pnpm/store.index';
|
|
7
|
+
import { writeCafsFiles } from '@pnpm/worker';
|
|
8
|
+
/**
|
|
9
|
+
* Fetch resolved dependencies from a pnpr server in a single round trip.
|
|
10
|
+
*
|
|
11
|
+
* `POST /v1/install` (with `inlineFiles`) answers with one gzipped binary
|
|
12
|
+
* body: a length-prefixed JSON header (lockfile, stats, store-index
|
|
13
|
+
* entries, or verification violations) followed by the missing files'
|
|
14
|
+
* contents as binary frames. We parse the header here and hand the file
|
|
15
|
+
* frames to a worker that writes them straight into the CAFS.
|
|
16
|
+
*/
|
|
17
|
+
export async function fetchFromPnpmRegistry(opts) {
|
|
18
|
+
const storeIntegrities = readStoreIntegrities(opts.storeIndex);
|
|
19
|
+
const projects = opts.projects ?? [{
|
|
20
|
+
dir: '.',
|
|
21
|
+
dependencies: opts.dependencies,
|
|
22
|
+
devDependencies: opts.devDependencies,
|
|
23
|
+
optionalDependencies: opts.optionalDependencies,
|
|
24
|
+
}];
|
|
25
|
+
const requestBody = JSON.stringify({
|
|
26
|
+
projects,
|
|
27
|
+
registry: opts.registry,
|
|
28
|
+
namedRegistries: opts.namedRegistries,
|
|
29
|
+
authHeaders: opts.authHeaders,
|
|
30
|
+
overrides: opts.overrides,
|
|
31
|
+
nodeVersion: opts.nodeVersion ?? process.version.slice(1),
|
|
32
|
+
os: process.platform,
|
|
33
|
+
arch: process.arch,
|
|
34
|
+
minimumReleaseAge: opts.minimumReleaseAge,
|
|
35
|
+
// Sent as-is: `opts.lockfile` is already the on-disk format the wire
|
|
36
|
+
// protocol carries (split `packages`/`snapshots`, `{ specifier, version }`
|
|
37
|
+
// importer deps).
|
|
38
|
+
lockfile: opts.lockfile,
|
|
39
|
+
lockfileOnly: opts.lockfileOnly,
|
|
40
|
+
storeIntegrities,
|
|
41
|
+
inlineFiles: true,
|
|
42
|
+
});
|
|
43
|
+
const body = await postInstall(opts.registryUrl, requestBody, opts.authorization);
|
|
44
|
+
// The combined response is `[u32 header length][header JSON][file frames]`.
|
|
45
|
+
if (body.length < 4) {
|
|
46
|
+
throw new Error('pnpr server returned a truncated /v1/install response');
|
|
47
|
+
}
|
|
48
|
+
const headerLength = body.readUInt32BE(0);
|
|
49
|
+
const header = JSON.parse(body.subarray(4, 4 + headerLength).toString('utf-8'));
|
|
50
|
+
if (header.violations != null && header.violations.length > 0) {
|
|
51
|
+
const rendered = header.violations
|
|
52
|
+
.map((violation) => ` ${violation.name}@${violation.version}: ${violation.reason}`)
|
|
53
|
+
.join('\n');
|
|
54
|
+
throw new Error(`pnpr server rejected the lockfile under the verification policy:\n${rendered}`);
|
|
55
|
+
}
|
|
56
|
+
const indexEntries = (header.indexEntries ?? []).map(({ key, b64 }) => ({
|
|
57
|
+
key,
|
|
58
|
+
buffer: new Uint8Array(Buffer.from(b64, 'base64')),
|
|
59
|
+
}));
|
|
60
|
+
// `--lockfile-only` fetches nothing: there are no file frames to write
|
|
61
|
+
// (the server sends only the end-of-stream marker), so leave the store
|
|
62
|
+
// untouched.
|
|
63
|
+
const fileDownloads = opts.lockfileOnly
|
|
64
|
+
? Promise.resolve()
|
|
65
|
+
: writeCafsFiles({
|
|
66
|
+
storeDir: opts.storeDir,
|
|
67
|
+
payload: body.subarray(4 + headerLength),
|
|
68
|
+
}).then(() => { });
|
|
69
|
+
return {
|
|
70
|
+
// The server speaks the on-disk lockfile format; convert it to the
|
|
71
|
+
// in-memory `LockfileObject` the rest of pnpm consumes.
|
|
72
|
+
lockfile: convertToLockfileObject(header.lockfile),
|
|
73
|
+
stats: header.stats,
|
|
74
|
+
fileDownloads,
|
|
75
|
+
indexEntries,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function readStoreIntegrities(storeIndex) {
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
for (const key of storeIndex.keys()) {
|
|
81
|
+
const tabIdx = key.indexOf('\t');
|
|
82
|
+
if (tabIdx === -1)
|
|
83
|
+
continue;
|
|
84
|
+
const integrity = key.slice(0, tabIdx);
|
|
85
|
+
// StoreIndex also stores non-integrity keys (e.g. git-hosted entries
|
|
86
|
+
// keyed by URL). Filter to actual SRI hashes — sending those over to
|
|
87
|
+
// the pnpr server would just bloat the request without ever matching.
|
|
88
|
+
if (!isIntegrityLike(integrity))
|
|
89
|
+
continue;
|
|
90
|
+
seen.add(integrity);
|
|
91
|
+
}
|
|
92
|
+
return [...seen];
|
|
93
|
+
}
|
|
94
|
+
function isIntegrityLike(value) {
|
|
95
|
+
return value.startsWith('sha512-') ||
|
|
96
|
+
value.startsWith('sha256-') ||
|
|
97
|
+
value.startsWith('sha1-');
|
|
98
|
+
}
|
|
99
|
+
const REQUEST_TIMEOUT = 600_000; // 10 minutes — server-side resolution can be slow on first run
|
|
100
|
+
/**
|
|
101
|
+
* `POST /v1/install` and return the full response body, decompressed.
|
|
102
|
+
*
|
|
103
|
+
* `urlPath` resolution normalizes the base to end with "/" so a path
|
|
104
|
+
* prefix configured on the pnpr server URL (e.g. https://host/pnpr/) is
|
|
105
|
+
* preserved.
|
|
106
|
+
*/
|
|
107
|
+
async function postInstall(registryUrl, body, authorization) {
|
|
108
|
+
const base = registryUrl.endsWith('/') ? registryUrl : `${registryUrl}/`;
|
|
109
|
+
const url = new URL('v1/install', base);
|
|
110
|
+
const requestFn = url.protocol === 'https:' ? https.request : http.request;
|
|
111
|
+
const headers = {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
'Content-Length': Buffer.byteLength(body),
|
|
114
|
+
'Accept-Encoding': 'gzip',
|
|
115
|
+
};
|
|
116
|
+
// Identify the caller to the pnpr server's access gate so protected
|
|
117
|
+
// packages resolve and the per-user grant table keys on the right user.
|
|
118
|
+
if (authorization != null) {
|
|
119
|
+
headers.Authorization = authorization;
|
|
120
|
+
}
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const req = requestFn(url, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
timeout: REQUEST_TIMEOUT,
|
|
125
|
+
headers,
|
|
126
|
+
}, (res) => {
|
|
127
|
+
const chunks = [];
|
|
128
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
129
|
+
res.on('end', () => {
|
|
130
|
+
const raw = Buffer.concat(chunks);
|
|
131
|
+
// The server gzips both the install body and its JSON error bodies
|
|
132
|
+
// (e.g. a 401/403 access denial), so decompress *before* branching
|
|
133
|
+
// on the status code — otherwise an error surfaces as binary
|
|
134
|
+
// garbage instead of the server's message. Skip it only when the
|
|
135
|
+
// HTTP stack already decompressed (no gzip magic bytes).
|
|
136
|
+
const finish = (body) => {
|
|
137
|
+
if (res.statusCode !== 200) {
|
|
138
|
+
reject(new Error(`pnpr server responded with ${res.statusCode}: ${body.toString('utf-8')}`));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
resolve(body);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
if (res.headers['content-encoding'] === 'gzip' || (raw[0] === 0x1f && raw[1] === 0x8b)) {
|
|
145
|
+
gunzip(raw, (err, decompressed) => {
|
|
146
|
+
if (err)
|
|
147
|
+
reject(err);
|
|
148
|
+
else
|
|
149
|
+
finish(decompressed);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
finish(raw);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
res.on('error', reject);
|
|
157
|
+
});
|
|
158
|
+
req.on('timeout', () => {
|
|
159
|
+
req.destroy(new Error(`pnpr server request timed out after ${REQUEST_TIMEOUT / 1000}s (${registryUrl})`));
|
|
160
|
+
});
|
|
161
|
+
req.on('error', (err) => {
|
|
162
|
+
if (err.code === 'ECONNREFUSED') {
|
|
163
|
+
reject(new Error(`Could not connect to pnpr server at ${registryUrl}. Is the server running?`));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
reject(err);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
req.write(body);
|
|
170
|
+
req.end();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
export function writeRawIndexEntries(indexEntries, storeIndex) {
|
|
174
|
+
const writes = indexEntries.filter(({ key }) => !storeIndex.has(key));
|
|
175
|
+
if (writes.length > 0) {
|
|
176
|
+
storeIndex.setRawMany(writes);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=fetchFromPnpmRegistry.js.map
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LockfileObject } from '@pnpm/lockfile.types';
|
|
2
|
+
export interface ResponseMetadata {
|
|
3
|
+
lockfile: LockfileObject;
|
|
4
|
+
stats: {
|
|
5
|
+
totalPackages: number;
|
|
6
|
+
alreadyInStore: number;
|
|
7
|
+
packagesToFetch: number;
|
|
8
|
+
filesInNewPackages: number;
|
|
9
|
+
filesAlreadyInCafs: number;
|
|
10
|
+
filesToDownload: number;
|
|
11
|
+
downloadBytes: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
package/lib/protocol.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pnpm/pnpr.client",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Client for the pnpr server — sends store state, receives resolved lockfile and missing files",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pnpm",
|
|
7
|
+
"pnpm11"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"funding": "https://opencollective.com/pnpm",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/pnpm/pnpm/tree/main/pnpr/client"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/pnpm/pnpm/tree/main/pnpr/client#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/pnpm/pnpm/issues"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "lib/index.js",
|
|
21
|
+
"types": "lib/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./lib/index.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"lib",
|
|
27
|
+
"!*.map"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@pnpm/store.index": "1100.1.0",
|
|
31
|
+
"@pnpm/lockfile.fs": "1100.1.3",
|
|
32
|
+
"@pnpm/store.cafs": "1100.1.8",
|
|
33
|
+
"@pnpm/lockfile.types": "1100.0.9"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@pnpm/logger": "^1001.0.1",
|
|
37
|
+
"@pnpm/worker": "^1100.1.9"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@pnpm/types": "1101.3.0",
|
|
41
|
+
"@pnpm/pnpr.client": "1.1.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22.13"
|
|
45
|
+
},
|
|
46
|
+
"jest": {
|
|
47
|
+
"preset": "@pnpm/jest-config"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"start": "tsgo --watch",
|
|
51
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
52
|
+
"test": "pn compile",
|
|
53
|
+
"compile": "tsgo --build && pn lint --fix"
|
|
54
|
+
}
|
|
55
|
+
}
|