@sixtyninecommerce/admin 0.5.0 → 0.7.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.
Files changed (40) hide show
  1. package/README.md +424 -2
  2. package/dist/apps/assertion.d.ts +30 -3
  3. package/dist/apps/assertion.js +40 -3
  4. package/dist/apps/config.d.ts +39 -0
  5. package/dist/apps/config.js +22 -0
  6. package/dist/apps/credential.d.ts +5 -0
  7. package/dist/apps/credential.js +55 -4
  8. package/dist/apps/index.d.ts +11 -0
  9. package/dist/client.d.ts +56 -1
  10. package/dist/client.js +39 -2
  11. package/dist/custom-objects/index.d.ts +66 -0
  12. package/dist/custom-objects/index.js +48 -0
  13. package/dist/customers/index.d.ts +225 -0
  14. package/dist/customers/index.js +279 -0
  15. package/dist/documents/index.d.ts +432 -0
  16. package/dist/documents/index.js +289 -0
  17. package/dist/index.d.ts +35 -2
  18. package/dist/index.js +16 -1
  19. package/dist/jobs/index.d.ts +143 -0
  20. package/dist/jobs/index.js +132 -0
  21. package/dist/metafields/index.d.ts +291 -0
  22. package/dist/metafields/index.js +141 -0
  23. package/dist/orders/index.d.ts +106 -0
  24. package/dist/orders/index.js +115 -0
  25. package/dist/orders/surcharges.d.ts +121 -0
  26. package/dist/orders/surcharges.js +110 -0
  27. package/dist/payments/index.d.ts +103 -0
  28. package/dist/payments/index.js +55 -0
  29. package/dist/payments/providers.d.ts +281 -0
  30. package/dist/payments/providers.js +205 -0
  31. package/dist/platform/provision-store.d.ts +9 -26
  32. package/dist/platform/provision-store.js +8 -66
  33. package/dist/tax/index.d.ts +128 -0
  34. package/dist/tax/index.js +101 -0
  35. package/dist/tenant/capabilities.d.ts +119 -0
  36. package/dist/tenant/capabilities.js +59 -0
  37. package/dist/types.d.ts +20 -3
  38. package/dist/uploads/index.d.ts +146 -0
  39. package/dist/uploads/index.js +72 -0
  40. package/package.json +3 -3
