@kubb/studio 5.3.2 → 5.3.3
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 +4 -0
- package/dist/index.cjs +220 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +224 -17
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +2 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +39 -3
- package/dist/protocol.js +2 -1
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -128,6 +128,10 @@ if (finished.status === 'failed') throw new Error(finished.error)
|
|
|
128
128
|
const snapshot = finished.snapshot
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
A snapshot job packs the tarball on the agent, not on Studio. The agent `PUT`s an empty request to
|
|
132
|
+
a path Studio provides, gets back a redirect to a short-lived storage URL, and uploads the tarball
|
|
133
|
+
there. The storage URL never crosses the WebSocket.
|
|
134
|
+
|
|
131
135
|
## Protocol
|
|
132
136
|
|
|
133
137
|
`@kubb/studio/protocol` holds the WebSocket message types shared by both ends, so the agent and
|
package/dist/index.cjs
CHANGED
|
@@ -8,18 +8,21 @@ let node_child_process = require("node:child_process");
|
|
|
8
8
|
let node_fs_promises = require("node:fs/promises");
|
|
9
9
|
let node_path = require("node:path");
|
|
10
10
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
11
|
+
let _kubb_core = require("@kubb/core");
|
|
11
12
|
let ofetch = require("ofetch");
|
|
12
13
|
let node_crypto = require("node:crypto");
|
|
13
14
|
let unstorage = require("unstorage");
|
|
14
15
|
let unstorage_drivers_fs = require("unstorage/drivers/fs");
|
|
15
16
|
unstorage_drivers_fs = require_rolldown_runtime.__toESM(unstorage_drivers_fs, 1);
|
|
16
|
-
let _kubb_core = require("@kubb/core");
|
|
17
17
|
let tinyexec = require("tinyexec");
|
|
18
18
|
let magicast = require("magicast");
|
|
19
19
|
let node_fs = require("node:fs");
|
|
20
20
|
let node_module = require("node:module");
|
|
21
21
|
let node_url = require("node:url");
|
|
22
22
|
let remeda = require("remeda");
|
|
23
|
+
let node_os = require("node:os");
|
|
24
|
+
let node_zlib = require("node:zlib");
|
|
25
|
+
let tsdown = require("tsdown");
|
|
23
26
|
let ws = require("ws");
|
|
24
27
|
ws = require_rolldown_runtime.__toESM(ws, 1);
|
|
25
28
|
let node_timers_promises = require("node:timers/promises");
|
|
@@ -561,19 +564,20 @@ async function runRegistration({ token, studioUrl, poolSize }) {
|
|
|
561
564
|
* Called on process termination or server close. A failed notify is logged and swallowed: the
|
|
562
565
|
* local socket is already gone, and failing teardown must not block shutdown or reconnect.
|
|
563
566
|
*/
|
|
564
|
-
async function disconnect({ sessionId, token, studioUrl, slug }) {
|
|
567
|
+
async function disconnect({ sessionId, token, studioUrl, slug, logLevel }) {
|
|
565
568
|
const url = `${studioUrl}/api/agent/sessions/${sessionId}/disconnect`;
|
|
566
569
|
const tag = slug ?? "agent";
|
|
570
|
+
const canLog = logLevel !== void 0 && logLevel > _kubb_core.logLevel.silent;
|
|
567
571
|
try {
|
|
568
572
|
await (0, ofetch.ofetch)(url, {
|
|
569
573
|
method: "POST",
|
|
570
574
|
headers: { Authorization: `Bearer ${token}` }
|
|
571
575
|
});
|
|
572
|
-
console.
|
|
576
|
+
if (canLog) console.error((0, node_util.styleText)("green", `[${tag}] Disconnected from Studio`));
|
|
573
577
|
} catch (error) {
|
|
574
578
|
const statusCode = error?.statusCode;
|
|
575
579
|
if (statusCode !== void 0 && statusCode >= 400 && statusCode < 500) return;
|
|
576
|
-
console.warn((0, node_util.styleText)("yellow", `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`));
|
|
580
|
+
if (canLog) console.warn((0, node_util.styleText)("yellow", `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`));
|
|
577
581
|
}
|
|
578
582
|
}
|
|
579
583
|
/**
|
|
@@ -651,7 +655,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
651
655
|
}
|
|
652
656
|
//#endregion
|
|
653
657
|
//#region package.json
|
|
654
|
-
var version = "5.3.
|
|
658
|
+
var version = "5.3.3";
|
|
655
659
|
//#endregion
|
|
656
660
|
//#region src/hooks.ts
|
|
657
661
|
/**
|
|
@@ -1584,6 +1588,134 @@ async function generate({ config, hooks }) {
|
|
|
1584
1588
|
}
|
|
1585
1589
|
}
|
|
1586
1590
|
//#endregion
|
|
1591
|
+
//#region src/snapshotPackage.ts
|
|
1592
|
+
const gzipAsync = (0, node_util.promisify)(node_zlib.gzip);
|
|
1593
|
+
/**
|
|
1594
|
+
* Maps a generated file's path to its place inside the tarball, stripping everything before a
|
|
1595
|
+
* `src`/`dist` segment and any `..`/empty path segment so a crafted file name cannot escape the
|
|
1596
|
+
* `package/` root.
|
|
1597
|
+
*/
|
|
1598
|
+
function packagePath(filePath) {
|
|
1599
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
1600
|
+
return `package/${(normalized.match(/\/(?:src|dist)\/.*$/)?.[0].slice(1) ?? normalized.replace(/^\/+/, "")).split("/").filter((part) => part && part !== "." && part !== "..").join("/")}`;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Splits a tarball entry path into the legacy 100-byte `name` field and, when the path does not
|
|
1604
|
+
* fit, the 155-byte USTAR `prefix` field that extends it. Throws rather than silently truncating
|
|
1605
|
+
* a path the format cannot address (max 256 bytes: 100 name + 1 separator + 155 prefix).
|
|
1606
|
+
*/
|
|
1607
|
+
function splitEntryPath(path) {
|
|
1608
|
+
if (Buffer.byteLength(path, "utf8") <= 100) return {
|
|
1609
|
+
name: path,
|
|
1610
|
+
prefix: ""
|
|
1611
|
+
};
|
|
1612
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
1613
|
+
if (path[i] !== "/") continue;
|
|
1614
|
+
const prefix = path.slice(0, i);
|
|
1615
|
+
const name = path.slice(i + 1);
|
|
1616
|
+
if (Buffer.byteLength(prefix, "utf8") <= 155 && Buffer.byteLength(name, "utf8") <= 100) return {
|
|
1617
|
+
name,
|
|
1618
|
+
prefix
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
throw new Error(`Snapshot path is too long for a tar entry: ${path}`);
|
|
1622
|
+
}
|
|
1623
|
+
function header(path, size) {
|
|
1624
|
+
const { name, prefix } = splitEntryPath(path);
|
|
1625
|
+
const value = Buffer.alloc(512);
|
|
1626
|
+
value.write(name, 0, "utf8");
|
|
1627
|
+
value.write("0000644\0", 100, "ascii");
|
|
1628
|
+
value.write("0000000\0", 108, "ascii");
|
|
1629
|
+
value.write("0000000\0", 116, "ascii");
|
|
1630
|
+
value.write(`${size.toString(8).padStart(11, "0")}\0`, 124, "ascii");
|
|
1631
|
+
value.write(`${Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0")}\0`, 136, "ascii");
|
|
1632
|
+
value.fill(32, 148, 156);
|
|
1633
|
+
value.write("0", 156, "ascii");
|
|
1634
|
+
value.write("ustar\0", 257, "ascii");
|
|
1635
|
+
value.write("00", 263, "ascii");
|
|
1636
|
+
value.write("0000000\0", 265, "ascii");
|
|
1637
|
+
value.write("0000000\0", 297, "ascii");
|
|
1638
|
+
value.write(prefix, 345, "utf8");
|
|
1639
|
+
const checksum = [...value].reduce((sum, byte) => sum + byte, 0);
|
|
1640
|
+
value.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
|
|
1641
|
+
return value;
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Packs a generation's files into a gzipped, npm-installable tarball: a `package/` root with the
|
|
1645
|
+
* generated sources, a `tsdown`-built `dist/` (esm + cjs), and a `package.json` manifest.
|
|
1646
|
+
*/
|
|
1647
|
+
async function createSnapshotPackage(files, packageInfo) {
|
|
1648
|
+
const root = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "kubb-snapshot-"));
|
|
1649
|
+
const dist = (0, node_path.join)(root, "dist");
|
|
1650
|
+
const resolvedPaths = Object.entries(files).map(([name, content]) => ({
|
|
1651
|
+
name,
|
|
1652
|
+
content,
|
|
1653
|
+
target: packagePath(name)
|
|
1654
|
+
}));
|
|
1655
|
+
const targetOwners = /* @__PURE__ */ new Map();
|
|
1656
|
+
for (const { name, target } of resolvedPaths) {
|
|
1657
|
+
const owner = targetOwners.get(target);
|
|
1658
|
+
if (owner) throw new Error(`Snapshot has two generated files that sanitize to the same path "${target}": "${owner}" and "${name}"`);
|
|
1659
|
+
targetOwners.set(target, name);
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
await (0, node_fs_promises.mkdir)(dist);
|
|
1663
|
+
await Promise.all(resolvedPaths.map(async ({ content, target }) => {
|
|
1664
|
+
const path = (0, node_path.join)(root, target.slice(8));
|
|
1665
|
+
await (0, node_fs_promises.mkdir)((0, node_path.join)(path, ".."), { recursive: true });
|
|
1666
|
+
await (0, node_fs_promises.writeFile)(path, content);
|
|
1667
|
+
}));
|
|
1668
|
+
const sourceEntries = resolvedPaths.filter(({ name }) => /\.(?:[cm]?[jt]sx?)$/.test(name)).map(({ target }) => (0, node_path.join)(root, target.slice(8)));
|
|
1669
|
+
if (sourceEntries.length) await (0, tsdown.build)({
|
|
1670
|
+
entry: sourceEntries,
|
|
1671
|
+
outDir: dist,
|
|
1672
|
+
format: ["esm", "cjs"],
|
|
1673
|
+
dts: false,
|
|
1674
|
+
sourcemap: false,
|
|
1675
|
+
unbundle: true,
|
|
1676
|
+
report: false,
|
|
1677
|
+
logLevel: "silent",
|
|
1678
|
+
fixedExtension: true
|
|
1679
|
+
});
|
|
1680
|
+
const builtEntries = await Promise.all((await Array.fromAsync((0, node_fs_promises.glob)("**/*", {
|
|
1681
|
+
cwd: dist,
|
|
1682
|
+
withFileTypes: true
|
|
1683
|
+
}))).filter((entry) => entry.isFile()).map(async (entry) => {
|
|
1684
|
+
const filePath = (0, node_path.join)(entry.parentPath, entry.name);
|
|
1685
|
+
return [`package/dist/${(0, node_path.relative)(dist, filePath).split(node_path.sep).join("/")}`, await (0, node_fs_promises.readFile)(filePath, "utf8")];
|
|
1686
|
+
}));
|
|
1687
|
+
const entries = {
|
|
1688
|
+
"package/package.json": JSON.stringify({
|
|
1689
|
+
...packageInfo,
|
|
1690
|
+
type: "module",
|
|
1691
|
+
main: "./dist/index.cjs",
|
|
1692
|
+
module: "./dist/index.mjs",
|
|
1693
|
+
exports: { ".": {
|
|
1694
|
+
import: "./dist/index.mjs",
|
|
1695
|
+
require: "./dist/index.cjs"
|
|
1696
|
+
} }
|
|
1697
|
+
}, null, 2),
|
|
1698
|
+
...Object.fromEntries(resolvedPaths.map(({ content, target }) => [target, content])),
|
|
1699
|
+
...Object.fromEntries(builtEntries)
|
|
1700
|
+
};
|
|
1701
|
+
const chunks = [];
|
|
1702
|
+
for (const [name, content] of Object.entries(entries)) {
|
|
1703
|
+
const bytes = Buffer.from(content);
|
|
1704
|
+
chunks.push(header(name, bytes.length), bytes, Buffer.alloc((512 - bytes.length % 512) % 512));
|
|
1705
|
+
}
|
|
1706
|
+
const bytes = await gzipAsync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
|
|
1707
|
+
return {
|
|
1708
|
+
bytes,
|
|
1709
|
+
integrity: `sha512-${(0, node_crypto.createHash)("sha512").update(bytes).digest("base64")}`
|
|
1710
|
+
};
|
|
1711
|
+
} finally {
|
|
1712
|
+
await (0, node_fs_promises.rm)(root, {
|
|
1713
|
+
recursive: true,
|
|
1714
|
+
force: true
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
//#endregion
|
|
1587
1719
|
//#region src/ws.ts
|
|
1588
1720
|
/**
|
|
1589
1721
|
* How many generated files are read from storage at once when building the
|
|
@@ -1679,7 +1811,7 @@ function sendErrorMessage(ws$4, error, jobId) {
|
|
|
1679
1811
|
/**
|
|
1680
1812
|
* Forwards selected Kubb lifecycle events to Studio as data messages for the active session.
|
|
1681
1813
|
*/
|
|
1682
|
-
function setupEventsStream(ws$5, hooks, jobId) {
|
|
1814
|
+
function setupEventsStream(ws$5, hooks, jobId, options = {}) {
|
|
1683
1815
|
const unhooks = [];
|
|
1684
1816
|
/**
|
|
1685
1817
|
* Registers a listener and keeps its remover, so one generation's listeners come off the session
|
|
@@ -1793,11 +1925,12 @@ function setupEventsStream(ws$5, hooks, jobId) {
|
|
|
1793
1925
|
if (content !== null) files[relativeStoragePath(config.root, path)] = content;
|
|
1794
1926
|
}
|
|
1795
1927
|
});
|
|
1928
|
+
options.onGenerationEnd?.(files);
|
|
1796
1929
|
sendDataMessage({
|
|
1797
1930
|
type: "kubb:generation:end",
|
|
1798
1931
|
data: [{
|
|
1799
1932
|
config,
|
|
1800
|
-
storage: files,
|
|
1933
|
+
storage: options.skipStorage ? {} : files,
|
|
1801
1934
|
peerDependencies,
|
|
1802
1935
|
missingDependencies
|
|
1803
1936
|
}]
|
|
@@ -1914,15 +2047,15 @@ function applyStudioDefaults(options) {
|
|
|
1914
2047
|
* socket, its hook emitter, or its session id alive for the length of the retry interval.
|
|
1915
2048
|
*/
|
|
1916
2049
|
function reconnect(options) {
|
|
1917
|
-
const { signal, retryInterval, onTokenRejected } = options;
|
|
2050
|
+
const { signal, retryInterval, onTokenRejected, logLevel } = options;
|
|
1918
2051
|
if (signal?.aborted) return;
|
|
1919
|
-
console.
|
|
2052
|
+
if (logLevel !== void 0 && logLevel > _kubb_core.logLevel.silent) console.error((0, node_util.styleText)("dim", `Retrying connection in ${retryInterval}ms to Kubb Studio ...`));
|
|
1920
2053
|
const cancel = () => clearTimeout(timer);
|
|
1921
2054
|
const timer = setTimeout(() => {
|
|
1922
2055
|
signal?.removeEventListener("abort", cancel);
|
|
1923
2056
|
if (signal?.aborted) return;
|
|
1924
2057
|
new StudioSession(options).connect().catch((error) => {
|
|
1925
|
-
console.error((0, node_util.styleText)("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
2058
|
+
if (logLevel !== void 0 && logLevel > _kubb_core.logLevel.silent) console.error((0, node_util.styleText)("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
1926
2059
|
if (error instanceof InvalidAgentTokenError) {
|
|
1927
2060
|
onTokenRejected?.(error);
|
|
1928
2061
|
return;
|
|
@@ -1957,6 +2090,7 @@ var StudioSession = class {
|
|
|
1957
2090
|
#heartbeatTimer;
|
|
1958
2091
|
#readyTimer;
|
|
1959
2092
|
#lastPongAt = Date.now();
|
|
2093
|
+
#lastGeneration;
|
|
1960
2094
|
constructor(options) {
|
|
1961
2095
|
this.#options = applyStudioDefaults(options);
|
|
1962
2096
|
}
|
|
@@ -2139,7 +2273,7 @@ var StudioSession = class {
|
|
|
2139
2273
|
* `#disposed` keeps the close event from running this twice, and a shutdown from reconnecting.
|
|
2140
2274
|
*/
|
|
2141
2275
|
async #end({ reason, retry }) {
|
|
2142
|
-
const { studioUrl, token } = this.#options;
|
|
2276
|
+
const { studioUrl, token, logLevel } = this.#options;
|
|
2143
2277
|
if (this.#disposed) return;
|
|
2144
2278
|
this.#disposed = true;
|
|
2145
2279
|
if (reason === "shutdown" && this.#ws) sendAgentMessage(this.#ws, {
|
|
@@ -2151,7 +2285,8 @@ var StudioSession = class {
|
|
|
2151
2285
|
sessionId: this.#session.sessionId,
|
|
2152
2286
|
studioUrl,
|
|
2153
2287
|
token,
|
|
2154
|
-
slug: this.#session.slug
|
|
2288
|
+
slug: this.#session.slug,
|
|
2289
|
+
logLevel
|
|
2155
2290
|
}).catch(() => {});
|
|
2156
2291
|
if (retry) reconnect(this.#options);
|
|
2157
2292
|
}
|
|
@@ -2210,6 +2345,9 @@ var StudioSession = class {
|
|
|
2210
2345
|
case "studio:save":
|
|
2211
2346
|
await this.#handleSave(ws, data, command);
|
|
2212
2347
|
return;
|
|
2348
|
+
case "studio:snapshot":
|
|
2349
|
+
await this.#handleSnapshot(ws, data, command);
|
|
2350
|
+
return;
|
|
2213
2351
|
}
|
|
2214
2352
|
}
|
|
2215
2353
|
async #handleGenerate(ws, data, command) {
|
|
@@ -2233,7 +2371,13 @@ var StudioSession = class {
|
|
|
2233
2371
|
await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
|
|
2234
2372
|
}
|
|
2235
2373
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2236
|
-
|
|
2374
|
+
let generatedFiles;
|
|
2375
|
+
const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, {
|
|
2376
|
+
skipStorage: client?.kind === "ci",
|
|
2377
|
+
onGenerationEnd: (files) => {
|
|
2378
|
+
generatedFiles = files;
|
|
2379
|
+
}
|
|
2380
|
+
})];
|
|
2237
2381
|
try {
|
|
2238
2382
|
await generate({
|
|
2239
2383
|
config: {
|
|
@@ -2254,6 +2398,7 @@ var StudioSession = class {
|
|
|
2254
2398
|
});
|
|
2255
2399
|
} finally {
|
|
2256
2400
|
for (const remove of detach) remove();
|
|
2401
|
+
this.#lastGeneration = generatedFiles;
|
|
2257
2402
|
}
|
|
2258
2403
|
await this.#hooks.callHook("studio:command:end", {
|
|
2259
2404
|
command,
|
|
@@ -2322,6 +2467,68 @@ var StudioSession = class {
|
|
|
2322
2467
|
refuse(getErrorMessage(error));
|
|
2323
2468
|
}
|
|
2324
2469
|
}
|
|
2470
|
+
async #handleSnapshot(ws, data, command) {
|
|
2471
|
+
const refuse = (message) => sendAgentMessage(ws, {
|
|
2472
|
+
type: "agent:snapshot",
|
|
2473
|
+
jobId: data.jobId,
|
|
2474
|
+
payload: {
|
|
2475
|
+
status: "error",
|
|
2476
|
+
message
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
if (this.#isSandbox) {
|
|
2480
|
+
await this.#warn("Ignored snapshot: a sandbox agent has no project to build a package from");
|
|
2481
|
+
refuse("a sandbox agent has no project to build a package from");
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
const { name, version, peerDependencies, uploadPath } = data.payload;
|
|
2485
|
+
if (!name || !version || !uploadPath) {
|
|
2486
|
+
await this.#warn("Ignored snapshot: the message was missing required fields");
|
|
2487
|
+
refuse("the message was missing required fields");
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
const files = this.#lastGeneration;
|
|
2491
|
+
if (!files) {
|
|
2492
|
+
await this.#warn("Ignored snapshot: no prior generation to pack");
|
|
2493
|
+
refuse("no prior generation exists to pack, run a generation first");
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
try {
|
|
2497
|
+
const { bytes, integrity } = await createSnapshotPackage(files, {
|
|
2498
|
+
name,
|
|
2499
|
+
version,
|
|
2500
|
+
peerDependencies: peerDependencies ?? {}
|
|
2501
|
+
});
|
|
2502
|
+
const { token, studioUrl } = this.#options;
|
|
2503
|
+
const redirect = await fetch(new URL(uploadPath, studioUrl), {
|
|
2504
|
+
method: "PUT",
|
|
2505
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
2506
|
+
redirect: "manual"
|
|
2507
|
+
});
|
|
2508
|
+
const storageUrl = redirect.headers.get("location");
|
|
2509
|
+
if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
|
|
2510
|
+
const response = await fetch(storageUrl, {
|
|
2511
|
+
method: "PUT",
|
|
2512
|
+
body: new Uint8Array(bytes)
|
|
2513
|
+
});
|
|
2514
|
+
if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
|
|
2515
|
+
sendAgentMessage(ws, {
|
|
2516
|
+
type: "agent:snapshot",
|
|
2517
|
+
jobId: data.jobId,
|
|
2518
|
+
payload: {
|
|
2519
|
+
status: "ok",
|
|
2520
|
+
integrity
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
2523
|
+
await this.#hooks.callHook("studio:command:end", {
|
|
2524
|
+
command,
|
|
2525
|
+
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
2526
|
+
});
|
|
2527
|
+
} catch (error) {
|
|
2528
|
+
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
2529
|
+
refuse(getErrorMessage(error));
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2325
2532
|
};
|
|
2326
2533
|
//#endregion
|
|
2327
2534
|
//#region src/client.ts
|