@steve31415/baselib 3.4.0 → 3.5.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/README.md +7 -2
- package/dist/bin/release.d.ts +2 -0
- package/dist/bin/release.js +16 -0
- package/dist/ops/gcp-auth.d.ts +14 -2
- package/dist/ops/gcp-auth.js +14 -2
- package/dist/ops/sql.js +1 -1
- package/dist/release.d.ts +15 -0
- package/dist/release.js +81 -0
- package/dist/slow-fetch.d.ts +21 -0
- package/dist/slow-fetch.js +87 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@ What it provides and why: `docs/SPEC.md`. How it's put together:
|
|
|
6
6
|
`~/migration/research/base-services-design.md` (step 5).
|
|
7
7
|
|
|
8
8
|
Server subpath exports: `config`, `log`, `auth`, `s2s`, `db`, `http`, `sync`,
|
|
9
|
-
`app-update`, `llm`, and `email`. Browser exports: `log-browser`, `rum`,
|
|
10
|
-
`app-update-browser`.
|
|
9
|
+
`app-update`, `llm`, and `email`. Browser exports: `log-browser`, `rum`,
|
|
10
|
+
`slow-fetch`, `sync-browser`, and `app-update-browser`.
|
|
11
11
|
|
|
12
12
|
`sync` provides the Postgres event-log and server protocol primitives;
|
|
13
13
|
`sync-browser` provides the IndexedDB outbox and offline-first client.
|
|
@@ -34,4 +34,9 @@ deploy (`~/plasticine-way/docs/OPERATIONS.md`, "Deployment safety").
|
|
|
34
34
|
first step of any production investigation (same doc, "Investigating
|
|
35
35
|
production state").
|
|
36
36
|
|
|
37
|
+
Releasing: bump `version`, commit, and run `npm run deploy` (Coder runs it
|
|
38
|
+
automatically when integrating a baselib branch) — it pushes the `v<version>`
|
|
39
|
+
tag, whose workflow gates on verify and publishes via OIDC, and waits for the
|
|
40
|
+
registry (`~/plasticine-way/docs/OPERATIONS.md`, "Publishing baselib").
|
|
41
|
+
|
|
37
42
|
Reflects Plasticine Way commit: see `docs/IMPL.md` footer.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// baselib's `npm run deploy` — release the checked-out version (tag push +
|
|
3
|
+
// wait for the registry; the tag-triggered workflow does the actual
|
|
4
|
+
// publish). All logic lives in ../release.ts.
|
|
5
|
+
import { realRunner } from '../deploy/exec.js';
|
|
6
|
+
import { release } from '../release.js';
|
|
7
|
+
try {
|
|
8
|
+
await release(realRunner, {
|
|
9
|
+
log: (m) => console.log(m),
|
|
10
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
package/dist/ops/gcp-auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type AuthClient } from 'google-auth-library';
|
|
2
2
|
import type { Runner } from '../deploy/types.js';
|
|
3
3
|
export type GcpCredential = {
|
|
4
4
|
kind: 'adc';
|
|
@@ -22,4 +22,16 @@ export interface CredentialDeps {
|
|
|
22
22
|
export declare function resolveGcpCredential(deps?: CredentialDeps): Promise<GcpCredential>;
|
|
23
23
|
/** Short human description for the tool's progress line. */
|
|
24
24
|
export declare function describeCredential(cred: GcpCredential): string;
|
|
25
|
-
|
|
25
|
+
/** The client the connector will mint the IAM login token from. Always a
|
|
26
|
+
* concrete AuthClient (JWT for a service-account key, UserRefreshClient for
|
|
27
|
+
* an authorized-user ADC file, OAuth2Client around a ready token), never a
|
|
28
|
+
* GoogleAuth: the connector tells the two apart with `instanceof GoogleAuth`
|
|
29
|
+
* against ITS copy of google-auth-library, and when a consumer's install
|
|
30
|
+
* hoists a different copy than baselib's (coder2, 2026-09-04 — connector
|
|
31
|
+
* 1.12 wants ^11, baselib ^10) a GoogleAuth of ours fails that check, gets
|
|
32
|
+
* wrapped as if it were an AuthClient, and the wrapper's getAccessToken()
|
|
33
|
+
* reads `.token` off the bare string GoogleAuth returns: "Failed to get
|
|
34
|
+
* access token for automatic IAM authentication". An AuthClient crosses the
|
|
35
|
+
* package boundary by duck typing alone, so the client carries both Cloud
|
|
36
|
+
* SQL scopes itself rather than relying on the wrapper's. */
|
|
37
|
+
export declare function authClientFor(cred: GcpCredential): Promise<AuthClient>;
|
package/dist/ops/gcp-auth.js
CHANGED
|
@@ -54,11 +54,23 @@ export function describeCredential(cred) {
|
|
|
54
54
|
return `gcloud access token for ${cred.account}`;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
/** The client the connector will mint the IAM login token from. Always a
|
|
58
|
+
* concrete AuthClient (JWT for a service-account key, UserRefreshClient for
|
|
59
|
+
* an authorized-user ADC file, OAuth2Client around a ready token), never a
|
|
60
|
+
* GoogleAuth: the connector tells the two apart with `instanceof GoogleAuth`
|
|
61
|
+
* against ITS copy of google-auth-library, and when a consumer's install
|
|
62
|
+
* hoists a different copy than baselib's (coder2, 2026-09-04 — connector
|
|
63
|
+
* 1.12 wants ^11, baselib ^10) a GoogleAuth of ours fails that check, gets
|
|
64
|
+
* wrapped as if it were an AuthClient, and the wrapper's getAccessToken()
|
|
65
|
+
* reads `.token` off the bare string GoogleAuth returns: "Failed to get
|
|
66
|
+
* access token for automatic IAM authentication". An AuthClient crosses the
|
|
67
|
+
* package boundary by duck typing alone, so the client carries both Cloud
|
|
68
|
+
* SQL scopes itself rather than relying on the wrapper's. */
|
|
69
|
+
export async function authClientFor(cred) {
|
|
58
70
|
if (cred.kind === 'access-token') {
|
|
59
71
|
const client = new OAuth2Client();
|
|
60
72
|
client.setCredentials({ access_token: cred.token });
|
|
61
73
|
return client;
|
|
62
74
|
}
|
|
63
|
-
return new GoogleAuth({ keyFilename: cred.path, scopes: CLOUD_SQL_SCOPES });
|
|
75
|
+
return new GoogleAuth({ keyFilename: cred.path, scopes: CLOUD_SQL_SCOPES }).getClient();
|
|
64
76
|
}
|
package/dist/ops/sql.js
CHANGED
|
@@ -104,7 +104,7 @@ function outputTypes() {
|
|
|
104
104
|
export async function connectCloudSql(target, log, runner) {
|
|
105
105
|
const cred = await resolveGcpCredential({ runner });
|
|
106
106
|
log(`connecting to ${target.instance} db=${target.database} as ${target.user} via ${describeCredential(cred)}`);
|
|
107
|
-
const { options: connection, connector } = await cloudSqlClientOptions(target.instance, authClientFor(cred));
|
|
107
|
+
const { options: connection, connector } = await cloudSqlClientOptions(target.instance, await authClientFor(cred));
|
|
108
108
|
const client = new pg.Client({
|
|
109
109
|
...connection,
|
|
110
110
|
user: target.user,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Runner } from './deploy/types.js';
|
|
2
|
+
export declare const PUBLISH_POLL_MS = 20000;
|
|
3
|
+
export declare const PUBLISH_TIMEOUT_MS: number;
|
|
4
|
+
export interface ReleaseIo {
|
|
5
|
+
log: (message: string) => void;
|
|
6
|
+
sleep: (ms: number) => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export interface ReleaseOptions {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
pollMs?: number;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** Release the checked-out version; throws (with a pointer to the publish
|
|
14
|
+
* workflow) on drift or when the registry never serves it. */
|
|
15
|
+
export declare function release(runner: Runner, io: ReleaseIo, opts?: ReleaseOptions): Promise<void>;
|
package/dist/release.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// baselib's own `npm run deploy`: release the version in package.json by
|
|
2
|
+
// pushing the `v<version>` tag and waiting for the registry to serve it.
|
|
3
|
+
// The actual publish stays where it is — the tag triggers
|
|
4
|
+
// .github/workflows/publish.yml, which gates on `npm run verify` and
|
|
5
|
+
// publishes via npm Trusted Publishing (OIDC) — so this script adds no
|
|
6
|
+
// publish authority; it only automates the tag push that used to be
|
|
7
|
+
// Steve's manual step (decision 2026-09-06). Giving baselib a `deploy`
|
|
8
|
+
// script also slots releases into Coder's ordered per-project deploys:
|
|
9
|
+
// a task can bump baselib and, once it serves, deploy consumers after it.
|
|
10
|
+
//
|
|
11
|
+
// Idempotent and drift-guarded:
|
|
12
|
+
// - version already on the registry, no source drift since its tag → no-op
|
|
13
|
+
// (docs-only changes release nothing);
|
|
14
|
+
// - version already on the registry but src/ or package.json changed since
|
|
15
|
+
// its tag → FAIL loudly ("bump the version") instead of silently shipping
|
|
16
|
+
// nothing;
|
|
17
|
+
// - tag already pushed but not yet on the registry (a publish in flight, or
|
|
18
|
+
// a rerun after a workflow failure) → skip the push, wait for the registry.
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
20
|
+
import { must } from './deploy/exec.js';
|
|
21
|
+
const PACKAGE = '@steve31415/baselib';
|
|
22
|
+
const ACTIONS_URL = 'https://github.com/plasticine-apps/baselib/actions';
|
|
23
|
+
export const PUBLISH_POLL_MS = 20_000;
|
|
24
|
+
export const PUBLISH_TIMEOUT_MS = 15 * 60_000;
|
|
25
|
+
// Files whose change must not go unreleased. docs/, test/, and workflow
|
|
26
|
+
// changes ride along with the next real release.
|
|
27
|
+
const RELEASED_PATHS = ['src', 'package.json', 'package-lock.json'];
|
|
28
|
+
/** The version on the registry for `spec`, or null when absent. */
|
|
29
|
+
async function publishedVersion(runner, version, cwd) {
|
|
30
|
+
const r = await runner('npm', ['view', `${PACKAGE}@${version}`, 'version'], { cwd });
|
|
31
|
+
const out = r.stdout.trim();
|
|
32
|
+
return r.code === 0 && out === version ? version : null;
|
|
33
|
+
}
|
|
34
|
+
async function tagOnOrigin(runner, tag, cwd) {
|
|
35
|
+
const r = await must(runner, 'git', ['ls-remote', 'origin', `refs/tags/${tag}`], { cwd });
|
|
36
|
+
return r.stdout.trim().length > 0;
|
|
37
|
+
}
|
|
38
|
+
/** Release the checked-out version; throws (with a pointer to the publish
|
|
39
|
+
* workflow) on drift or when the registry never serves it. */
|
|
40
|
+
export async function release(runner, io, opts = {}) {
|
|
41
|
+
const cwd = opts.cwd;
|
|
42
|
+
const pkg = JSON.parse(readFileSync(`${cwd ?? '.'}/package.json`, 'utf8'));
|
|
43
|
+
if (!pkg.version)
|
|
44
|
+
throw new Error('package.json has no version');
|
|
45
|
+
const version = pkg.version;
|
|
46
|
+
const tag = `v${version}`;
|
|
47
|
+
if (await publishedVersion(runner, version, cwd)) {
|
|
48
|
+
// Already released. Guard the footgun: released paths changed since the
|
|
49
|
+
// tag means someone forgot the bump — fail rather than silently ship
|
|
50
|
+
// nothing. (Ensure the tag exists locally first; a fresh clone may not
|
|
51
|
+
// have fetched it.)
|
|
52
|
+
const local = await runner('git', ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`], { cwd });
|
|
53
|
+
if (local.code !== 0)
|
|
54
|
+
await must(runner, 'git', ['fetch', 'origin', 'tag', tag], { cwd });
|
|
55
|
+
const drift = await runner('git', ['diff', '--quiet', tag, 'HEAD', '--', ...RELEASED_PATHS], { cwd });
|
|
56
|
+
if (drift.code !== 0) {
|
|
57
|
+
throw new Error(`${RELEASED_PATHS.join('/')} changed since ${tag} was released — bump the version in package.json to release the change`);
|
|
58
|
+
}
|
|
59
|
+
io.log(`${PACKAGE}@${version} is already on the registry and matches ${tag}; nothing to release`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (await tagOnOrigin(runner, tag, cwd)) {
|
|
63
|
+
io.log(`tag ${tag} is already on origin (publish in flight or previously failed); waiting for the registry`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
await must(runner, 'git', ['tag', tag], { cwd });
|
|
67
|
+
await must(runner, 'git', ['push', 'origin', tag], { cwd });
|
|
68
|
+
io.log(`pushed ${tag}; the publish workflow (verify gate + OIDC publish) is running`);
|
|
69
|
+
}
|
|
70
|
+
const timeoutMs = opts.timeoutMs ?? PUBLISH_TIMEOUT_MS;
|
|
71
|
+
const pollMs = opts.pollMs ?? PUBLISH_POLL_MS;
|
|
72
|
+
for (let waited = 0; waited < timeoutMs; waited += pollMs) {
|
|
73
|
+
await io.sleep(pollMs);
|
|
74
|
+
if (await publishedVersion(runner, version, cwd)) {
|
|
75
|
+
io.log(`${PACKAGE}@${version} is live on the registry`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
io.log(`waiting for ${PACKAGE}@${version} on the registry (${Math.round((waited + pollMs) / 1000)}s)`);
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`${PACKAGE}@${version} did not appear on the registry within ${Math.round(timeoutMs / 60_000)} min — check the publish workflow: ${ACTIONS_URL}`);
|
|
81
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Logger } from './log-core.js';
|
|
2
|
+
export declare const DEFAULT_SLOW_FETCH_MS = 2000;
|
|
3
|
+
export declare const DEFAULT_MAX_REPORTS_PER_PAGE = 20;
|
|
4
|
+
export interface SlowFetchOptions {
|
|
5
|
+
/** Successful fetches at or above this many ms are logged (default 2000). */
|
|
6
|
+
slowMs?: number;
|
|
7
|
+
/** Ceiling on reports per page life, slow and failed combined (default 20). */
|
|
8
|
+
maxReportsPerPage?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Wire the reporter once at startup; until then reportFetchOutcome no-ops
|
|
11
|
+
* (so unit tests exercising the fetch wrapper stay silent). Also widens the
|
|
12
|
+
* resource-timing buffer and clears it when full: a polling app fills the
|
|
13
|
+
* default 250-entry buffer within minutes, after which timing lookups come
|
|
14
|
+
* back empty for the rest of the tab's life. Nothing else reads resource
|
|
15
|
+
* entries; if a consumer does, it should own the buffer instead. */
|
|
16
|
+
export declare function initSlowFetchLogging(l: Logger, opts?: SlowFetchOptions): void;
|
|
17
|
+
/** Call on every fetch settle from the app's fetch wrapper. Logs the slow
|
|
18
|
+
* successes and the failures; fast successes and deliberate aborts are
|
|
19
|
+
* silent. `status` is the HTTP status on success, null on rejection;
|
|
20
|
+
* `error` is the thrown value on rejection, omitted on success. */
|
|
21
|
+
export declare function reportFetchOutcome(path: string, method: string, startedMs: number, status: number | null, error?: unknown): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Diagnostic timing for slow or failed browser fetches. A request can stall
|
|
2
|
+
// entirely client-side — dead connection reuse after an idle gap, QUIC→TCP
|
|
3
|
+
// fallback, DNS — where server logs show nothing: a coder2 enqueue on
|
|
4
|
+
// 2026-09-06 took ~10 s at the browser against 0.94 s on the server, and no
|
|
5
|
+
// log could attribute the gap. An app wires reportFetchOutcome into its own
|
|
6
|
+
// fetch wrapper (the one place all API traffic settles); any fetch slower
|
|
7
|
+
// than the threshold, or one that rejects, ships a browser log carrying the
|
|
8
|
+
// Resource Timing milestones — connection setup vs first-byte wait vs
|
|
9
|
+
// download, plus the negotiated protocol (h2 vs h3) — which is exactly the
|
|
10
|
+
// split those incidents need.
|
|
11
|
+
//
|
|
12
|
+
// Browser-only module — do not import server-side.
|
|
13
|
+
import { serializeError } from './log-core.js';
|
|
14
|
+
export const DEFAULT_SLOW_FETCH_MS = 2000;
|
|
15
|
+
// A broken network could make every poll slow: cap the reports per page life.
|
|
16
|
+
export const DEFAULT_MAX_REPORTS_PER_PAGE = 20;
|
|
17
|
+
let logger = null;
|
|
18
|
+
let slowMs = DEFAULT_SLOW_FETCH_MS;
|
|
19
|
+
let maxReports = DEFAULT_MAX_REPORTS_PER_PAGE;
|
|
20
|
+
let reported = 0;
|
|
21
|
+
/** Wire the reporter once at startup; until then reportFetchOutcome no-ops
|
|
22
|
+
* (so unit tests exercising the fetch wrapper stay silent). Also widens the
|
|
23
|
+
* resource-timing buffer and clears it when full: a polling app fills the
|
|
24
|
+
* default 250-entry buffer within minutes, after which timing lookups come
|
|
25
|
+
* back empty for the rest of the tab's life. Nothing else reads resource
|
|
26
|
+
* entries; if a consumer does, it should own the buffer instead. */
|
|
27
|
+
export function initSlowFetchLogging(l, opts = {}) {
|
|
28
|
+
logger = l;
|
|
29
|
+
slowMs = opts.slowMs ?? DEFAULT_SLOW_FETCH_MS;
|
|
30
|
+
maxReports = opts.maxReportsPerPage ?? DEFAULT_MAX_REPORTS_PER_PAGE;
|
|
31
|
+
reported = 0;
|
|
32
|
+
performance.setResourceTimingBufferSize?.(1000);
|
|
33
|
+
performance.addEventListener?.('resourcetimingbufferfull', () => performance.clearResourceTimings());
|
|
34
|
+
}
|
|
35
|
+
/** The fetch's Resource Timing milestones, ms relative to its start; null
|
|
36
|
+
* when the entry is gone (a failed request records none, or the buffer was
|
|
37
|
+
* cleared) or the environment lacks the API. A milestone of null means the
|
|
38
|
+
* phase was absent — e.g. no connect_* on a reused connection. */
|
|
39
|
+
function timingFor(url, startedMs, endedMs) {
|
|
40
|
+
if (typeof performance.getEntriesByName !== 'function')
|
|
41
|
+
return null;
|
|
42
|
+
const entries = performance.getEntriesByName(url, 'resource');
|
|
43
|
+
// Polls reuse URLs: take the last entry that started in this fetch's window.
|
|
44
|
+
const entry = entries.filter((e) => e.startTime >= startedMs - 50 && e.startTime <= endedMs).at(-1);
|
|
45
|
+
if (!entry)
|
|
46
|
+
return null;
|
|
47
|
+
const rel = (t) => (t === 0 ? null : Math.round(t - entry.startTime));
|
|
48
|
+
return {
|
|
49
|
+
protocol: entry.nextHopProtocol || null,
|
|
50
|
+
fetch_start_ms: rel(entry.fetchStart),
|
|
51
|
+
domain_lookup_start_ms: rel(entry.domainLookupStart),
|
|
52
|
+
connect_start_ms: rel(entry.connectStart),
|
|
53
|
+
secure_connection_start_ms: rel(entry.secureConnectionStart),
|
|
54
|
+
connect_end_ms: rel(entry.connectEnd),
|
|
55
|
+
request_start_ms: rel(entry.requestStart),
|
|
56
|
+
response_start_ms: rel(entry.responseStart),
|
|
57
|
+
response_end_ms: rel(entry.responseEnd),
|
|
58
|
+
transfer_size: entry.transferSize,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Call on every fetch settle from the app's fetch wrapper. Logs the slow
|
|
62
|
+
* successes and the failures; fast successes and deliberate aborts are
|
|
63
|
+
* silent. `status` is the HTTP status on success, null on rejection;
|
|
64
|
+
* `error` is the thrown value on rejection, omitted on success. */
|
|
65
|
+
export function reportFetchOutcome(path, method, startedMs, status, error) {
|
|
66
|
+
if (!logger || reported >= maxReports)
|
|
67
|
+
return;
|
|
68
|
+
// Deliberate aborts (a page navigation cancelling its poll) are not failures.
|
|
69
|
+
if (error instanceof DOMException && error.name === 'AbortError')
|
|
70
|
+
return;
|
|
71
|
+
const endedMs = performance.now();
|
|
72
|
+
const durationMs = Math.round(endedMs - startedMs);
|
|
73
|
+
const failed = error !== undefined;
|
|
74
|
+
if (!failed && durationMs < slowMs)
|
|
75
|
+
return;
|
|
76
|
+
reported += 1;
|
|
77
|
+
logger.warn(failed ? 'fetch_failed' : 'slow_fetch', {
|
|
78
|
+
path,
|
|
79
|
+
method,
|
|
80
|
+
status,
|
|
81
|
+
duration_ms: durationMs,
|
|
82
|
+
online: navigator.onLine,
|
|
83
|
+
visibility: document.visibilityState,
|
|
84
|
+
...(failed ? { error: serializeError(error) } : {}),
|
|
85
|
+
timing: timingFor(new URL(path, location.href).href, startedMs, endedMs),
|
|
86
|
+
});
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve31415/baselib",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Plasticine new-world shared platform library: logging, auth, service-to-service auth, db, HTTP, sync, app updates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
"types": "./dist/rum.d.ts",
|
|
46
46
|
"default": "./dist/rum.js"
|
|
47
47
|
},
|
|
48
|
+
"./slow-fetch": {
|
|
49
|
+
"types": "./dist/slow-fetch.d.ts",
|
|
50
|
+
"default": "./dist/slow-fetch.js"
|
|
51
|
+
},
|
|
48
52
|
"./sync": {
|
|
49
53
|
"types": "./dist/sync/index.d.ts",
|
|
50
54
|
"default": "./dist/sync/index.js"
|
|
@@ -82,6 +86,7 @@
|
|
|
82
86
|
},
|
|
83
87
|
"scripts": {
|
|
84
88
|
"build": "tsc -p tsconfig.build.json",
|
|
89
|
+
"deploy": "tsx src/bin/release.ts",
|
|
85
90
|
"prepack": "npm run build",
|
|
86
91
|
"typecheck": "tsc --noEmit",
|
|
87
92
|
"test": "vitest run",
|