@@ -0,0 +1,119 @@
1
+ import { type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call } from '../types.js';
3
+ /** One capability's state on the calling tenant's channel. */
4
+ export interface Capability {
5
+ /**
6
+ * The underlying objects exist here. For a feature a merchant switches on —
7
+ * quotes, returns, reviews, the age gate, subscription plans, B2B price
8
+ * lists — that means SWITCHED ON, not "supported by the platform".
9
+ */
10
+ provisioned: boolean;
11
+ /** No hard blocker in the configuration. A degradation leaves this true. */
12
+ configured: boolean;
13
+ /**
14
+ * The tenant holds this plugin's entitlement.
15
+ *
16
+ * FALSE ON EVERY CORE CAPABILITY, and that is not a bug: the entitlement
17
+ * registry has rows only for the entitlement-gated plugins of the catalogue,
18
+ * and `catalog`, `search`, `routes` and the rest are the platform itself.
19
+ * Branch on {@link ready}, never on this alone.
20
+ */
21
+ entitled: boolean;
22
+ /** The answer to "may I use this": provisioned, configured, and licensed. */
23
+ ready: boolean;
24
+ /** Capability keys this one needs first — what to fix when it is not ready. */
25
+ dependsOn: string[];
26
+ /**
27
+ * The server's evidence for anything short of a clean pass, in the words the
28
+ * platform's own readiness preflight uses ("search returned 0 hits — reindex
29
+ * Meilisearch for this channel"). Never a UI flag, and never a sentence this
30
+ * SDK composed.
31
+ *
32
+ * NON-EMPTY WITH `ready: true` MEANS DEGRADED, not broken — a storefront that
33
+ * renders anyway is correct, and a merchant still wants to see it. A feature
34
+ * deliberately switched off carries no blocker at all: nothing is wrong.
35
+ */
36
+ blockers: string[];
37
+ /** A field added after this SDK release arrives camelised and untouched. */
38
+ [extra: string]: unknown;
39
+ }
40
+ /**
41
+ * What the tenant's channel can do, per capability key.
42
+ *
43
+ * KEYS ARE DATA. `content_pages`, `age_gate`, `b2b_pricing`, `payment-links`
44
+ * are identifiers the platform matches literally, so they reach you in their
45
+ * wire spelling and are never camelised — the same rule the meta catalogue's
46
+ * data-keyed maps follow. The FIELDS inside each entry are camelised.
47
+ *
48
+ * Core keys: `catalog`, `search`, `routes`, `menus`, `content_pages`,
49
+ * `checkout`, `branding`, `age_gate`, `reviews`, `subscriptions`,
50
+ * `b2b_pricing`, `quotes`, `returns`. Beside them, one key per
51
+ * entitlement-gated plugin in the platform catalogue (`mollie`, `stripe`,
52
+ * `myparcel`, `hendriks`, `afas`, `pos`, …).
53
+ *
54
+ * THE SET GROWS, so read an absent key as UNKNOWN and never as `false`. A
55
+ * capability whose rules are not yet settled is held back rather than published
56
+ * wrong, and `b2b_pricing` — which shipped one release later than the rest, for
57
+ * exactly that reason — is the precedent rather than the exception.
58
+ */
59
+ export interface CapabilityManifest {
60
+ channelId: string;
61
+ channelCode: string;
62
+ /** ISO-8601 with an offset — when the probes ran, not when this was served. */
63
+ generatedAt: string;
64
+ capabilities: Record<string, Capability>;
65
+ /**
66
+ * The tenant's installed apps. Always empty until the app registry ships, so
67
+ * there is no element shape yet — declaring one here would pin a contract the
68
+ * apps work has not written.
69
+ */
70
+ installedApps: never[];
71
+ /**
72
+ * The `aud` an app assertion must carry to be exchanged at this deployment —
73
+ * its public Vendure origin. Copy it VERBATIM into the claim; re-deriving it
74
+ * from a URL you hold is the drift the platform compares against as a plain
75
+ * string. `createAppCredential` already does this for you, so this member is
76
+ * for a caller signing assertions by hand.
77
+ *
78
+ * Camelised from the wire's `assertion_audience`, like every other member
79
+ * here. `null` means the plugins service cannot state one — which does NOT
80
+ * mean the exchange accepts an assertion without `aud`, since the two run as
81
+ * separate deployment resources; ask the operator rather than omitting it.
82
+ *
83
+ * Optional because a platform older than this field simply does not send it.
84
+ */
85
+ assertionAudience?: string | null;
86
+ /** A member added after this SDK release arrives camelised and untouched. */
87
+ [extra: string]: unknown;
88
+ }
89
+ /**
90
+ * `admin.tenant.capabilities()` — what this tenant's channel can actually do.
91
+ *
92
+ * One read replaces a dozen probes: whether the catalogue has products, whether
93
+ * search is indexed, whether checkout has an enabled method on both sides,
94
+ * which integrations are licensed AND connected, and for anything less than
95
+ * ready, the server's own evidence.
96
+ *
97
+ * Reading needs any permission on the channel — an admin credential always has
98
+ * one. It is cached 60 s per channel by the service, so polling it costs
99
+ * nothing; {@link CapabilitiesModule.refresh} is the escape when you have just
100
+ * changed something and want to see it.
101
+ */
102
+ export interface CapabilitiesModule {
103
+ (options?: Call): Promise<CapabilityManifest>;
104
+ /**
105
+ * Rebuild the manifest now, ignoring the service's freshness window.
106
+ *
107
+ * Needs `UpdateSettings`: it makes the server re-probe three services, so it
108
+ * is not something a read-only credential may trigger at will.
109
+ */
110
+ refresh(options?: Call): Promise<CapabilityManifest>;
111
+ }
112
+ /**
113
+ * Camelise each capability entry's OWN fields (`depends_on` → `dependsOn`) and
114
+ * leave the map's keys alone. A field this release has never seen is renamed
115
+ * and passed through, which is the half that matters — nothing else would
116
+ * protect it.
117
+ */
118
+ export declare function projectManifest(body: unknown): CapabilityManifest;
119
+ export declare function createCapabilitiesModule(transport: Transport): CapabilitiesModule;
@@ -0,0 +1,59 @@
1
+ import { PermissionDeniedError } from '@sixtyninecommerce/core';
2
+ /**
3
+ * The `capabilities` MAP is handed back verbatim by the transport, because its
4
+ * keys are capability identifiers rather than structure. Camelising them would
5
+ * be invisible damage: `content_pages` would simply read back as `contentPages`
6
+ * and match no key the platform has ever published.
7
+ */
8
+ const PRESERVE_CAPABILITIES = new Set(['capabilities']);
9
+ function isRecord(value) {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ }
12
+ function snakeToCamel(key) {
13
+ return key.replace(/_+([a-z0-9])/g, (_match, char) => char.toUpperCase());
14
+ }
15
+ /**
16
+ * Camelise each capability entry's OWN fields (`depends_on` → `dependsOn`) and
17
+ * leave the map's keys alone. A field this release has never seen is renamed
18
+ * and passed through, which is the half that matters — nothing else would
19
+ * protect it.
20
+ */
21
+ export function projectManifest(body) {
22
+ if (!isRecord(body) || !isRecord(body.capabilities)) {
23
+ // The transport has already turned a non-2xx into a PlatformError, so
24
+ // anything else here is a service answering 200 with a body this module
25
+ // cannot read — worth saying plainly rather than crashing two frames later.
26
+ throw new TypeError('GET /capabilities did not answer with a capability manifest');
27
+ }
28
+ const capabilities = {};
29
+ for (const [key, state] of Object.entries(body.capabilities)) {
30
+ if (!isRecord(state))
31
+ continue;
32
+ if (!('provisioned' in state)) {
33
+ // The public projection: `{ ready }` and nothing else. The service serves
34
+ // it to a principal it did not accept for this channel, and the status is
35
+ // 200, so nothing else in the hierarchy would tell the caller that their
36
+ // credential — not their request — is the problem.
37
+ throw new PermissionDeniedError({
38
+ code: 'CAPABILITIES_PROJECTION_UNAVAILABLE',
39
+ message: 'The service returned the anonymous capability projection, so this credential was not accepted for the channel — it is invalid or expired, or its principal holds no permission there. Only `ready` would be available.',
40
+ status: 403,
41
+ service: 'plugins',
42
+ });
43
+ }
44
+ const projected = {};
45
+ for (const [field, value] of Object.entries(state))
46
+ projected[snakeToCamel(field)] = value;
47
+ capabilities[key] = projected;
48
+ }
49
+ return { ...body, capabilities };
50
+ }
51
+ export function createCapabilitiesModule(transport) {
52
+ const read = async (options) => projectManifest(await transport.rest('plugins', { method: 'GET', path: '/capabilities' }, { ...options, preserveKeys: PRESERVE_CAPABILITIES }));
53
+ // A callable module: `capabilities()` is the read a consumer makes constantly
54
+ // and `capabilities.refresh()` the one they make rarely, so the common case
55
+ // stays the shortest thing to write (spec §6.6 names it exactly this way).
56
+ const capabilities = ((options) => read(options));
57
+ capabilities.refresh = async (options) => projectManifest(await transport.rest('plugins', { method: 'POST', path: '/capabilities/refresh' }, { ...options, preserveKeys: PRESERVE_CAPABILITIES }));
58
+ return capabilities;
59
+ }
package/dist/types.d.ts CHANGED
@@ -2,9 +2,26 @@ import type { ChannelInfo, TenantContext, TenantScope } from '@sixtyninecommerce
2
2
  /** Per-call options every admin method accepts (spec §9.2). */
