@feasibleone/blong-gogo 1.16.0 → 1.17.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.17.1](https://github.com/feasibleone/blong/compare/blong-gogo-v1.17.0...blong-gogo-v1.17.1) (2026-04-10)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * build ([32951e4](https://github.com/feasibleone/blong/commit/32951e46561648651ee4e032545655007c05e274))
9
+
10
+ ## [1.17.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.16.0...blong-gogo-v1.17.0) (2026-04-10)
11
+
12
+
13
+ ### Features
14
+
15
+ * auto-discover and run blong from a folder with loose handler files ([#120](https://github.com/feasibleone/blong/issues/120)) ([4b407b6](https://github.com/feasibleone/blong/commit/4b407b6d0888eed0bb6cdc8428d82ed15f5152c5))
16
+ * **blong-gogo:** browser compatibility — replace got/tls with ky+undici, add BrowserLog, static tree-shaking split ([#123](https://github.com/feasibleone/blong/issues/123)) ([91b8d1a](https://github.com/feasibleone/blong/commit/91b8d1a159463f9e5a0f4f6d52c4c285989cac43))
17
+ * blong-ui ([9ca1281](https://github.com/feasibleone/blong/commit/9ca1281c0178c69dfc722bc4a5978f649bc1fa0d))
18
+ * improve logging ([5f43391](https://github.com/feasibleone/blong/commit/5f43391a43a99e0dd591401e24c45cd02b3bdba2))
19
+ * simplify api loading ([e6ff342](https://github.com/feasibleone/blong/commit/e6ff34239d7f1f53e3a21d30768ed5ab45042786))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * bin deduplicate ([af41171](https://github.com/feasibleone/blong/commit/af41171c0c4dbb0599f7bbe1c82efc4923fd79c8))
25
+
3
26
  ## [1.16.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.15.0...blong-gogo-v1.16.0) (2026-04-01)
4
27
 
5
28
 
package/bin/blong-dev.ts CHANGED
@@ -1,47 +1,8 @@
1
1
  #!/usr/bin/env -S node --watch --inspect
2
2
 
3
3
  import minimist from 'minimist';
4
- import {existsSync} from 'node:fs';
5
- import {basename, resolve} from 'node:path';
6
- import load from '../src/load.ts';
4
+ import {autoRun} from '../src/loadServer.ts';
7
5
 
8
6
  const argv: {_: string[]} = minimist(process.argv.slice(2));
9
- const cwd = process.cwd();
10
- const target = argv._[0];
11
7
 
12
- if (target) {
13
- // Explicit file provided — load it directly (existing behavior)
14
- (await import(resolve(target))).default(load);
15
- } else {
16
- // Auto-detect what to run based on available files in the current directory.
17
- const indexFile = resolve(cwd, 'index.ts');
18
- const serverFile = resolve(cwd, 'server.ts');
19
- const browserFile = resolve(cwd, 'browser.ts');
20
-
21
- if (existsSync(indexFile)) {
22
- (await import(indexFile)).default(load);
23
- } else if (existsSync(serverFile) && existsSync(browserFile)) {
24
- const {default: server} = await import(serverFile);
25
- const {default: browser} = await import(browserFile);
26
- const name = basename(cwd);
27
- const platforms: Awaited<ReturnType<typeof load>>[] = await Promise.all([
28
- load(server, name, name, ['microservice', 'integration', 'dev']),
29
- load(browser, name, name, ['microservice', 'integration', 'dev']),
30
- ]);
31
- for (const platform of platforms) await platform.start();
32
- await platforms[1].test();
33
- if (process.env.CI) for (const platform of platforms) await platform.stop();
34
- } else if (existsSync(serverFile)) {
35
- const {default: server} = await import(serverFile);
36
- const name = basename(cwd);
37
- const platform = await load(server, name, name, ['microservice', 'integration', 'dev']);
38
- await platform.start();
39
- await platform.test();
40
- if (process.env.CI) await platform.stop();
41
- } else {
42
- throw new Error(
43
- `No index.ts, server.ts, or browser.ts found in ${cwd}. ` +
44
- 'Run blong from a suite or realm folder, or provide a file path.',
45
- );
46
- }
47
- }
8
+ await autoRun({cwd: process.cwd(), target: argv._[0]});
package/bin/blong.ts CHANGED
@@ -1,48 +1,8 @@
1
1
  #!/usr/bin/env -S node
2
2
 
3
3
  import minimist from 'minimist';
4
- import { existsSync } from 'node:fs';
5
- import { basename, join, resolve } from 'node:path';
6
- import load from '../src/load.ts';
4
+ import {autoRun} from '../src/loadServer.ts';
7
5
 
8
6
  const argv: {_: string[]} = minimist(process.argv.slice(2));
9
- const cwd = process.cwd();
10
- const target = argv._[0];
11
7
 
12
- if (target) {
13
- // Explicit file provided — load it directly
14
- (await import(resolve(target))).default(load);
15
- } else {
16
- const indexFile = join(cwd, 'index.ts');
17
- const serverFile = join(cwd, 'server.ts');
18
- const browserFile = join(cwd, 'browser.ts');
19
- const name = basename(cwd);
20
-
21
- if (existsSync(indexFile)) {
22
- // index.ts exists — use it directly (suite or realm with a custom runner)
23
- (await import(indexFile)).default(load);
24
- } else if (existsSync(serverFile) && existsSync(browserFile)) {
25
- // Both server.ts and browser.ts — two-platform (suite or realm with browser)
26
- const {default: serverDef} = await import(serverFile);
27
- const {default: browserDef} = await import(browserFile);
28
- const platforms: Awaited<ReturnType<typeof load>>[] = await Promise.all([
29
- load(serverDef, name, name, ['microservice', 'integration', 'dev']),
30
- load(browserDef, name, name, ['microservice', 'integration', 'dev']),
31
- ]);
32
- for (const platform of platforms) await platform.start();
33
- await platforms[1].test();
34
- if (process.env.CI) for (const platform of platforms) await platform.stop();
35
- } else if (existsSync(serverFile)) {
36
- // Only server.ts — suite or realm (loadRealm detects and wraps realms automatically)
37
- const {default: serverDef} = await import(serverFile);
38
- const platform = await load(serverDef, name, name, ['microservice', 'integration', 'dev']);
39
- await platform.start();
40
- await platform.test();
41
- if (process.env.CI) await platform.stop();
42
- } else {
43
- throw new Error(
44
- `No index.ts or server.ts found in ${cwd}. ` +
45
- 'Run blong from a suite or realm folder, or provide a file path.',
46
- );
47
- }
48
- }
8
+ await autoRun({cwd: process.cwd(), target: argv._[0]});
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@feasibleone/blong-gogo",
3
- "version": "1.16.0",
3
+ "version": "1.17.1",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/feasibleone/blong.git"
6
6
  },
7
7
  "type": "module",
8
8
  "exports": {
9
- ".": "./src/load.ts",
9
+ ".": {
10
+ "browser": "./src/browser.ts",
11
+ "import": "./src/load.ts"
12
+ },
13
+ "./browser": "./src/adapter/browser.ts",
10
14
  "./ConfigRuntime.js": "./src/ConfigRuntime.ts"
11
15
  },
12
16
  "bin": {
@@ -16,7 +20,7 @@
16
20
  },
17
21
  "dependencies": {
18
22
  "@apidevtools/swagger-parser": "^12.1.0",
19
- "@aws-sdk/client-s3": "^3.1003.0",
23
+ "@aws-sdk/client-s3": "^3.1028.0",
20
24
  "@fastify/basic-auth": "^6.2.0",
21
25
  "@fastify/bearer-auth": "^10.1.2",
22
26
  "@fastify/cookie": "^11.0.2",
@@ -35,7 +39,7 @@
35
39
  "ajv-formats": "^3.0.1",
36
40
  "browser-process-hrtime": "^1.0.0",
37
41
  "chokidar": "^5.0.0",
38
- "fastify": "^5.8.2",
42
+ "fastify": "^5.8.4",
39
43
  "fastify-plugin": "^5.1.0",
40
44
  "glob": "^13.0.6",
41
45
  "got": "^14.6.6",
@@ -56,6 +60,7 @@
56
60
  "reconnect-core": "^1.3.0",
57
61
  "typebox": "^1.1.5",
58
62
  "ulidx": "^2.4.1",
63
+ "undici": "^8.0.1",
59
64
  "ut-bitsyntax": "^6.2.7",
60
65
  "ut-dns-discovery": "^6.2.3",
61
66
  "ut-function.interpolate": "1.1.3",
@@ -63,7 +68,7 @@
63
68
  "ut-function.timing": "^1.2.0",
64
69
  "ut-port": "^6.45.2",
65
70
  "uuid": "^13.0.0",
66
- "yaml": "^2.8.2"
71
+ "yaml": "^2.8.3"
67
72
  },
68
73
  "devDependencies": {
69
74
  "@rushstack/eslint-config": "^4.6.4",
@@ -72,10 +77,12 @@
72
77
  "@rushstack/heft-typescript-plugin": "^1.3.1",
73
78
  "@types/node": "^24",
74
79
  "eslint": "~9.39.2",
80
+ "playwright": "^1.58.2",
75
81
  "tap": "^21.6.2",
76
82
  "typescript": "^5.9.3"
77
83
  },
78
84
  "scripts": {
85
+ "browser-check": "node scripts/browser-compat-check.mjs",
79
86
  "build": "true",
80
87
  "ci-publish": "node ../../common/scripts/install-run-rush-pnpm.js publish --access public --provenance",
81
88
  "ci-unit": "tap src/ConfigRuntime.test.ts src/lib.test.ts --allow-incomplete-coverage"
@@ -0,0 +1,144 @@
1
+ import {Internal, type ILog} from '@feasibleone/blong/types';
2
+ import {pino, type Level, type Logger, type LoggerOptions} from 'pino';
3
+
4
+ // ── level constants (pino uses numeric levels matching bunyan) ──────────────
5
+ const TRACE = 10, DEBUG = 20, INFO = 30, WARN = 40, ERROR = 50, FATAL = 60;
6
+
7
+ const nameFromLevel: Record<number, string> = {
8
+ [TRACE]: 'trace', [DEBUG]: 'debug', [INFO]: 'info',
9
+ [WARN]: 'warn', [ERROR]: 'error', [FATAL]: 'fatal',
10
+ };
11
+
12
+ const LEVEL_CSS: Record<string, string> = {
13
+ trace: 'color: grey',
14
+ debug: 'color: blue',
15
+ info: 'color: cyan',
16
+ warn: 'color: magenta',
17
+ error: 'color: red',
18
+ fatal: 'color: red; font-weight: bold',
19
+ };
20
+ const DEFAULT_CSS = {
21
+ def: 'color: black',
22
+ msg: 'color: darkblue',
23
+ service: 'color: darkorange',
24
+ mtid: 'color: Magenta',
25
+ src: 'color: DimGray; font-style: italic; font-size: 0.9em',
26
+ };
27
+
28
+ // Fields written separately; everything else goes into the `details` object
29
+ const SKIP = new Set([
30
+ 'name', 'hostname', 'pid', 'level', 'component', 'msg', 'time', 'v',
31
+ 'src', 'error', 'clientReq', 'clientRes', 'req', 'res',
32
+ '$meta', 'mtid', 'jsException', 'service',
33
+ ]);
34
+
35
+ export interface IBrowserLogConfig {
36
+ level?: Level;
37
+ /** Route each log call to the matching console.warn / console.error etc.
38
+ * instead of always using console.log. Default: false */
39
+ logByLevel?: boolean;
40
+ }
41
+
42
+ function write(rec: Record<string, unknown>, logByLevel: boolean): void {
43
+ const level = rec.level as number;
44
+ const levelKey = nameFromLevel[level] ?? 'info';
45
+ const paddedLevel = levelKey.toUpperCase().padStart(5);
46
+
47
+ // Choose the console method
48
+ let consoleMethod: (...a: unknown[]) => void = console.log; // eslint-disable-line no-console
49
+ if (logByLevel) {
50
+ const mapped = level <= TRACE ? 'debug' : level >= FATAL ? 'error' : levelKey;
51
+ consoleMethod = (typeof (console as Record<string, unknown>)[mapped] === 'function'
52
+ ? (console as Record<string, (...a: unknown[]) => void>)[mapped]
53
+ : console.log); // eslint-disable-line no-console
54
+ }
55
+
56
+ const levelCss =
57
+ level < DEBUG ? LEVEL_CSS.trace :
58
+ level < INFO ? LEVEL_CSS.debug :
59
+ level < WARN ? LEVEL_CSS.info :
60
+ level < ERROR ? LEVEL_CSS.warn :
61
+ level < FATAL ? LEVEL_CSS.error : LEVEL_CSS.fatal;
62
+
63
+ // any fields not in the skip list become the expandable details object
64
+ const details: Record<string, unknown> = {};
65
+ for (const [k, v] of Object.entries(rec)) {
66
+ if (v != null && !SKIP.has(k)) details[k] = v;
67
+ }
68
+ const hasDetails = Object.keys(details).length > 0;
69
+
70
+ const loggerName = rec.childName
71
+ ? `${rec.name}/${rec.childName}`
72
+ : (rec.name as string | undefined) ?? '';
73
+
74
+ const time = rec.time instanceof Date
75
+ ? rec.time.toISOString().slice(11, 23)
76
+ : new Date().toISOString().slice(11, 23);
77
+
78
+ const label =
79
+ (rec.$meta as Record<string, string> | undefined)?.method ??
80
+ (rec.$meta as Record<string, string> | undefined)?.opcode ??
81
+ (rec.msg as string | undefined) ?? '';
82
+
83
+ const fmt = `[%s] %c%s%c %s%c %s: %c%s %c%s${hasDetails ? ' %c%o' : ''}`;
84
+ const args: unknown[] = [
85
+ fmt,
86
+ time,
87
+ levelCss, paddedLevel,
88
+ DEFAULT_CSS.service, rec.service ?? '',
89
+ DEFAULT_CSS.def, loggerName,
90
+ DEFAULT_CSS.mtid, (rec.mtid as string | undefined) ?? '',
91
+ DEFAULT_CSS.msg, label,
92
+ ];
93
+ if (hasDetails) args.push(DEFAULT_CSS.src, details);
94
+
95
+ consoleMethod(...args);
96
+
97
+ if (rec.error && (rec.error as {stack?: string}).stack)
98
+ console.error(rec.error); // eslint-disable-line no-console
99
+ }
100
+
101
+ export default class BrowserLog extends Internal implements ILog {
102
+ #logger: Logger;
103
+ #config: IBrowserLogConfig = {level: 'info', logByLevel: false};
104
+
105
+ public constructor(config: IBrowserLogConfig) {
106
+ super();
107
+ this.merge(this.#config, config);
108
+ const logByLevel = this.#config.logByLevel ?? false;
109
+ this.#logger = pino({
110
+ level: this.#config.level ?? 'info',
111
+ browser: {
112
+ asObject: true,
113
+ write: (rec: Record<string, unknown>) => write(rec, logByLevel),
114
+ },
115
+ });
116
+ }
117
+
118
+ public child<T extends string>(...params: Parameters<Logger<never>['child']>): Logger<T> {
119
+ return this.#logger.child(...params) as Logger<T>;
120
+ }
121
+
122
+ public logger(
123
+ level: LoggerOptions['level'] = this.#config.level,
124
+ bindings: object,
125
+ ): ReturnType<ILog['logger']> {
126
+ const child = this.#logger.child(bindings, {level});
127
+ const result = {trace: null, debug: null, info: null, warn: null, error: null, fatal: null};
128
+ switch (level) {
129
+ case 'trace':
130
+ result.trace = child.trace.bind(child);
131
+ case 'debug': // eslint-disable-line no-fallthrough
132
+ result.debug = child.debug.bind(child);
133
+ case 'info': // eslint-disable-line no-fallthrough
134
+ result.info = child.info.bind(child);
135
+ case 'warn': // eslint-disable-line no-fallthrough
136
+ result.warn = child.warn.bind(child);
137
+ case 'error': // eslint-disable-line no-fallthrough
138
+ result.error = child.error.bind(child);
139
+ case 'fatal': // eslint-disable-line no-fallthrough
140
+ result.fatal = child.fatal.bind(child);
141
+ }
142
+ return result;
143
+ }
144
+ }
package/src/Realm.ts CHANGED
@@ -17,16 +17,21 @@ export default class RealmImpl implements IRealm {
17
17
 
18
18
  public constructor(
19
19
  config: {
20
- realm?: {logLevel?: Parameters<ILog['logger']>[0]};
20
+ server: {realm: {logLevel: Parameters<ILog['logger']>[0]}};
21
+ browser: {realm: {logLevel: Parameters<ILog['logger']>[0]}};
21
22
  name: string;
22
23
  pkg: {name: string; version: string};
23
24
  },
24
25
  {log, registry}: {log?: ILog; registry?: IRegistry},
26
+ platform: 'server' | 'browser',
25
27
  ) {
26
28
  this.#config = config;
27
29
  this.#registry = registry;
28
30
  this.#log = log;
29
- this.#logger = this.#log?.logger(config.realm?.logLevel, {name: 'realm'});
31
+ this.#logger = this.#log?.logger(config[platform]?.realm?.logLevel, {
32
+ name: config.name,
33
+ context: `${platform}`,
34
+ });
30
35
  }
31
36
 
32
37
  private _addModuleInternal(name: string | symbol, mod: IRegistry): void {
@@ -39,7 +44,10 @@ export default class RealmImpl implements IRealm {
39
44
  }
40
45
 
41
46
  public addModule(name: string, mod: IRegistry): void {
42
- this.#logger?.debug?.(`Module ${this.#config.name}.${name}`);
47
+ this.#logger?.debug?.(
48
+ {$meta: {mtid: 'event', method: 'module.add'}},
49
+ `${this.#config.name}.${name}`,
50
+ );
43
51
  this._addModuleInternal(name, mod);
44
52
  }
45
53
 
@@ -58,8 +66,16 @@ export default class RealmImpl implements IRealm {
58
66
  }
59
67
  });
60
68
  if (source.length === 1)
61
- this.#logger?.debug?.(`Layer ${this.#config.name}.${layerName} ${source[0]}`);
69
+ // this.#logger?.debug?.(`Layer ${this.#config.name}.${layerName} ${source[0]}`);
70
+ this.#logger?.debug?.(
71
+ {$meta: {mtid: 'event', method: 'layer.add'}},
72
+ `${this.#config.name}.${layerName} ${source[0]}`,
73
+ );
62
74
  else if (!source.length) this.#logger?.debug?.(`Layer ${this.#config.name}.${layerName}`);
63
- else this.#logger?.debug?.({source}, `Layer ${this.#config.name}.${layerName}`);
75
+ else
76
+ this.#logger?.debug?.(
77
+ {$meta: {mtid: 'event', method: 'layer.add'}},
78
+ `${this.#config.name}.${layerName} ${source}`,
79
+ );
64
80
  }
65
81
  }
package/src/Registry.ts CHANGED
@@ -12,18 +12,18 @@ import type {
12
12
  IRemote,
13
13
  IRpcServer,
14
14
  } from '@feasibleone/blong/types';
15
- import { Internal } from '@feasibleone/blong/types';
15
+ import {Internal} from '@feasibleone/blong/types';
16
16
  import nodeAssert from 'node:assert';
17
17
  import PQueue from 'p-queue';
18
- import { Type } from 'typebox';
19
- import { monotonicFactory } from 'ulidx';
18
+ import {Type} from 'typebox';
19
+ import {monotonicFactory} from 'ulidx';
20
20
  import merge from 'ut-function.merge';
21
- import { v4 as uuid4, v7 as uuid7 } from 'uuid';
21
+ import {v4 as uuid4, v7 as uuid7} from 'uuid';
22
22
 
23
- import { createAttachCheckpoint } from './checkpoint.ts';
24
- import { methodId, methodParts } from './lib.ts';
25
- import type { IResolution } from './Resolution.ts';
26
- import type { IWatch } from './Watch.ts';
23
+ import {createAttachCheckpoint} from './checkpoint.ts';
24
+ import {methodId, methodParts} from './lib.ts';
25
+ import type {IResolution} from './Resolution.ts';
26
+ import type {IWatch} from './Watch.ts';
27
27
 
28
28
  type MatchMethodsCallback = (
29
29
  name: string,
@@ -103,7 +103,9 @@ export default class Registry extends Internal implements IRegistry {
103
103
  this.#log = log;
104
104
  this.#watch = watch;
105
105
  this.#apiSchema = apiSchema;
106
- this.#attachCheckpoint = createAttachCheckpoint(this.#config.checkpointMode ?? 'production');
106
+ this.#attachCheckpoint = createAttachCheckpoint(
107
+ this.#config.checkpointMode ?? 'production',
108
+ );
107
109
  this.#rpcServer?.setAttachCheckpoint?.(this.#attachCheckpoint);
108
110
  }
109
111
 
package/src/Watch.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ handler,
2
3
  Internal,
3
4
  kind,
4
5
  type IApiSchema,
@@ -34,6 +35,8 @@ export interface IWatch {
34
35
  setConfigRuntime?(configRuntime: ConfigRuntime): void;
35
36
  }
36
37
 
38
+ const isYaml = (filename: string): boolean => /\.ya?ml$/i.test(filename);
39
+ const isJSON = (filename: string): boolean => /\.jsonl?$/i.test(filename);
37
40
  const isCode = (filename: string): boolean => /(?<!\.d)\.m?(t|j)sx?$/i.test(filename);
38
41
  const isLayerActivation = (filename: string): boolean =>
39
42
  /^layer\.(server|browser)\.[mc]?[tj]sx?$/i.test(filename);
@@ -182,6 +185,7 @@ export default class Watch extends Internal implements IWatch {
182
185
  const validations = [];
183
186
  const apis = [];
184
187
  const libs = [];
188
+ const assets = [];
185
189
  const handlerFilenames = [];
186
190
  let latest = 0;
187
191
  const allFiles = await scan(dir);
@@ -247,6 +251,17 @@ export default class Watch extends Internal implements IWatch {
247
251
  handlerFilenames.push({name, filename});
248
252
  }
249
253
  }
254
+ const assetFiles = allFiles.filter(
255
+ entry => entry.isFile() && (isYaml(entry.name) || isJSON(entry.name)),
256
+ );
257
+ for (const assetFile of assetFiles) {
258
+ const filename = join(dir, assetFile.name);
259
+ assets.push(
260
+ handler(() => ({
261
+ assets: {[basename(filename)]: `file://${filename}`},
262
+ })),
263
+ );
264
+ }
250
265
  this.#handlerFolders.set(dir, config);
251
266
  return api => {
252
267
  if (validations.length)
@@ -261,6 +276,12 @@ export default class Watch extends Internal implements IWatch {
261
276
  config.name + '.' + basename(dir) + '.api',
262
277
  relative('.', dir),
263
278
  );
279
+ if (assets.length)
280
+ api[basename(dir) + '.asset'](
281
+ assets,
282
+ config.name + '.' + basename(dir) + '.asset',
283
+ relative('.', dir),
284
+ );
264
285
  if (handlers.length)
265
286
  api[basename(dir)](
266
287
  [...libs, ...handlers],
@@ -1,14 +1,13 @@
1
1
  import type {Errors, IErrorMap, IMeta} from '@feasibleone/blong/types';
2
2
  import {adapter} from '@feasibleone/blong/types';
3
- import got, {type HttpsOptions, type Options} from 'got';
4
-
5
- import tls from '../../tls.ts';
3
+ import ky, {type Options as KyOptions} from 'ky';
6
4
 
7
5
  export interface IConfig {
8
6
  tls?: {
9
7
  key?: string;
10
8
  cert?: string;
11
9
  ca?: string | string[];
10
+ crl?: string;
12
11
  };
13
12
  url?: string;
14
13
  }
@@ -22,7 +21,7 @@ let _errors: Errors<typeof errorMap>;
22
21
  export default adapter<IConfig>(({utError}) => {
23
22
  _errors ||= utError.register(errorMap);
24
23
 
25
- let https: HttpsOptions;
24
+ let kyInstance: typeof ky = ky;
26
25
  return {
27
26
  activation: {
28
27
  default: {
@@ -31,7 +30,36 @@ export default adapter<IConfig>(({utError}) => {
31
30
  },
32
31
  async init(...configs: object[]) {
33
32
  await super.init(...configs);
34
- https = tls(this.config, true);
33
+ if (this.config.tls) {
34
+ // Dynamic imports — only run on the server; @vite-ignore prevents
35
+ // Vite from trying to bundle these Node.js-only modules for the browser.
36
+ const [{Agent}, {readFileSync}] = await Promise.all([
37
+ import(/* @vite-ignore */ 'undici') as Promise<typeof import('undici')>,
38
+ import(/* @vite-ignore */ 'node:fs') as Promise<typeof import('node:fs')>,
39
+ ]);
40
+ const {tls} = this.config;
41
+ const agent = new Agent({
42
+ connect: {
43
+ minVersion: 'TLSv1.3',
44
+ ...(tls.key && {key: readFileSync(tls.key)}),
45
+ ...(tls.cert && {cert: readFileSync(tls.cert)}),
46
+ ...(tls.ca && {
47
+ ca: Array.isArray(tls.ca)
48
+ ? tls.ca.map(f => readFileSync(f))
49
+ : readFileSync(tls.ca),
50
+ }),
51
+ ...(tls.crl && {crl: readFileSync(tls.crl)}),
52
+ },
53
+ });
54
+ kyInstance = ky.create({
55
+ fetch: (url, options) =>
56
+ fetch(url as string, {
57
+ ...(options as RequestInit),
58
+ // @ts-expect-error: undici dispatcher is not in the standard RequestInit type
59
+ dispatcher: agent,
60
+ }),
61
+ });
62
+ }
35
63
  },
36
64
  start() {
37
65
  super.connect();
@@ -50,57 +78,56 @@ export default adapter<IConfig>(({utError}) => {
50
78
  json,
51
79
  }: {
52
80
  path: string;
53
- query: string;
81
+ query: string | Record<string, string>;
54
82
  url: URL;
55
- responseType: Options['responseType'];
56
- method: Options['method'];
57
- headers: Options['headers'];
58
- body: Options['body'];
59
- form: Options['form'];
60
- json: Options['json'];
83
+ responseType: 'json' | 'text' | 'buffer';
84
+ method: string;
85
+ headers: Record<string, string>;
86
+ body: BodyInit;
87
+ form: Record<string, string>;
88
+ json: unknown;
61
89
  },
62
- {stream}: IMeta,
90
+ _meta: IMeta,
63
91
  ) {
64
92
  try {
65
93
  this.log.debug?.({
66
94
  req: {
67
- method: method.toUpperCase(),
95
+ method: (method || 'POST').toUpperCase(),
68
96
  url,
69
97
  headers,
70
98
  body,
71
99
  json,
72
100
  },
73
101
  });
74
- const result = (await got({
75
- url,
76
- searchParams,
77
- https,
102
+ const kyOptions: KyOptions = {
78
103
  method: method || 'POST',
79
104
  headers,
80
- responseType,
81
- body,
82
- form,
83
- json,
84
105
  throwHttpErrors: false,
85
- followRedirect: false,
86
- isStream: !!stream,
87
- })) as {
88
- statusCode: number;
89
- statusMessage: string;
90
- headers: Record<string, unknown>;
91
- body: unknown;
106
+ redirect: 'manual',
107
+ ...(json != null ? {json} : {}),
108
+ ...(form != null ? {body: new URLSearchParams(form)} : {}),
109
+ ...(body != null && json == null && form == null ? {body} : {}),
110
+ ...(searchParams != null ? {searchParams: searchParams as Record<string, string>} : {}),
111
+ };
112
+ const res = await kyInstance(url.toString(), kyOptions);
113
+ const resolvedBody =
114
+ responseType === 'buffer'
115
+ ? await res.arrayBuffer()
116
+ : responseType === 'text'
117
+ ? await res.text()
118
+ : await res.json().catch(() => null);
119
+ const result = {
120
+ statusCode: res.status,
121
+ statusMessage: res.statusText,
122
+ headers: Object.fromEntries(res.headers.entries()),
123
+ body: resolvedBody,
92
124
  };
93
125
  this.log.debug?.({
94
126
  req: {
95
127
  url,
96
- method: method.toUpperCase(),
97
- },
98
- res: {
99
- statusCode: result.statusCode,
100
- statusMessage: result.statusMessage,
101
- headers: result.headers,
102
- body: result.body,
128
+ method: (method || 'POST').toUpperCase(),
103
129
  },
130
+ res: result,
104
131
  });
105
132
  return result;
106
133
  } catch (error) {