3
3
  export interface Call {
4
4
  /**
5
- * Sent as `Idempotency-Key` on a REST write, and ignored on a GraphQL one —
6
- * the Admin API has no header for it, so an `apiTokens.*` write is never
7
- * deduplicated and a repeat is a second write.
5
+ * Sent as `Idempotency-Key` on a REST write. On a GraphQL one it is sent as
6
+ * an ARGUMENT — the Admin API has no header for it — and only the mutations
7
+ * that declare one act on it. Today those are the money mutations
8
+ * `payments.record`, `orders.park`, `orders.settle` and all three
9
+ * `orders.surcharges.*`, plus `payments.providers.completeOnboarding` and
10
+ * `payments.providers.terminals.provision`. Repeating one under the same key
11
+ * returns the first call's result instead of moving the money again; a
12
+ * DIFFERENT request under the same key is refused with
13
+ * `IDEMPOTENCY_CONFLICT`.
14
+ *
15
+ * Two deliberate exceptions on that list. `customers.erase` takes a key but
16
+ * RECORDS it for provenance rather than keying on it: that mutation is
17
+ * idempotent on the customer itself, which is the stronger key. And
18
+ * `payments.providers.startOnboarding` takes none at all, because its
19
+ * response carries a link that can be single-use and a stored response is
20
+ * re-served for 24 hours.
21
+ *
22
+ * Everything else — `apiTokens.*` and the rest of the GraphQL surface —
23
+ * still sends the key and acts on nothing, so a repeat there is a second
24
+ * write.
8
25
  *
9
26
  * On REST, three of this SDK's writes are deduplicated by the service itself:
10
27
  * `content.forms.upsertBySlug` (`PUT /forms/by-slug/:slug`),
@@ -0,0 +1,146 @@
1
+ import { type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call } from '../types.js';
3
+ /**
4
+ * What an upload is for. It decides which permission stages it and who may
5
+ * consume it.
6
+ *
7
+ * `asset` is the only purpose today. `bulk` and `document` arrive with the
8
+ * calls that consume them, each with its own storage decision — everything
9
+ * staged currently lands in the public asset bucket, which is right for a media
10
+ * asset and not for a customer export or an order attachment.
11
+ */
12
+ export type UploadPurpose = 'asset';
13
+ export type UploadStatus = 'staged' | 'consumed' | 'expired';
14
+ export interface StagedUpload {
15
+ id: string;
16
+ purpose: UploadPurpose;
17
+ status: UploadStatus;
18
+ mimeType: string;
19
+ /**
20
+ * The EXACT size the signature was computed for, as a string.
21
+ *
22
+ * A string because it is a 64-bit column and a JSON number is a double; use
23
+ * `Number(...)` if you need arithmetic, which is exact below 2^53.
24
+ */
25
+ sizeBytes: string;
26
+ /** The label you gave the file. It is never part of the object's location. */
27
+ filename: string | null;
28
+ /** When the RESERVATION lapses. After this nothing will accept the upload. */
29
+ expiresAt: string;
30
+ consumedAt: string | null;
31
+ createdAt: string;
32
+ }
33
+ export interface StagedUploadTicket extends StagedUpload {
34
+ /** PUT the bytes here. Single use. */
35
+ url: string;
36
+ method: 'PUT';
37
+ /**
38
+ * Send ALL of these, spelled exactly like this. They are part of the
39
+ * signature, so storage refuses a PUT that omits or changes one.
40
+ */
41
+ headers: Record<string, string>;
42
+ /**
43
+ * When the URL stops working — at most {@link StagedUpload.expiresAt}, and
44
+ * less than it when you re-staged an upload that was already running down.
45
+ */
46
+ urlExpiresAt: string;
47
+ }
48
+ export interface StageUploadInput {
49
+ purpose: UploadPurpose;
50
+ /** Checked against the platform allow-list and bound into the signature. */
51
+ mimeType: string;
52
+ /**
53
+ * The EXACT number of bytes you will send — not a maximum.
54
+ *
55
+ * A presigned PUT can only pin a precise `Content-Length`; there is no way to
56
+ * sign an upper bound. Storage refuses any other length, so this must be the
57
+ * real size of the body (`blob.size`, `buffer.length`, `stat.size`).
58
+ */
59
+ size: number;
60
+ /**
61
+ * Your own handle for this upload, making the call repeatable.
62
+ *
63
+ * Required. Staging is the call that hands out a URL, and a dropped response
64
+ * on it is the ordinary failure: without a token every retry would reserve
65
+ * another place and leave an object nobody ever claims. Staging twice with
66
+ * the same token returns the SAME upload; staging DIFFERENT terms under it is
67
+ * refused with `UPLOAD_TOKEN_CONFLICT`.
68
+ */
69
+ clientToken: string;
70
+ /** A label for humans. It never reaches the object's location. */
71
+ filename?: string;
72
+ }
73
+ /**
74
+ * Bytes this SDK knows how to PUT. A `Blob` in any modern runtime, or a
75
+ * `Uint8Array`/`ArrayBuffer` from Node.
76
+ */
77
+ export type UploadBody = Blob | ArrayBuffer | ArrayBufferView;
78
+ export interface ClaimAsAssetInput {
79
+ /** Place the asset in this CMS asset folder. */
80
+ folderId?: string;
81
+ altText?: string;
82
+ }
83
+ /**
84
+ * Signed direct-to-storage uploads (spec §6.11).
85
+ *
86
+ * **The shape of it.** You say what you are uploading; the platform decides
87
+ * WHERE it goes and signs exactly that. You never see or choose a path. The
88
+ * three steps are `stage` (reserve and get a URL), `put` (send the bytes
89
+ * straight to storage — they do not pass through the platform), and a consuming
90
+ * call that takes the upload's id.
91
+ *
92
+ * ```ts
93
+ * const bytes = await readFile('hero.png');
94
+ * const ticket = await admin.uploads.stage({
95
+ * purpose: 'asset',
96
+ * mimeType: 'image/png',
97
+ * size: bytes.length,
98
+ * clientToken: crypto.randomUUID(),
99
+ * filename: 'hero.png',
100
+ * });
101
+ * await admin.uploads.put(ticket, bytes);
102
+ * const asset = await admin.uploads.claimAsAsset(ticket.id, { altText: 'Our hero' });
103
+ * ```
104
+ *
105
+ * **The size is exact.** `size` is the byte count storage will accept and no
106
+ * other — not a ceiling. Sending a different number of bytes fails at storage
107
+ * with a signature error, so compute it from the body you are about to send.
108
+ *
109
+ * **One use, and a window scaled to the size.** A URL writes one object, once.
110
+ * The reservation lasts at least fifteen minutes, and longer for a large file —
111
+ * enough to send it at about 1 Mbit/s — because a window too short to upload in
112
+ * would expire between the PUT and the claim. `expiresAt` is the reservation;
113
+ * `urlExpiresAt` is the signature, and never outlives it. An upload nobody
114
+ * consumes expires and its bytes are deleted.
115
+ *
116
+ * **It is yours alone.** An upload is visible and usable only to the principal
117
+ * that staged it, on the store it was staged for. Anything else answers
118
+ * `UPLOAD_NOT_FOUND` — the same answer as an id that never existed.
119
+ *
120
+ * **Browsers need CORS.** `put` sends a cross-origin PUT to the object store,
121
+ * which requires CORS rules on the bucket that this platform does not
122
+ * configure. Server-side callers are unaffected.
123
+ */
124
+ export interface UploadsModule {
125
+ /** Reserve a place and get a signed URL to PUT one file to. */
126
+ stage(input: StageUploadInput, options?: Call): Promise<StagedUploadTicket>;
127
+ /**
128
+ * Send the bytes to storage using the ticket's URL and headers.
129
+ *
130
+ * It does NOT go through the platform and carries none of your credentials —
131
+ * the URL is the authorisation. Resolves on a 2xx and throws the store's
132
+ * status otherwise.
133
+ */
134
+ put(ticket: StagedUploadTicket, body: UploadBody): Promise<void>;
135
+ /** One upload's state. Unknown, or another principal's, is `UPLOAD_NOT_FOUND`. */
136
+ get(id: string, options?: Call): Promise<StagedUpload>;
137
+ /**
138
+ * Turn a staged upload into a CMS asset.
139
+ *
140
+ * This is the consuming call: it verifies the object is there and is what was
141
+ * signed, spends the upload, and ingests the bytes. An upload can be consumed
142
+ * once — a second call is `UPLOAD_ALREADY_CONSUMED`.
143
+ */
144
+ claimAsAsset(id: string, input?: ClaimAsAssetInput, options?: Call): Promise<unknown>;
145
+ }
146
+ export declare function createUploadsModule(transport: Transport, fetchImpl: typeof globalThis.fetch): UploadsModule;
@@ -0,0 +1,72 @@
1
+ import { PLATFORM_PASSTHROUGH_KEYS, ValidationError, } from '@sixtyninecommerce/core';
2
+ /**
3
+ * `headers` carries the exact header NAMES a signature was computed over.
4
+ *
5
+ * Case-converting them would rewrite `content-type` to `contentType`, and the
6
+ * PUT would then go out without the header storage is checking for — a
7
+ * signature failure that looks like a credential problem and is not.
8
+ */
9
+ const PRESERVE = new Set([...PLATFORM_PASSTHROUGH_KEYS, 'headers']);
10
+ export function createUploadsModule(transport, fetchImpl) {
11
+ return {
12
+ async stage(input, options) {
13
+ if (!Number.isSafeInteger(input.size) || input.size < 1) {
14
+ // Refused here rather than sent: a fractional or negative size would be
15
+ // rejected by the service as a validation failure whose message is
16
+ // about a field the caller did not type, and `size: NaN` from a missing
17
+ // `blob.size` is the way this goes wrong in practice.
18
+ throw new ValidationError({
19
+ code: 'UPLOAD_SIZE_INVALID',
20
+ message: `size must be the exact whole number of bytes you will upload; received ${String(input.size)}.`,
21
+ status: 400,
22
+ service: 'plugins',
23
+ });
24
+ }
25
+ return await transport.rest('plugins', {
26
+ method: 'POST',
27
+ path: '/uploads/stage',
28
+ body: {
29
+ purpose: input.purpose,
30
+ mime_type: input.mimeType,
31
+ size_bytes: input.size,
32
+ client_token: input.clientToken,
33
+ ...(input.filename === undefined ? {} : { filename: input.filename }),
34
+ },
35
+ }, { ...options, preserveKeys: PRESERVE });
36
+ },
37
+ async put(ticket, body) {
38
+ const response = await fetchImpl(ticket.url, {
39
+ method: ticket.method,
40
+ // Exactly what was signed, and nothing else. An extra header is
41
+ // harmless, but a MISSING or altered one is a signature failure.
42
+ headers: { ...ticket.headers },
43
+ body: body,
44
+ });
45
+ if (!response.ok) {
46
+ const detail = await response.text().catch(() => '');
47
+ throw new ValidationError({
48
+ code: 'UPLOAD_PUT_REFUSED',
49
+ message: `The object store refused the upload (HTTP ${String(response.status)}). The commonest causes are a body whose length is not the \`size\` this upload was staged for, ` +
50
+ `a changed \`content-type\`, and a URL that has expired.${detail ? ` Store said: ${detail.slice(0, 300)}` : ''}`,
51
+ status: response.status,
52
+ service: 'plugins',
53
+ });
54
+ }
55
+ },
56
+ async get(id, options) {
57
+ return await transport.rest('plugins', { method: 'GET', path: `/uploads/${encodeURIComponent(id)}` }, { ...options, preserveKeys: PRESERVE });
58
+ },
59
+ async claimAsAsset(id, input, options) {
60
+ const body = await transport.rest('cms', {
61
+ method: 'POST',
62
+ path: '/assets/from-staged',
63
+ body: {
64
+ staged_upload_id: id,
65
+ ...(input?.folderId === undefined ? {} : { folder_id: input.folderId }),
66
+ ...(input?.altText === undefined ? {} : { alt_text: input.altText }),
67
+ },
68
+ }, options);
69
+ return body.data;
70
+ },
71
+ };
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixtyninecommerce/admin",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Back-office SDK for sixtynine-commerce: every capability of the platform as a method",
5
5
  "homepage": "https://github.com/SIXTYNINE-digital/sixtynine-commerce-engine/tree/main/packages/admin-client#readme",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@sixtyninecommerce/core": "^0.2.4"
36
+ "@sixtyninecommerce/core": "^0.4.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "22.19.7",
@@ -41,7 +41,7 @@
41
41
  "vitest": "4.1.6"
42
42
  },
43
43
  "scripts": {
44
- "build": "tsc -p tsconfig.build.json",
44
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
45
45
  "dev": "tsc -p tsconfig.build.json --watch",
46
46
  "typecheck": "tsc --noEmit",
47
47
  "test": "vitest run",